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