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
package/transports/pino.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const split = require('split2');
|
|
4
|
+
const { isMainThread } = require('worker_threads');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
function writeOutput(line) {
|
|
8
|
+
if (isMainThread) return process.stdout.write(line);
|
|
9
|
+
// ThreadStream.end() can block the parent on Atomics.wait. Worker stdout
|
|
10
|
+
// callbacks need that parent's event loop, so write to the fd directly.
|
|
11
|
+
const bytes = Buffer.from(line);
|
|
12
|
+
let offset = 0;
|
|
13
|
+
while (offset < bytes.length) offset += fs.writeSync(1, bytes, offset, bytes.length - offset);
|
|
14
|
+
}
|
|
4
15
|
const colors = require('../lib/colors');
|
|
5
16
|
|
|
6
17
|
const levelColors = colors.console;
|
|
@@ -38,6 +49,31 @@ const STATUS_COLORS = {
|
|
|
38
49
|
5: '\x1b[31m',
|
|
39
50
|
};
|
|
40
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Structured context fields rendered as a compact `{k=v ...}` suffix right
|
|
54
|
+
* after the module label. Order here is the render order.
|
|
55
|
+
*/
|
|
56
|
+
const CONTEXT_KEYS = [
|
|
57
|
+
'tenantId',
|
|
58
|
+
'userId',
|
|
59
|
+
'correlationId',
|
|
60
|
+
'traceId',
|
|
61
|
+
'spanId',
|
|
62
|
+
'jobId',
|
|
63
|
+
'agentUuid',
|
|
64
|
+
'commandId',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Boilerplate messages emitted by pino-http/Fastify for the request-start
|
|
69
|
+
* line. They carry no information once the method and URL are rendered.
|
|
70
|
+
*/
|
|
71
|
+
const GENERIC_REQUEST_MESSAGES = new Set(['incoming request', 'request received']);
|
|
72
|
+
|
|
73
|
+
// Defaults for the in-flight request map. Both are overridable per transport.
|
|
74
|
+
const DEFAULT_MAX_INFLIGHT_REQUESTS = 1000;
|
|
75
|
+
const DEFAULT_INFLIGHT_TTL_MS = 60000;
|
|
76
|
+
|
|
41
77
|
/**
|
|
42
78
|
* Format a timestamp to YYYY-MM-DD HH:mm:ss (NTL style).
|
|
43
79
|
*/
|
|
@@ -59,14 +95,108 @@ function padLevel(name) {
|
|
|
59
95
|
return name.padEnd(8);
|
|
60
96
|
}
|
|
61
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Bounded map of in-flight HTTP requests.
|
|
100
|
+
*
|
|
101
|
+
* pino-http/Fastify split a request across two lines: the request-start line
|
|
102
|
+
* carries `req.method` / `req.url`, the completion line carries only
|
|
103
|
+
* `res.statusCode` and `responseTime`. Remembering the start line lets the
|
|
104
|
+
* completion line render `METHOD /path — STATUS (Nms)`.
|
|
105
|
+
*
|
|
106
|
+
* A request that never completes (aborted socket, crashed handler) would leak
|
|
107
|
+
* an entry forever, so entries expire by age and the map is capped. Both
|
|
108
|
+
* sweeps are lazy — no timers, nothing that can keep a process alive.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} opts
|
|
111
|
+
* @param {number} [opts.maxInflightRequests=1000] Hard cap on tracked requests.
|
|
112
|
+
* @param {number} [opts.inflightTtlMs=60000] Max age of a tracked request.
|
|
113
|
+
*/
|
|
114
|
+
function createRequestTracker(opts = {}) {
|
|
115
|
+
const max = Number.isFinite(opts.maxInflightRequests) && opts.maxInflightRequests > 0
|
|
116
|
+
? Math.floor(opts.maxInflightRequests)
|
|
117
|
+
: DEFAULT_MAX_INFLIGHT_REQUESTS;
|
|
118
|
+
const ttl = Number.isFinite(opts.inflightTtlMs) && opts.inflightTtlMs > 0
|
|
119
|
+
? opts.inflightTtlMs
|
|
120
|
+
: DEFAULT_INFLIGHT_TTL_MS;
|
|
121
|
+
|
|
122
|
+
// Map preserves insertion order, and entries are always (re)inserted at the
|
|
123
|
+
// tail, so the head is always the oldest entry.
|
|
124
|
+
const entries = new Map();
|
|
125
|
+
|
|
126
|
+
function prune(now = Date.now()) {
|
|
127
|
+
for (const [key, value] of entries) {
|
|
128
|
+
if (now - value.at < ttl) break;
|
|
129
|
+
entries.delete(key);
|
|
130
|
+
}
|
|
131
|
+
while (entries.size > max) {
|
|
132
|
+
const oldest = entries.keys().next();
|
|
133
|
+
if (oldest.done) break;
|
|
134
|
+
entries.delete(oldest.value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
/**
|
|
140
|
+
* Remember an in-flight request. Re-tracking an id refreshes its position.
|
|
141
|
+
*/
|
|
142
|
+
track(id, method, url, now = Date.now()) {
|
|
143
|
+
if (!id) return;
|
|
144
|
+
if (entries.has(id)) entries.delete(id);
|
|
145
|
+
entries.set(id, { method: method || '', url: url || '', at: now });
|
|
146
|
+
prune(now);
|
|
147
|
+
},
|
|
148
|
+
/**
|
|
149
|
+
* Look up and forget a tracked request.
|
|
150
|
+
* @returns {{method: string, url: string}|null}
|
|
151
|
+
*/
|
|
152
|
+
take(id) {
|
|
153
|
+
if (!id) return null;
|
|
154
|
+
const entry = entries.get(id);
|
|
155
|
+
if (!entry) return null;
|
|
156
|
+
entries.delete(id);
|
|
157
|
+
return { method: entry.method, url: entry.url };
|
|
158
|
+
},
|
|
159
|
+
prune,
|
|
160
|
+
get size() {
|
|
161
|
+
return entries.size;
|
|
162
|
+
},
|
|
163
|
+
get maxInflightRequests() {
|
|
164
|
+
return max;
|
|
165
|
+
},
|
|
166
|
+
get inflightTtlMs() {
|
|
167
|
+
return ttl;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Render the structured context suffix, e.g. `{tenantId=t1 correlationId=c9}`.
|
|
174
|
+
* Only keys that are actually present (and non-empty) are rendered.
|
|
175
|
+
*
|
|
176
|
+
* @returns {string} Dim-wrapped suffix, or '' when no context keys are present.
|
|
177
|
+
*/
|
|
178
|
+
function formatContext(obj) {
|
|
179
|
+
const pairs = [];
|
|
180
|
+
for (const key of CONTEXT_KEYS) {
|
|
181
|
+
const value = obj[key];
|
|
182
|
+
if (value === undefined || value === null || value === '') continue;
|
|
183
|
+
pairs.push(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`);
|
|
184
|
+
}
|
|
185
|
+
if (pairs.length === 0) return '';
|
|
186
|
+
return `${DIM}{${pairs.join(' ')}}${RESET}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
62
189
|
/**
|
|
63
190
|
* Format a single Pino log object into an NTL-style string.
|
|
64
191
|
*
|
|
65
192
|
* @param {Object} obj - Pino JSON log object
|
|
66
193
|
* @param {Object} opts - Transport options
|
|
194
|
+
* @param {Object} [tracker] - Optional in-flight request tracker (see
|
|
195
|
+
* createRequestTracker). When omitted, request/response pairing is skipped
|
|
196
|
+
* and only inline `req` data is used.
|
|
67
197
|
* @returns {string} Formatted log line
|
|
68
198
|
*/
|
|
69
|
-
function formatLine(obj, opts = {}) {
|
|
199
|
+
function formatLine(obj, opts = {}, tracker = null) {
|
|
70
200
|
const parts = [];
|
|
71
201
|
|
|
72
202
|
// 1. Timestamp
|
|
@@ -89,20 +219,45 @@ function formatLine(obj, opts = {}) {
|
|
|
89
219
|
parts.push(`${GREEN}[${location}]${RESET}:`);
|
|
90
220
|
}
|
|
91
221
|
|
|
92
|
-
// 5.
|
|
222
|
+
// 5. Structured context — {tenantId=… correlationId=…}
|
|
223
|
+
const context = formatContext(obj);
|
|
224
|
+
if (context) {
|
|
225
|
+
parts.push(context);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 6. Message
|
|
93
229
|
let msg = obj.msg || obj.message || '';
|
|
94
230
|
|
|
95
|
-
|
|
96
|
-
|
|
231
|
+
if (obj.req && !obj.res) {
|
|
232
|
+
// Request start — remember it so the completion line can name the route.
|
|
233
|
+
const method = (obj.req.method) || '';
|
|
234
|
+
const url = (obj.req.url) || '';
|
|
235
|
+
if (tracker && (method || url)) {
|
|
236
|
+
tracker.track(id, method, url);
|
|
237
|
+
}
|
|
238
|
+
if (method && url) {
|
|
239
|
+
msg = GENERIC_REQUEST_MESSAGES.has(msg) || !msg
|
|
240
|
+
? `${method} ${url}`
|
|
241
|
+
: `${method} ${url} ${GREY}—${RESET} ${msg}`;
|
|
242
|
+
}
|
|
243
|
+
} else if (obj.res && obj.responseTime !== undefined) {
|
|
244
|
+
// 7. HTTP request completion — format as: METHOD /path — STATUS (TIMEms)
|
|
97
245
|
const status = obj.res.statusCode;
|
|
98
246
|
const statusGroup = Math.floor(status / 100);
|
|
99
247
|
const statusColor = STATUS_COLORS[statusGroup] || '';
|
|
100
|
-
const method = (obj.req && obj.req.method) || '';
|
|
101
|
-
const url = (obj.req && obj.req.url) || '';
|
|
102
248
|
const time = typeof obj.responseTime === 'number'
|
|
103
249
|
? obj.responseTime.toFixed(1)
|
|
104
250
|
: obj.responseTime;
|
|
105
251
|
|
|
252
|
+
// Inline req wins; otherwise fall back to the tracked request-start line.
|
|
253
|
+
let method = (obj.req && obj.req.method) || '';
|
|
254
|
+
let url = (obj.req && obj.req.url) || '';
|
|
255
|
+
const tracked = tracker ? tracker.take(id) : null;
|
|
256
|
+
if (!(method && url) && tracked) {
|
|
257
|
+
method = tracked.method;
|
|
258
|
+
url = tracked.url;
|
|
259
|
+
}
|
|
260
|
+
|
|
106
261
|
if (method && url) {
|
|
107
262
|
msg = `${method} ${url} — ${statusColor}${status}${RESET} ${GREY}(${time}ms)${RESET}`;
|
|
108
263
|
} else if (msg) {
|
|
@@ -112,7 +267,7 @@ function formatLine(obj, opts = {}) {
|
|
|
112
267
|
}
|
|
113
268
|
}
|
|
114
269
|
|
|
115
|
-
//
|
|
270
|
+
// 8. Error stack traces
|
|
116
271
|
if (obj.err) {
|
|
117
272
|
const stack = obj.err.stack || obj.err.message || '';
|
|
118
273
|
if (stack) {
|
|
@@ -122,7 +277,8 @@ function formatLine(obj, opts = {}) {
|
|
|
122
277
|
|
|
123
278
|
parts.push(msg);
|
|
124
279
|
|
|
125
|
-
|
|
280
|
+
const line = parts.join(' ');
|
|
281
|
+
return opts.colorize === false ? line.replace(/\x1b\[[0-9;]*m/g, '') : line;
|
|
126
282
|
}
|
|
127
283
|
|
|
128
284
|
/**
|
|
@@ -149,24 +305,44 @@ function formatLine(obj, opts = {}) {
|
|
|
149
305
|
*
|
|
150
306
|
* Options:
|
|
151
307
|
* - defaultModule: Default location label when obj.module is not set (e.g., 'API')
|
|
308
|
+
* - colorize: Set to false to strip all ANSI codes from the output
|
|
309
|
+
* - maxInflightRequests: Cap on remembered in-flight requests (default 1000)
|
|
310
|
+
* - inflightTtlMs: Max age of a remembered in-flight request (default 60000)
|
|
152
311
|
*
|
|
153
312
|
* @param {Object} opts - Transport options
|
|
154
313
|
*/
|
|
155
314
|
module.exports = function (opts = {}) {
|
|
156
|
-
|
|
315
|
+
const tracker = createRequestTracker(opts);
|
|
316
|
+
|
|
317
|
+
const stream = split(function (line) {
|
|
157
318
|
let obj;
|
|
158
319
|
try {
|
|
159
320
|
obj = JSON.parse(line);
|
|
160
321
|
} catch {
|
|
161
|
-
|
|
322
|
+
writeOutput(line + '\n');
|
|
162
323
|
return;
|
|
163
324
|
}
|
|
164
|
-
const formatted = formatLine(obj, opts);
|
|
165
|
-
|
|
325
|
+
const formatted = formatLine(obj, opts, tracker);
|
|
326
|
+
writeOutput(formatted + '\n');
|
|
327
|
+
});
|
|
328
|
+
// The worker consumes only the writable side; drain the readable side so
|
|
329
|
+
// split2 can emit end/close and let Pino terminate its worker.
|
|
330
|
+
const flush = stream._flush.bind(stream);
|
|
331
|
+
stream._flush = callback => flush(error => {
|
|
332
|
+
if (error) return callback(error);
|
|
333
|
+
// stdout is asynchronous in a Pino worker. Wait for preceding writes before
|
|
334
|
+
// signalling completion, otherwise worker shutdown can discard the output.
|
|
335
|
+
if (isMainThread) process.stdout.write('', callback);
|
|
336
|
+
else callback();
|
|
166
337
|
});
|
|
338
|
+
stream.resume();
|
|
339
|
+
return stream;
|
|
167
340
|
};
|
|
168
341
|
|
|
169
342
|
// Export internals for testing
|
|
170
343
|
module.exports.formatLine = formatLine;
|
|
171
344
|
module.exports.formatTimestamp = formatTimestamp;
|
|
172
345
|
module.exports.padLevel = padLevel;
|
|
346
|
+
module.exports.formatContext = formatContext;
|
|
347
|
+
module.exports.createRequestTracker = createRequestTracker;
|
|
348
|
+
module.exports.CONTEXT_KEYS = CONTEXT_KEYS;
|