forked from keymetrics/pm2-logrotate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.js
355 lines (316 loc) · 9.91 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
const fs = require("graceful-fs");
const path = require("path");
const pmx = require("pmx");
const pm2 = require("pm2");
const moment = require("moment-timezone");
const scheduler = require("node-schedule");
const zlib = require("zlib");
const util = require("util");
///
/// ENTRY POINT
///
const conf = pmx.initModule({
widget: {
type: "generic",
logo: "https://raw.githubusercontent.com/pm2-hive/pm2-logrotate/master/pres/logo.png",
theme: ["#111111", "#1B2228", "#31C2F1", "#807C7C"],
el: {
probes: false,
actions: false,
},
block: {
issues: true,
cpu: true,
mem: true,
actions: true,
main_probes: ["Global logs size", "Files count"],
},
},
});
const PM2_ROOT_PATH = getPM2RootPath();
const WORKER_INTERVAL = isNaN(parseInt(conf.workerInterval))
? 30 * 1000
: parseInt(conf.workerInterval) * 1000; // default: 30 secs
const SIZE_LIMIT = get_limit_size(); // default : 10MB
const ROTATE_CRON = conf.rotateInterval || "0 0 * * *"; // default : every day at midnight
const RETAIN = isNaN(parseInt(conf.retain)) ? undefined : parseInt(conf.retain); // All
const COMPRESSION = JSON.parse(conf.compress) || false; // Do not compress by default
const DATE_FORMAT = conf.dateFormat || "YYYY-MM-DD_HH-mm-ss";
const TZ = conf.TZ;
const ROTATE_MODULE = JSON.parse(conf.rotateModule) || true;
const WATCHED_FILES = [];
log("\n\nStarting pm2-logrotate v" + require("./package.json").version);
log_config();
// Connect to local PM2 and schedule the rotation process
pm2.connect(
(err) => {
if (err)
return error(err.stack || err);
// start background task (execute at WORKER_INTERVAL)
setInterval(() => {
//log("interval triggered");
proceed_apps(false); // not forced rotation (only rotate if size > limit)
}, WORKER_INTERVAL);
// register the cron to force rotate file (default = every day at midnight)
scheduler.scheduleJob(ROTATE_CRON, () => {
log("cron triggered");
proceed_apps(true); // forced rotation (at start of new day)
});
}
);
function log_config() {
log("conf: \n" + inspect(conf) + "\n");
log("PM2_ROOT_PATH: " + PM2_ROOT_PATH);
log("WORKER_INTERVAL: " + WORKER_INTERVAL);
log("SIZE_LIMIT: " + SIZE_LIMIT);
log("ROTATE_CRON: " + ROTATE_CRON);
log("RETAIN: " + RETAIN);
log("COMPRESSION: " + COMPRESSION);
log("DATE_FORMAT: " + DATE_FORMAT);
log("TZ: " + TZ);
log("ROTATE_MODULE: " + ROTATE_MODULE);
}
function log(msg) {
console.log(new Date().toLocaleString() + " " + msg);
}
function error(err) {
console.error(new Date().toLocaleString() + " " + err);
}
function inspect(item) {
return util.inspect(item, {
showHidden: false,
depth: 8,
maxArrayLength: 25,
maxStringLength: 255,
compact: 10,
sorted: false,
});
}
function notify_error(err) {
error(err);
pmx.notify(err);
}
/**
* Get PM2 root path from environment variables
*/
function getPM2RootPath() {
if (process.env.PM2_HOME)
return process.env.PM2_HOME;
if (process.env.HOME && !process.env.HOMEPATH)
return path.resolve(process.env.HOME, ".pm2");
if (process.env.HOME || process.env.HOMEPATH)
return path.resolve(
process.env.HOMEDRIVE,
process.env.HOME || process.env.HOMEPATH,
".pm2"
);
return "";
}
/**
* Parse the `max_size` string and return the size in bytes.
*/
function get_limit_size() {
if (conf.max_size === "")
return 1024 * 1024 * 10;
if (typeof conf.max_size !== "string")
conf.max_size = conf.max_size + "";
if (conf.max_size.slice(-1) === "G")
return parseInt(conf.max_size) * 1024 * 1024 * 1024;
if (conf.max_size.slice(-1) === "M")
return parseInt(conf.max_size) * 1024 * 1024;
if (conf.max_size.slice(-1) === "K")
return parseInt(conf.max_size) * 1024;
return parseInt(conf.max_size);
}
/**
* Delete all rotated files except the `RETAIN` newest.
*
* @param {string} file
*/
function delete_old(file) {
if (file === "/dev/null")
return;
const fileBaseName = path.basename(getFileBaseName(file));
const dirName = path.dirname(file);
fs.readdir(dirName, (err, files) => {
if (err)
return notify_error(err);
const rotated_files = [];
for (let i = 0; i < files.length; i++) {
if (files[i].indexOf(fileBaseName) >= 0)
rotated_files.push(files[i]);
}
// sort newest to oldest
rotated_files.sort().reverse();
// keep RETAIN newest files
for (let i = rotated_files.length - 1; i >= RETAIN; i--) {
((rotated_file) => {
fs.unlink(path.resolve(dirName, rotated_file), (err) => {
if (err) {
return error(err);
}
log('"' + rotated_file + '" has been deleted');
});
})(rotated_files[i]);
}
});
}
/**
* Get timestamp string to use in rotated file name
*/
function getFinalTime() {
// set default final time
let final_time = moment().format(DATE_FORMAT);
// check for a timezone
if (TZ) {
try {
final_time = moment().tz(TZ).format(DATE_FORMAT);
} catch (err) {
// use default
}
}
return final_time;
}
/**
* Get base file name - i.e. without file extension, but with '__' at the end
*
* @param {string} file
*/
function getFileBaseName(file) {
// Get base file name - i.e. without file extension : remove everything after the last dot (up to then end of string)
const base_file_name = file.replace(/\.[^\.]+$/, '');
return base_file_name + '__';
}
/**
* Get full path of file after rotation
*
* @param {string} file
*/
function getFinalName(file) {
// Get file extension: replace everything by string after the last dot
const file_ext = file.replace(/^.*\.([^\.]*)$/i, '$1')
// Build final filename with base + time + extension
let final_name = getFileBaseName(file) + getFinalTime() + '.' + file_ext;
if (COMPRESSION)
final_name += ".gz";
return final_name;
}
/**
* Apply the rotation process of the log file.
*
* @param {string} file
*/
function proceed(file) {
let final_name = getFinalName(file);
// if compression is enabled, add gz extension and create a gzip instance
let GZIP;
if (COMPRESSION)
GZIP = zlib.createGzip({ level: zlib.Z_BEST_COMPRESSION, memLevel: zlib.Z_BEST_COMPRESSION });
// create our read/write streams
const readStream = fs.createReadStream(file);
const writeStream = fs.createWriteStream(final_name, { flags: "w+" });
// pipe whole stream
if (COMPRESSION)
readStream.pipe(GZIP).pipe(writeStream);
else
readStream.pipe(writeStream);
// listen for errors
readStream.on("error", notify_error);
writeStream.on("error", notify_error);
if (COMPRESSION)
GZIP.on("error", notify_error);
// when the read is done, empty the file and check for retain option
writeStream.on("finish", () => {
if (GZIP)
GZIP.close();
readStream.close();
writeStream.close();
// final file has been created, empty the original file
fs.truncate(file, (err) => {
if (err)
return notify_error(err);
log('"' + final_name + '" has been created');
log('"' + file + '" has been emptied');
if (typeof RETAIN === "number")
delete_old(file);
});
});
}
/**
* Apply the rotation process if the `file` size exceeds the `SIZE_LIMIT`.
*
* @param {string} file
* @param {boolean} force - Do not check the SIZE_LIMIT and rotate every time.
*/
function proceed_file(file, force) {
if (!fs.existsSync(file))
return;
if (!WATCHED_FILES.includes(file))
WATCHED_FILES.push(file);
fs.stat(file,
(err, data) => {
if (err)
return error(err);
if (data.size > 0 && (data.size >= SIZE_LIMIT || force)) {
log("proceed_file: " + file + ", forced: " + force);
proceed(file);
}
});
}
/**
* Apply the rotation process of all log files of `app` where the file size exceeds the`SIZE_LIMIT`.
*
* @param {Object} app
* @param {boolean} force - Do not check the SIZE_LIMIT and rotate every time.
*/
function proceed_app(app, force) {
// Check all log paths
// Note: If same file is defined for multiple purposes, it will be processed only once
if (app.pm2_env.pm_out_log_path)
proceed_file(app.pm2_env.pm_out_log_path, force);
if (app.pm2_env.pm_err_log_path &&
app.pm2_env.pm_err_log_path !== app.pm2_env.pm_out_log_path)
proceed_file(app.pm2_env.pm_err_log_path, force);
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)
proceed_file(app.pm2_env.pm_log_path, force);
}
/**
* Check if both apps have the exact same log paths
*
* @param {Object} app1
* @param {Object} app2
*/
function has_same_logs(app1, app2) {
// return true only if all log paths are identical
if (!app1 || !app2) return false;
if (app1.pm2_env.pm_out_log_path !== app2.pm2_env.pm_out_log_path) return false;
if (app1.pm2_env.pm_err_log_path !== app2.pm2_env.pm_err_log_path) return false;
if (app1.pm2_env.pm_log_path !== app2.pm2_env.pm_log_path) return false;
return true;
}
/**
* Apply the rotation process of log files of all suitable apps
*
* @param {boolean} force - Do not check the SIZE_LIMIT and rotate regardless of the size.
*/
function proceed_apps(force) {
// get list of process managed by pm2
pm2.list((err, apps) => {
if (err)
return error(err.stack || err);
//log("apps: " + JSON.stringify(apps.map((app) => app.name)));
const appMap = {};
apps.forEach((app) => {
// if its a module and ROTATE_MODULE is disabled, ignore
if (typeof app.pm2_env.axm_options.isModule !== "undefined" && !ROTATE_MODULE)
return;
// if apps instances are multi and the logs of instances are combined, ignore
if (app.pm2_env.instances > 1 && has_same_logs(appMap[app.name], app))
return;
appMap[app.name] = app;
proceed_app(app, force);
});
});
}