ntlogger 2.10.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -2
- package/index.d.ts +192 -183
- package/lib/lifecycle.js +53 -0
- package/lib/logger.js +100 -57
- package/lib/pinoHooks.js +446 -0
- package/lib/processHandlers.js +305 -0
- package/lib/secretRedaction.js +675 -0
- package/lib/signalHandler.js +31 -48
- package/package.json +44 -18
- package/pino.d.ts +287 -0
- package/pino.js +434 -13
- package/plugins/README.md +129 -1
- package/plugins/discord.js +30 -48
- package/plugins/index.js +27 -12
- package/plugins/lib/httpDelivery.js +98 -0
- package/plugins/lib/syslogClient.js +18 -8
- package/plugins/mysql.js +73 -83
- package/plugins/openobserve.js +33 -207
- package/plugins/otel.js +5 -7
- package/plugins/postgres.js +69 -67
- package/plugins/sentry.js +36 -3
- package/plugins/syslog.js +6 -9
- package/plugins/teams.js +16 -67
- package/transports/pino.js +188 -12
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Standard "log fatal, bounded drain, exit" process handlers for Pino (and any
|
|
4
|
+
// generic) logger. Winston users already have lib/signalHandler.js; this module
|
|
5
|
+
// deliberately has no Winston dependency so it can ship with ntlogger/pino.
|
|
6
|
+
//
|
|
7
|
+
// Explicit opt-in: importing a library must not install process exit handlers,
|
|
8
|
+
// so nothing here runs until installProcessHandlers() is called.
|
|
9
|
+
|
|
10
|
+
const { inspect } = require('util');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT = 4000;
|
|
13
|
+
const DEFAULT_SIGNALS = ['SIGTERM', 'SIGINT'];
|
|
14
|
+
|
|
15
|
+
// POSIX signal numbers for the 128+n force-exit convention.
|
|
16
|
+
const SIGNAL_NUMBERS = {
|
|
17
|
+
SIGHUP: 1,
|
|
18
|
+
SIGINT: 2,
|
|
19
|
+
SIGQUIT: 3,
|
|
20
|
+
SIGABRT: 6,
|
|
21
|
+
SIGUSR1: 10,
|
|
22
|
+
SIGUSR2: 12,
|
|
23
|
+
SIGTERM: 15,
|
|
24
|
+
SIGBREAK: 21,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// One entry per logger so a double install is a no-op (see installProcessHandlers).
|
|
28
|
+
const registry = new WeakMap();
|
|
29
|
+
|
|
30
|
+
function isPromise(value) {
|
|
31
|
+
return !!value && typeof value.then === 'function';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unref(timer) {
|
|
35
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
36
|
+
return timer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pino puts Symbol('pino.*') properties on every logger and child logger.
|
|
40
|
+
function isPinoLogger(logger) {
|
|
41
|
+
if (!logger || typeof logger !== 'object') return false;
|
|
42
|
+
for (let target = logger; target; target = Object.getPrototypeOf(target)) {
|
|
43
|
+
for (const symbol of Object.getOwnPropertySymbols(target)) {
|
|
44
|
+
if (typeof symbol.description === 'string' && symbol.description.startsWith('pino.')) return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return typeof logger.bindings === 'function' && typeof logger.child === 'function';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// unhandledRejection hands us whatever was thrown — strings, numbers, plain
|
|
51
|
+
// objects. Downstream serializers expect Error-likes, so normalize.
|
|
52
|
+
function toError(value) {
|
|
53
|
+
if (value instanceof Error) return value;
|
|
54
|
+
if (value && typeof value === 'object' && typeof value.message === 'string' && typeof value.stack === 'string') {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
let description;
|
|
58
|
+
try {
|
|
59
|
+
description = typeof value === 'string' ? value : inspect(value, { depth: 2, breakLength: Infinity });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
description = Object.prototype.toString.call(value);
|
|
62
|
+
}
|
|
63
|
+
const normalized = new Error(description);
|
|
64
|
+
normalized.name = 'NonErrorRejection';
|
|
65
|
+
normalized.reason = value;
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Flush whatever the logger supports, bounded by `timeout`. Never rejects.
|
|
71
|
+
*
|
|
72
|
+
* Resolution order:
|
|
73
|
+
* 1. logger.flush(cb) — Pino v7+ (callback signature, flush.length >= 1)
|
|
74
|
+
* 2. logger.flush() — ntlogger's Winston logger (returns a Promise)
|
|
75
|
+
* 3. logger.close() — last resort, when only close() exists
|
|
76
|
+
* 4. setImmediate — nothing to flush, yield one turn and resolve
|
|
77
|
+
*
|
|
78
|
+
* @param {object} logger
|
|
79
|
+
* @param {number} [timeout=5000] milliseconds; <= 0 means "no budget left"
|
|
80
|
+
* @returns {Promise<{drained: boolean, error?: Error}>}
|
|
81
|
+
*/
|
|
82
|
+
function drainLogger(logger, timeout = 5000) {
|
|
83
|
+
const budget = typeof timeout === 'number' && isFinite(timeout) ? timeout : 5000;
|
|
84
|
+
if (budget <= 0) {
|
|
85
|
+
return Promise.resolve({ drained: false, error: new Error('Logger drain budget exhausted') });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return new Promise(resolve => {
|
|
89
|
+
let settled = false;
|
|
90
|
+
const finish = result => {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve(result);
|
|
95
|
+
};
|
|
96
|
+
const timer = unref(setTimeout(
|
|
97
|
+
() => finish({ drained: false, error: new Error(`Logger drain timed out after ${budget}ms`) }),
|
|
98
|
+
budget,
|
|
99
|
+
));
|
|
100
|
+
|
|
101
|
+
const settleWith = value => {
|
|
102
|
+
if (isPromise(value)) {
|
|
103
|
+
value.then(() => finish({ drained: true }), error => finish({ drained: false, error }));
|
|
104
|
+
} else {
|
|
105
|
+
finish({ drained: true });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (logger && typeof logger.flush === 'function') {
|
|
111
|
+
if (logger.flush.length >= 1) {
|
|
112
|
+
// Pino: the callback fires once the destination has flushed.
|
|
113
|
+
logger.flush(error => (error ? finish({ drained: false, error }) : finish({ drained: true })));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
settleWith(logger.flush());
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (logger && typeof logger.close === 'function') {
|
|
120
|
+
settleWith(logger.close());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setImmediate(() => finish({ drained: true }));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
finish({ drained: false, error });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Install uncaughtException / unhandledRejection / signal handlers that log,
|
|
132
|
+
* run a user shutdown hook, drain the logger, and exit — all under one budget.
|
|
133
|
+
*
|
|
134
|
+
* Installing twice for the same logger returns the first uninstall function and
|
|
135
|
+
* registers nothing new; the options from the first call keep winning.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} logger any logger with level methods (Pino, ntlogger, console-like)
|
|
138
|
+
* @param {object} [options]
|
|
139
|
+
* @returns {function(): void} uninstall — removes only this call's listeners, idempotent
|
|
140
|
+
*/
|
|
141
|
+
function installProcessHandlers(logger, options = {}) {
|
|
142
|
+
const canCache = !!logger && (typeof logger === 'object' || typeof logger === 'function');
|
|
143
|
+
if (canCache && registry.has(logger)) return registry.get(logger);
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
timeout = DEFAULT_TIMEOUT,
|
|
147
|
+
signals = DEFAULT_SIGNALS,
|
|
148
|
+
exitOnSignal = true,
|
|
149
|
+
signalExitCode = 0,
|
|
150
|
+
uncaughtExitCode = 1,
|
|
151
|
+
exitOnUnhandledRejection = false,
|
|
152
|
+
unhandledRejectionLevel = 'error',
|
|
153
|
+
onShutdown,
|
|
154
|
+
proc = process,
|
|
155
|
+
logPrefix,
|
|
156
|
+
} = options;
|
|
157
|
+
|
|
158
|
+
const exit = options.exit
|
|
159
|
+
|| (typeof proc.exit === 'function' ? proc.exit.bind(proc) : process.exit.bind(process));
|
|
160
|
+
|
|
161
|
+
const prefix = typeof logPrefix === 'string' && logPrefix.length ? `${logPrefix} ` : '';
|
|
162
|
+
|
|
163
|
+
let shuttingDown = false;
|
|
164
|
+
let exited = false;
|
|
165
|
+
|
|
166
|
+
const doExit = code => {
|
|
167
|
+
if (exited) return;
|
|
168
|
+
exited = true;
|
|
169
|
+
try {
|
|
170
|
+
exit(code);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
try {
|
|
173
|
+
console.error('[ntlogger] process exit failed:', error);
|
|
174
|
+
} catch (ignored) { /* nothing left to do */ }
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// The logger is the thing that may be broken, so every call is guarded and
|
|
179
|
+
// console.error is the single fallback.
|
|
180
|
+
const safeLog = (level, meta, message) => {
|
|
181
|
+
const text = `${prefix}${message}`;
|
|
182
|
+
const method = typeof logger?.[level] === 'function'
|
|
183
|
+
? level
|
|
184
|
+
: (typeof logger?.error === 'function' ? 'error' : null);
|
|
185
|
+
if (method) {
|
|
186
|
+
try {
|
|
187
|
+
if (isPinoLogger(logger)) logger[method](meta, text);
|
|
188
|
+
else logger[method](text, meta);
|
|
189
|
+
return;
|
|
190
|
+
} catch (error) { /* fall through to console.error exactly once */ }
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
console.error(text, meta);
|
|
194
|
+
} catch (ignored) { /* nothing left to do */ }
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Resolves instead of rejecting so the shutdown path never branches on throw.
|
|
198
|
+
const runHook = (reason, budget) => new Promise(resolve => {
|
|
199
|
+
if (typeof onShutdown !== 'function') return resolve();
|
|
200
|
+
let done = false;
|
|
201
|
+
const finish = () => {
|
|
202
|
+
if (done) return;
|
|
203
|
+
done = true;
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
resolve();
|
|
206
|
+
};
|
|
207
|
+
const timer = unref(setTimeout(() => {
|
|
208
|
+
if (done) return;
|
|
209
|
+
safeLog('warn', { reason, timeout: budget }, `Shutdown hook timed out after ${budget}ms`);
|
|
210
|
+
finish();
|
|
211
|
+
}, budget > 0 ? budget : 1));
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = onShutdown(reason);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
safeLog('error', { err: error, reason }, 'Shutdown hook failed');
|
|
217
|
+
return finish();
|
|
218
|
+
}
|
|
219
|
+
if (!isPromise(result)) return finish();
|
|
220
|
+
result.then(finish, error => {
|
|
221
|
+
safeLog('error', { err: error, reason }, 'Shutdown hook failed');
|
|
222
|
+
finish();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const runShutdown = (reason, exitCode, shouldExit) => {
|
|
227
|
+
shuttingDown = true;
|
|
228
|
+
const deadline = Date.now() + timeout;
|
|
229
|
+
// Hard backstop: a wedged hook or an unflushable logger must not pin the
|
|
230
|
+
// process past the supervisor's grace window. unref'd so it never keeps
|
|
231
|
+
// an otherwise-idle event loop alive on its own.
|
|
232
|
+
const forceTimer = shouldExit
|
|
233
|
+
? unref(setTimeout(() => doExit(exitCode), timeout))
|
|
234
|
+
: null;
|
|
235
|
+
|
|
236
|
+
return (async () => {
|
|
237
|
+
await runHook(reason, Math.max(0, deadline - Date.now()));
|
|
238
|
+
const result = await drainLogger(logger, deadline - Date.now());
|
|
239
|
+
if (!result.drained && result.error) {
|
|
240
|
+
try {
|
|
241
|
+
console.error(`[ntlogger] logger drain incomplete during ${reason}:`, result.error.message);
|
|
242
|
+
} catch (ignored) { /* nothing left to do */ }
|
|
243
|
+
}
|
|
244
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
245
|
+
if (shouldExit) doExit(exitCode);
|
|
246
|
+
})();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const onUncaughtException = (err, origin) => {
|
|
250
|
+
if (shuttingDown) {
|
|
251
|
+
safeLog('fatal', { err, origin }, 'Uncaught exception during shutdown, already exiting');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
safeLog('fatal', { err, origin }, 'Uncaught exception');
|
|
255
|
+
void runShutdown('uncaughtException', uncaughtExitCode, true);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const onUnhandledRejection = reason => {
|
|
259
|
+
const err = toError(reason);
|
|
260
|
+
if (shuttingDown) {
|
|
261
|
+
safeLog(unhandledRejectionLevel, { err }, 'Unhandled promise rejection during shutdown, already exiting');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
safeLog(unhandledRejectionLevel, { err }, 'Unhandled promise rejection');
|
|
265
|
+
if (!exitOnUnhandledRejection) return;
|
|
266
|
+
void runShutdown('unhandledRejection', uncaughtExitCode, true);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const makeSignalHandler = signal => () => {
|
|
270
|
+
if (shuttingDown) {
|
|
271
|
+
// Second Ctrl-C / second SIGTERM: the operator wants out now.
|
|
272
|
+
safeLog('warn', { signal }, `Received ${signal} during shutdown, forcing exit`);
|
|
273
|
+
doExit(128 + (SIGNAL_NUMBERS[signal] || 0));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
safeLog('info', { signal }, `Received ${signal}, shutting down`);
|
|
277
|
+
void runShutdown(signal, signalExitCode, exitOnSignal);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const listeners = [
|
|
281
|
+
['uncaughtException', onUncaughtException],
|
|
282
|
+
['unhandledRejection', onUnhandledRejection],
|
|
283
|
+
];
|
|
284
|
+
for (const signal of Array.isArray(signals) ? signals : []) {
|
|
285
|
+
listeners.push([signal, makeSignalHandler(signal)]);
|
|
286
|
+
}
|
|
287
|
+
for (const [event, handler] of listeners) proc.on(event, handler);
|
|
288
|
+
|
|
289
|
+
let removed = false;
|
|
290
|
+
const uninstall = () => {
|
|
291
|
+
if (removed) return;
|
|
292
|
+
removed = true;
|
|
293
|
+
for (const [event, handler] of listeners) {
|
|
294
|
+
if (typeof proc.off === 'function') proc.off(event, handler);
|
|
295
|
+
else if (typeof proc.removeListener === 'function') proc.removeListener(event, handler);
|
|
296
|
+
}
|
|
297
|
+
listeners.length = 0;
|
|
298
|
+
if (canCache && registry.get(logger) === uninstall) registry.delete(logger);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (canCache) registry.set(logger, uninstall);
|
|
302
|
+
return uninstall;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = { installProcessHandlers, drainLogger };
|