├── pres └── logo.png ├── .gitignore ├── CHANGELOG.md ├── test └── test.js ├── package.json ├── README.md └── app.js /pres/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatosevic/pm2-logrotate-ext/HEAD/pres/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.log 4 | *.pid 5 | test/child 6 | *.iml 7 | .idea/** 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 2.7.0 3 | 4 | - [#129] fix overwrite logs in cluster mode (@EllieSummer) 5 | - [#90] Fixed typo by changing GMT-1 to GMT+1 to match the tz (@nitrocode) 6 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var MB = ''; 2 | 3 | for (var i = 0; i < 1024 * 1024; ++i) 4 | MB += '1'; 5 | 6 | setInterval(function() { 7 | process.stdout.write(MB); 8 | process.stderr.write(MB); 9 | }, 1000); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pm2-logrotate-ext", 3 | "version": "2.7.0", 4 | "description": "Module to rotate logs of every pm2 application. With some extended features.", 5 | "main": "app.js", 6 | "dependencies": { 7 | "graceful-fs": "^4.2.2", 8 | "moment-timezone": "^0.5.11", 9 | "node-schedule": "^1.3.1", 10 | "pm2": "latest", 11 | "pmx": "latest" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Joni SHKURTI", 17 | "contributors": [ 18 | { 19 | "name": "Luka Matosevic" 20 | } 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/lmatosevic/pm2-logrotate-ext.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/lmatosevic/pm2-logrotate-ext/issues" 28 | }, 29 | "homepage": "https://github.com/lmatosevic/pm2-logrotate-ext", 30 | "license": "MIT", 31 | "apps": [ 32 | { 33 | "name": "pm2-logrotate-ext", 34 | "script": "app.js", 35 | "max_memory_restart": "500M" 36 | } 37 | ], 38 | "config": { 39 | "max_size": "10M", 40 | "retain": "30", 41 | "compress": false, 42 | "dateFormat": "YYYY-MM-DD_HH-mm-ss", 43 | "workerInterval": "30", 44 | "rotateInterval": "0 0 * * *", 45 | "rotateModule": true, 46 | "forced": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | PM2 module to automatically rotate logs of processes managed by PM2. This is a forked version of 4 | original [pm2-logrotate](https://github.com/pm2-hive/pm2-logrotate) with some extended features. 5 | 6 | New features in this forked version: 7 | 8 | - Added new configuration flag `forced` which specifies if logs should be rotated every time on `rotateInterval` cron. 9 | 10 | All credits for the original rotator go to the 11 | pm2-logrotate [authors and contributors](https://github.com/pm2-hive/pm2-logrotate/graphs/contributors) 12 | 13 | ## Install 14 | 15 | pm2 install pm2-logrotate-ext 16 | 17 | **NOTE:** the command is `pm2 install` NOT `npm install` 18 | 19 | ## Configure 20 | 21 | - `max_size` (Defaults to `10M`): When a file size becomes higher than this value it will rotate it (its possible that 22 | the worker check the file after it actually pass the limit) . You can specify the unit at then 23 | end: `10G`, `10M`, `10K` 24 | - `retain` (Defaults to `30` file logs): This number is the number of rotated logs that are keep at any one time, it 25 | means that if you have retain = 7 you will have at most 7 rotated logs and your current one. 26 | - `compress` (Defaults to `false`): Enable compression via gzip for all rotated logs 27 | - `dateFormat` (Defaults to `YYYY-MM-DD_HH-mm-ss`) : Format of the data used the name the file of log 28 | - `rotateModule` (Defaults to `true`) : Rotate the log of pm2's module like other apps 29 | - `workerInterval` (Defaults to `30` in secs) : You can control at which interval the worker is checking the log's 30 | size (minimum is `1`) 31 | - `rotateInterval` (Defaults to `0 0 * * *` everyday at midnight): This cron is used to a force rotate when executed. 32 | We are using [node-schedule](https://github.com/node-schedule/node-schedule) to schedule cron, so all valid cron 33 | for [node-schedule](https://github.com/node-schedule/node-schedule) is valid cron for this option. Cron style : 34 | - `TZ` (Defaults to system time): This is the 35 | standard [tz database timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used to offset the log 36 | file saved. For instance, a value of `Etc/GMT+1`, with an hourly log, will save a file at hour `14` GMT with 37 | hour `13` (GMT+1) in the log name. 38 | - `forced` (Defaults to `true`): Enable or disable forced rotation on `rotateInterval`, if set to false, rotation of the 39 | log files will occur only when `max_size` limit is reached. 40 | 41 | ``` 42 | * * * * * * 43 | ┬ ┬ ┬ ┬ ┬ ┬ 44 | │ │ │ │ │ | 45 | │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) 46 | │ │ │ │ └───── month (1 - 12) 47 | │ │ │ └────────── day of month (1 - 31) 48 | │ │ └─────────────── hour (0 - 23) 49 | │ └──────────────────── minute (0 - 59) 50 | └───────────────────────── second (0 - 59, OPTIONAL) 51 | ``` 52 | 53 | ### How to view current configuration of the above values ? 54 | 55 | After having installed the module, you have to type : 56 | `pm2 conf` 57 | 58 | ### How to set these values ? 59 | 60 | After having installed the module you have to type : 61 | `pm2 set pm2-logrotate-ext: ` 62 | 63 | e.g: 64 | 65 | - `pm2 set pm2-logrotate-ext:max_size 1K` (1KB) 66 | - `pm2 set pm2-logrotate-ext:compress true` (compress logs when rotated) 67 | - `pm2 set pm2-logrotate-ext:rotateInterval '*/1 * * * *'` (force rotate every minute) 68 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var fs = require('graceful-fs'); 2 | var path = require('path'); 3 | var pmx = require('pmx'); 4 | var pm2 = require('pm2'); 5 | var moment = require('moment-timezone'); 6 | var scheduler = require('node-schedule'); 7 | var zlib = require('zlib'); 8 | 9 | var conf = pmx.initModule({ 10 | widget: { 11 | type: 'generic', 12 | logo: 'https://raw.githubusercontent.com/lmatosevic/pm2-logrotate-ext/master/pres/logo.png', 13 | theme: ['#111111', '#1B2228', '#31C2F1', '#807C7C'], 14 | el: { 15 | probes: false, 16 | actions: false 17 | }, 18 | block: { 19 | issues: true, 20 | cpu: true, 21 | mem: true, 22 | actions: true, 23 | main_probes: ['Global logs size', 'Files count'] 24 | } 25 | } 26 | }); 27 | 28 | var PM2_ROOT_PATH = ''; 29 | var Probe = pmx.probe(); 30 | 31 | if (process.env.PM2_HOME) 32 | PM2_ROOT_PATH = process.env.PM2_HOME; 33 | else if (process.env.HOME && !process.env.HOMEPATH) 34 | PM2_ROOT_PATH = path.resolve(process.env.HOME, '.pm2'); 35 | else if (process.env.HOME || process.env.HOMEPATH) 36 | PM2_ROOT_PATH = path.resolve(process.env.HOMEDRIVE, process.env.HOME || process.env.HOMEPATH, '.pm2'); 37 | 38 | var WORKER_INTERVAL = isNaN(parseInt(conf.workerInterval)) ? 30 * 1000 : 39 | parseInt(conf.workerInterval) * 1000; // default: 30 secs 40 | var SIZE_LIMIT = get_limit_size(); // default : 10MB 41 | var ROTATE_CRON = conf.rotateInterval || "0 0 * * *"; // default : every day at midnight 42 | var RETAIN = isNaN(parseInt(conf.retain)) ? undefined : parseInt(conf.retain); // All 43 | var COMPRESSION = JSON.parse(conf.compress) || false; // Do not compress by default 44 | var DATE_FORMAT = conf.dateFormat || 'YYYY-MM-DD_HH-mm-ss'; 45 | var TZ = conf.TZ; 46 | var ROTATE_MODULE = JSON.parse(conf.rotateModule) || true; 47 | var FORCED = (typeof JSON.parse(conf.forced) === 'boolean') ? JSON.parse(conf.forced) : true; 48 | var WATCHED_FILES = []; 49 | 50 | function get_limit_size() { 51 | if (conf.max_size === '') 52 | return (1024 * 1024 * 10); 53 | if (typeof (conf.max_size) !== 'string') 54 | conf.max_size = conf.max_size + ""; 55 | if (conf.max_size.slice(-1) === 'G') 56 | return (parseInt(conf.max_size) * 1024 * 1024 * 1024); 57 | if (conf.max_size.slice(-1) === 'M') 58 | return (parseInt(conf.max_size) * 1024 * 1024); 59 | if (conf.max_size.slice(-1) === 'K') 60 | return (parseInt(conf.max_size) * 1024); 61 | return parseInt(conf.max_size); 62 | } 63 | 64 | function delete_old(file) { 65 | if (file === "/dev/null") return; 66 | var fileBaseName = file.substr(0, file.length - 4).split('/').pop() + "__"; 67 | var dirName = path.dirname(file); 68 | 69 | fs.readdir(dirName, function (err, files) { 70 | var i, len; 71 | if (err) return pmx.notify(err); 72 | 73 | var rotated_files = []; 74 | for (i = 0, len = files.length; i < len; i++) { 75 | if (files[i].indexOf(fileBaseName) >= 0) 76 | rotated_files.push(files[i]); 77 | } 78 | rotated_files.sort().reverse(); 79 | 80 | for (i = rotated_files.length - 1; i >= RETAIN; i--) { 81 | (function (i) { 82 | fs.unlink(path.resolve(dirName, rotated_files[i]), function (err) { 83 | if (err) return console.error(err); 84 | console.log('"' + rotated_files[i] + '" has been deleted'); 85 | }); 86 | })(i); 87 | } 88 | }); 89 | } 90 | 91 | 92 | /** 93 | * Apply the rotation process of the log file. 94 | * 95 | * @param {string} file 96 | */ 97 | function proceed(file) { 98 | // set default final time 99 | var final_time = moment().format(DATE_FORMAT); 100 | // check for a timezone 101 | if (TZ) { 102 | try { 103 | final_time = moment().tz(TZ).format(DATE_FORMAT); 104 | } catch (err) { 105 | // use default 106 | } 107 | } 108 | var final_name = file.substr(0, file.length - 4) + '__' + final_time + '.log'; 109 | // if compression is enabled, add gz extention and create a gzip instance 110 | if (COMPRESSION) { 111 | var GZIP = zlib.createGzip({ level: zlib.Z_BEST_COMPRESSION, memLevel: zlib.Z_BEST_COMPRESSION }); 112 | final_name += ".gz"; 113 | } 114 | 115 | // create our read/write streams 116 | var readStream = fs.createReadStream(file); 117 | var writeStream = fs.createWriteStream(final_name, { 'flags': 'w+' }); 118 | 119 | // pipe all stream 120 | if (COMPRESSION) 121 | readStream.pipe(GZIP).pipe(writeStream); 122 | else 123 | readStream.pipe(writeStream); 124 | 125 | 126 | // listen for error 127 | readStream.on('error', pmx.notify.bind(pmx)); 128 | writeStream.on('error', pmx.notify.bind(pmx)); 129 | if (COMPRESSION) { 130 | GZIP.on('error', pmx.notify.bind(pmx)); 131 | } 132 | 133 | // when the read is done, empty the file and check for retain option 134 | writeStream.on('finish', function () { 135 | if (GZIP) { 136 | GZIP.close(); 137 | } 138 | readStream.close(); 139 | writeStream.close(); 140 | fs.truncate(file, function (err) { 141 | if (err) return pmx.notify(err); 142 | console.log('"' + final_name + '" has been created'); 143 | 144 | if (typeof (RETAIN) === 'number') 145 | delete_old(file); 146 | }); 147 | }); 148 | } 149 | 150 | 151 | /** 152 | * Apply the rotation process if the `file` size exceeds the `SIZE_LIMIT`. 153 | * 154 | * @param {string} file 155 | * @param {boolean} force - Do not check the SIZE_LIMIT and rotate everytime. 156 | */ 157 | function proceed_file(file, force) { 158 | if (!fs.existsSync(file)) return; 159 | 160 | if (!WATCHED_FILES.includes(file)) { 161 | WATCHED_FILES.push(file); 162 | } 163 | 164 | fs.stat(file, function (err, data) { 165 | if (err) return console.error(err); 166 | 167 | if (data.size > 0 && (data.size >= SIZE_LIMIT || force)) 168 | proceed(file); 169 | }); 170 | } 171 | 172 | 173 | /** 174 | * Apply the rotation process of all log files of `app` where the file size exceeds the`SIZE_LIMIT`. 175 | * 176 | * @param {Object} app 177 | * @param {boolean} force - Do not check the SIZE_LIMIT and rotate everytime. 178 | */ 179 | function proceed_app(app, force) { 180 | // Check all log path 181 | // Note: If same file is defined for multiple purposes, it will be processed once only. 182 | if (app.pm2_env.pm_out_log_path) { 183 | proceed_file(app.pm2_env.pm_out_log_path, force); 184 | } 185 | if (app.pm2_env.pm_err_log_path && app.pm2_env.pm_err_log_path !== app.pm2_env.pm_out_log_path) { 186 | proceed_file(app.pm2_env.pm_err_log_path, force); 187 | } 188 | if (app.pm2_env.pm_log_path && app.pm2_env.pm_log_path !== app.pm2_env.pm_out_log_path && app.pm2_env.pm_log_path !== app.pm2_env.pm_err_log_path) { 189 | proceed_file(app.pm2_env.pm_log_path, force); 190 | } 191 | } 192 | 193 | // Connect to local PM2 194 | pm2.connect(function (err) { 195 | if (err) return console.error(err.stack || err); 196 | 197 | // start background task 198 | setInterval(function () { 199 | // get list of process managed by pm2 200 | pm2.list(function (err, apps) { 201 | if (err) return console.error(err.stack || err); 202 | 203 | var appMap = {}; 204 | // rotate log that are bigger than the limit 205 | apps.forEach(function (app) { 206 | // if its a module and the rotate of module is disabled, ignore 207 | if (typeof (app.pm2_env.axm_options.isModule) !== 'undefined' && !ROTATE_MODULE) return; 208 | 209 | // if apps instances are multi and one of the instances has rotated, ignore 210 | if (app.pm2_env.instances > 1 && appMap[app.name]) return; 211 | 212 | appMap[app.name] = app; 213 | 214 | proceed_app(app, false); 215 | }); 216 | }); 217 | 218 | // rotate pm2 log 219 | proceed_file(PM2_ROOT_PATH + '/pm2.log', false); 220 | proceed_file(PM2_ROOT_PATH + '/agent.log', false); 221 | }, WORKER_INTERVAL); 222 | 223 | // register the cron to force rotate file if forced flag is true 224 | if (FORCED) { 225 | scheduler.scheduleJob(ROTATE_CRON, function () { 226 | // get list of process managed by pm2 227 | pm2.list(function (err, apps) { 228 | if (err) return console.error(err.stack || err); 229 | 230 | var appMap = {}; 231 | // force rotate for each app 232 | apps.forEach(function (app) { 233 | // if its a module and the rotate of module is disabled, ignore 234 | if (typeof (app.pm2_env.axm_options.isModule) !== 'undefined' && !ROTATE_MODULE) return; 235 | 236 | // if apps instances are multi and one of the instances has rotated, ignore 237 | if (app.pm2_env.instances > 1 && appMap[app.name]) return; 238 | 239 | appMap[app.name] = app; 240 | 241 | proceed_app(app, true); 242 | }); 243 | }); 244 | }); 245 | } 246 | }); 247 | 248 | /** ACTION PMX **/ 249 | pmx.action('list watched logs', function (reply) { 250 | var returned = {}; 251 | WATCHED_FILES.forEach(function (file) { 252 | returned[file] = (fs.statSync(file).size); 253 | }); 254 | return reply(returned); 255 | }); 256 | 257 | pmx.action('list all logs', function (reply) { 258 | var returned = {}; 259 | var folder = PM2_ROOT_PATH + "/logs"; 260 | fs.readdir(folder, function (err, files) { 261 | if (err) { 262 | console.error(err.stack || err); 263 | return reply(0) 264 | } 265 | 266 | files.forEach(function (file) { 267 | returned[file] = (fs.statSync(folder + "/" + file).size); 268 | }); 269 | return reply(returned); 270 | }); 271 | }); 272 | 273 | /** PROB PMX **/ 274 | var metrics = {}; 275 | metrics.totalsize = Probe.metric({ 276 | name: 'Global logs size', 277 | value: 'N/A' 278 | }); 279 | 280 | metrics.totalcount = Probe.metric({ 281 | name: 'Files count', 282 | value: 'N/A' 283 | }); 284 | 285 | // update folder size of logs every 10secs 286 | function updateFolderSizeProbe() { 287 | var returned = 0; 288 | var folder = PM2_ROOT_PATH + "/logs"; 289 | fs.readdir(folder, function (err, files) { 290 | if (err) { 291 | console.error(err.stack || err); 292 | return metrics.totalsize.set("N/A"); 293 | } 294 | 295 | files.forEach(function (file, idx, arr) { 296 | returned += fs.statSync(folder + "/" + file).size; 297 | }); 298 | 299 | metrics.totalsize.set(handleUnit(returned, 2)); 300 | }); 301 | } 302 | 303 | updateFolderSizeProbe(); 304 | setInterval(updateFolderSizeProbe, 30000); 305 | 306 | // update file count every 10secs 307 | function updateFileCountProbe() { 308 | fs.readdir(PM2_ROOT_PATH + "/logs", function (err, files) { 309 | if (err) { 310 | console.error(err.stack || err); 311 | return metrics.totalcount.set(0); 312 | } 313 | 314 | return metrics.totalcount.set(files.length); 315 | }); 316 | } 317 | 318 | updateFileCountProbe(); 319 | setInterval(updateFileCountProbe, 30000); 320 | 321 | function handleUnit(bytes, precision) { 322 | var kilobyte = 1024; 323 | var megabyte = kilobyte * 1024; 324 | var gigabyte = megabyte * 1024; 325 | var terabyte = gigabyte * 1024; 326 | 327 | if ((bytes >= 0) && (bytes < kilobyte)) { 328 | return bytes + ' B'; 329 | } else if ((bytes >= kilobyte) && (bytes < megabyte)) { 330 | return (bytes / kilobyte).toFixed(precision) + ' KB'; 331 | } else if ((bytes >= megabyte) && (bytes < gigabyte)) { 332 | return (bytes / megabyte).toFixed(precision) + ' MB'; 333 | } else if ((bytes >= gigabyte) && (bytes < terabyte)) { 334 | return (bytes / gigabyte).toFixed(precision) + ' GB'; 335 | } else if (bytes >= terabyte) { 336 | return (bytes / terabyte).toFixed(precision) + ' TB'; 337 | } else { 338 | return bytes + ' B'; 339 | } 340 | } 341 | --------------------------------------------------------------------------------