├── 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 |
--------------------------------------------------------------------------------