ntlogger 3.0.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 +242 -17
- 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 +10 -0
- package/index.js +18 -1
- package/lib/logObserver.js +55 -0
- package/lib/logger.js +17 -0
- package/lib/pinoOtel.js +43 -0
- package/lib/secretRedaction.js +10 -1
- package/lib/secretScope.js +24 -0
- package/package.json +47 -20
- package/pino.d.ts +28 -4
- package/pino.js +144 -43
- package/transports/pino.js +5 -0
package/index.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ declare namespace logger {
|
|
|
53
53
|
* Logger configuration options
|
|
54
54
|
*/
|
|
55
55
|
export interface LoggerConfig {
|
|
56
|
+
/** Maximum in-flight callback promises; overflow drops newest callback, not the primary log. */
|
|
57
|
+
onLogMaxPending?: number;
|
|
58
|
+
onLog?: (record: Record<string, any>) => void | Promise<void>;
|
|
59
|
+
otel?: boolean | { loggerProvider?: { getLogger(name: string): { emit(record: any): void }; forceFlush?(): Promise<void> }; name?: string };
|
|
60
|
+
redact?: false | { extraKeys?: string[]; patterns?: boolean; replacement?: string; maxDepth?: number; maxStringLength?: number };
|
|
56
61
|
/** Minimum log level to be logged */
|
|
57
62
|
level?: LogLevel;
|
|
58
63
|
/** Enable console logging (default: true) */
|
|
@@ -95,6 +100,9 @@ declare namespace logger {
|
|
|
95
100
|
* Statistics object returned by getStats()
|
|
96
101
|
*/
|
|
97
102
|
export interface LoggerStats {
|
|
103
|
+
levels: Record<string, number>;
|
|
104
|
+
hookErrors: number;
|
|
105
|
+
onLog: { pending: number; dropped: number; limit: number };
|
|
98
106
|
sampling: {
|
|
99
107
|
total: Record<string, number>;
|
|
100
108
|
sampled: Record<string, number>;
|
|
@@ -199,6 +207,7 @@ declare namespace logger {
|
|
|
199
207
|
* @returns Statistics object
|
|
200
208
|
*/
|
|
201
209
|
getStats(): LoggerStats;
|
|
210
|
+
resetStats(): void;
|
|
202
211
|
|
|
203
212
|
/**
|
|
204
213
|
* Flush all pending logs (returns Promise)
|
|
@@ -227,6 +236,7 @@ declare function logger(location?: string, config?: logger.LoggerConfig): logger
|
|
|
227
236
|
|
|
228
237
|
declare namespace logger {
|
|
229
238
|
/** Opt in to process-wide signal handlers; returns an unregister function. */
|
|
239
|
+
function withSecretValues<T>(values: string[], callback: () => T): T;
|
|
230
240
|
function setupSignalHandlers(options?: { timeout?: number }): () => void;
|
|
231
241
|
}
|
|
232
242
|
|
package/index.js
CHANGED
|
@@ -114,4 +114,21 @@
|
|
|
114
114
|
* Initial release
|
|
115
115
|
*/
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
// Only the Winston entry point resolves these peers. Pino consumers never load it.
|
|
118
|
+
for (const peer of ['winston', 'winston-transport']) {
|
|
119
|
+
try {
|
|
120
|
+
require.resolve(peer);
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
if (cause.code !== 'MODULE_NOT_FOUND') throw cause;
|
|
123
|
+
const error = new Error(
|
|
124
|
+
`ntlogger's Winston backend requires ${peer}. Run: npm install winston winston-transport. ` +
|
|
125
|
+
`For Pino, import 'ntlogger/pino' instead.`,
|
|
126
|
+
{ cause },
|
|
127
|
+
);
|
|
128
|
+
error.code = 'MODULE_NOT_FOUND';
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = require('./lib/logger');
|
|
134
|
+
module.exports.withSecretValues = require('./lib/secretScope').withSecretValues;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
3
|
+
const { withTimeout } = require('./lifecycle');
|
|
4
|
+
|
|
5
|
+
// Counts represent serialized records, not confirmed delivery to any destination.
|
|
6
|
+
function createLogObserver({ onLog, otel, onLogMaxPending = 100 } = {}) {
|
|
7
|
+
if (onLog !== undefined && typeof onLog !== 'function') throw new TypeError('onLog must be a function');
|
|
8
|
+
if (!Number.isSafeInteger(onLogMaxPending) || onLogMaxPending < 0) throw new TypeError('onLogMaxPending must be a nonnegative safe integer');
|
|
9
|
+
const scope = new AsyncLocalStorage();
|
|
10
|
+
const pending = new Set();
|
|
11
|
+
const counts = Object.fromEntries(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'internal'].map(k => [k, 0]));
|
|
12
|
+
let hookErrors = 0;
|
|
13
|
+
let dropped = 0;
|
|
14
|
+
const bridge = otel ? require('./pinoOtel').createOtelBridge(otel) : null;
|
|
15
|
+
const levelNames = { 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal' };
|
|
16
|
+
function failure() { hookErrors++; }
|
|
17
|
+
function observe(record) {
|
|
18
|
+
const level = typeof record.level === 'number' ? levelNames[record.level] : record.level;
|
|
19
|
+
if (Object.hasOwn(counts, level)) counts[level]++;
|
|
20
|
+
// Suppress observers for logs made by an observer, including async descendants.
|
|
21
|
+
if (scope.getStore()) return;
|
|
22
|
+
scope.run(true, () => {
|
|
23
|
+
if (bridge) {
|
|
24
|
+
try { bridge.emit(record); } catch (_) { failure(); }
|
|
25
|
+
}
|
|
26
|
+
if (onLog) {
|
|
27
|
+
// In-flight promises cannot be cancelled: drop the newest callback only.
|
|
28
|
+
if (pending.size >= onLogMaxPending) { dropped++; return; }
|
|
29
|
+
try {
|
|
30
|
+
// Mutation of the callback record cannot change output or OTel data.
|
|
31
|
+
const result = onLog(JSON.parse(JSON.stringify(record)));
|
|
32
|
+
if (result && typeof result.then === 'function') {
|
|
33
|
+
const task = Promise.resolve(result).catch(failure);
|
|
34
|
+
pending.add(task);
|
|
35
|
+
task.finally(() => pending.delete(task));
|
|
36
|
+
}
|
|
37
|
+
} catch (_) { failure(); }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
context: () => bridge ? bridge.context() : {},
|
|
43
|
+
observe,
|
|
44
|
+
streamWrite(line) { observe(JSON.parse(line)); return line; },
|
|
45
|
+
stats: () => ({ levels: { ...counts }, hookErrors, onLog: { pending: pending.size, dropped, limit: onLogMaxPending } }),
|
|
46
|
+
reset() { for (const key of Object.keys(counts)) counts[key] = 0; hookErrors = 0; dropped = 0; },
|
|
47
|
+
async flush(timeout = 5000) {
|
|
48
|
+
await withTimeout((async () => {
|
|
49
|
+
while (pending.size) await Promise.all([...pending]);
|
|
50
|
+
if (bridge) await bridge.flush();
|
|
51
|
+
})(), timeout, 'Log observers flush');
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
module.exports = { createLogObserver };
|
package/lib/logger.js
CHANGED
|
@@ -344,6 +344,8 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
344
344
|
statsInterval = 0,
|
|
345
345
|
} = config;
|
|
346
346
|
|
|
347
|
+
const observer = require('./logObserver').createLogObserver(config);
|
|
348
|
+
const redactor = config.redact === false ? null : require('./secretRedaction').createRedactor(config.redact);
|
|
347
349
|
let logTransport = [].filter(Boolean);
|
|
348
350
|
|
|
349
351
|
if (console) {
|
|
@@ -376,6 +378,13 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
376
378
|
const logger = winston.createLogger({
|
|
377
379
|
level: level,
|
|
378
380
|
levels: customSettings.levels,
|
|
381
|
+
format: winston.format(info => {
|
|
382
|
+
const safe = redactor ? redactor.redactObject(info) : { ...info };
|
|
383
|
+
Object.assign(safe, observer.context());
|
|
384
|
+
Object.assign(info, safe);
|
|
385
|
+
observer.observe(safe);
|
|
386
|
+
return info;
|
|
387
|
+
})(),
|
|
379
388
|
transports: logTransport,
|
|
380
389
|
defaultMeta: {
|
|
381
390
|
location,
|
|
@@ -469,10 +478,17 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
469
478
|
sampling: sampler ? sampler.getStats() : null,
|
|
470
479
|
deduplication: deduplicator ? deduplicator.getStats() : null,
|
|
471
480
|
performance: perfTracker ? perfTracker.getStats() : null,
|
|
481
|
+
...observer.stats(),
|
|
472
482
|
};
|
|
473
483
|
return stats;
|
|
474
484
|
};
|
|
475
485
|
|
|
486
|
+
logger.resetStats = () => {
|
|
487
|
+
sampler?.resetStats();
|
|
488
|
+
deduplicator?.resetStats();
|
|
489
|
+
observer.reset();
|
|
490
|
+
};
|
|
491
|
+
|
|
476
492
|
const timeout = config.shutdownTimeout ?? 30000;
|
|
477
493
|
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Invalid shutdownTimeout');
|
|
478
494
|
let closePromise;
|
|
@@ -480,6 +496,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
480
496
|
logger.flush = () => withTimeout((async () => {
|
|
481
497
|
await drain(logger, state, timeout);
|
|
482
498
|
await Promise.all(logger.transports.map(t => t.flush?.()));
|
|
499
|
+
await observer.flush(timeout);
|
|
483
500
|
await drain(logger, state, timeout);
|
|
484
501
|
})(), timeout, 'Logger flush');
|
|
485
502
|
|
package/lib/pinoOtel.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// No SDK/provider is created or registered here. Export through the application's
|
|
4
|
+
// existing logs provider in the calling thread, where active context is available.
|
|
5
|
+
function createOtelBridge(options) {
|
|
6
|
+
const opts = options === true ? {} : options;
|
|
7
|
+
const api = require('@opentelemetry/api');
|
|
8
|
+
const provider = opts.loggerProvider || require('@opentelemetry/api-logs').logs.getLoggerProvider();
|
|
9
|
+
const logger = provider.getLogger(opts.name || 'ntlogger');
|
|
10
|
+
const severity = { 10: 1, 20: 5, 30: 9, 40: 13, 50: 17, 60: 21,
|
|
11
|
+
trace: 1, debug: 5, info: 9, warn: 13, error: 17, fatal: 21, internal: 1 };
|
|
12
|
+
const names = { 1: 'TRACE', 5: 'DEBUG', 9: 'INFO', 13: 'WARN', 17: 'ERROR', 21: 'FATAL' };
|
|
13
|
+
return {
|
|
14
|
+
context() {
|
|
15
|
+
const span = api.trace.getActiveSpan();
|
|
16
|
+
const ctx = span && span.spanContext();
|
|
17
|
+
return ctx && api.isSpanContextValid(ctx)
|
|
18
|
+
? { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.traceFlags }
|
|
19
|
+
: {};
|
|
20
|
+
},
|
|
21
|
+
emit(record) {
|
|
22
|
+
const attributes = {};
|
|
23
|
+
for (const [key, value] of Object.entries(record)) {
|
|
24
|
+
if (['level', 'msg', 'message', 'time', 'timestamp'].includes(key) || value == null) continue;
|
|
25
|
+
attributes[key] = typeof value === 'object' ? JSON.stringify(value) : value;
|
|
26
|
+
}
|
|
27
|
+
if (record.err) {
|
|
28
|
+
if (record.err.type) attributes['exception.type'] = record.err.type;
|
|
29
|
+
if (record.err.message) attributes['exception.message'] = record.err.message;
|
|
30
|
+
if (record.err.stack) attributes['exception.stacktrace'] = record.err.stack;
|
|
31
|
+
}
|
|
32
|
+
const severityNumber = severity[record.level] || 9;
|
|
33
|
+
logger.emit({
|
|
34
|
+
context: api.context.active(),
|
|
35
|
+
timestamp: new Date(record.time ?? record.timestamp ?? Date.now()),
|
|
36
|
+
severityNumber, severityText: names[severityNumber],
|
|
37
|
+
body: record.msg ?? record.message ?? '', attributes,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
flush: () => typeof provider.forceFlush === 'function' ? provider.forceFlush() : Promise.resolve(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
module.exports = { createOtelBridge };
|
package/lib/secretRedaction.js
CHANGED
|
@@ -87,6 +87,10 @@ const DEFAULT_KEYS = Object.freeze([
|
|
|
87
87
|
'webhookurl',
|
|
88
88
|
'snmpcommunity',
|
|
89
89
|
'community_string',
|
|
90
|
+
'community',
|
|
91
|
+
'authPassword',
|
|
92
|
+
'privPassword',
|
|
93
|
+
'passphrase',
|
|
90
94
|
]);
|
|
91
95
|
|
|
92
96
|
/*
|
|
@@ -145,6 +149,9 @@ const KV_SECRET = /(?<pre>\b(?:api[_-]?key|apikey|access[_-]?key|access[_-]?toke
|
|
|
145
149
|
*/
|
|
146
150
|
const GENERIC_KV = /(?<pre>\b(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*)(?!["'[])\S+/gi;
|
|
147
151
|
|
|
152
|
+
/** Scanner credentials in free text, including short and quoted values. */
|
|
153
|
+
const SCANNER_SECRET = /(?<pre>["']?\b(?:snmp[_-]?community|community(?:[_-]?string)?|auth[_-]?password|priv[_-]?password|passphrase)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|[^\s,;}]+)/gi;
|
|
154
|
+
|
|
148
155
|
/** Default pattern set, in application order. */
|
|
149
156
|
const DEFAULT_PATTERNS = Object.freeze([
|
|
150
157
|
PEM_PRIVATE_KEY,
|
|
@@ -156,6 +163,7 @@ const DEFAULT_PATTERNS = Object.freeze([
|
|
|
156
163
|
BEARER_TOKEN,
|
|
157
164
|
BASIC_AUTH,
|
|
158
165
|
DISCORD_WEBHOOK,
|
|
166
|
+
SCANNER_SECRET,
|
|
159
167
|
KV_SECRET,
|
|
160
168
|
GENERIC_KV,
|
|
161
169
|
URL_CREDENTIALS,
|
|
@@ -177,6 +185,7 @@ const PATTERN_HINTS = new Map([
|
|
|
177
185
|
[BEARER_TOKEN, 'bearer'],
|
|
178
186
|
[BASIC_AUTH, 'basic'],
|
|
179
187
|
[DISCORD_WEBHOOK, 'discord'],
|
|
188
|
+
[SCANNER_SECRET, 'community|auth[_-]?password|priv[_-]?password|passphrase'],
|
|
180
189
|
[KV_SECRET, 'key|secret|passw|pwd|token'],
|
|
181
190
|
[GENERIC_KV, 'key|secret|passw|token'],
|
|
182
191
|
[URL_CREDENTIALS, '://'],
|
|
@@ -384,7 +393,7 @@ function createRedactor(options) {
|
|
|
384
393
|
return value;
|
|
385
394
|
}
|
|
386
395
|
try {
|
|
387
|
-
let text = value;
|
|
396
|
+
let text = require('./secretScope').redactSecretValues(value, replacement);
|
|
388
397
|
let truncated = false;
|
|
389
398
|
if (text.length > maxStringLength) {
|
|
390
399
|
text = text.slice(0, maxStringLength);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
3
|
+
const scope = new AsyncLocalStorage();
|
|
4
|
+
|
|
5
|
+
function withSecretValues(values, callback) {
|
|
6
|
+
if (!Array.isArray(values) || values.some(v => typeof v !== 'string' || !v)) {
|
|
7
|
+
throw new TypeError('Secret values must be nonempty strings');
|
|
8
|
+
}
|
|
9
|
+
const secrets = [...new Set([...(scope.getStore() || []), ...values])];
|
|
10
|
+
if (secrets.length > 128 || secrets.some(v => v.length > 65536)) {
|
|
11
|
+
throw new RangeError('Secret scope exceeds 128 values or 65536 characters per value');
|
|
12
|
+
}
|
|
13
|
+
return scope.run(secrets, callback);
|
|
14
|
+
}
|
|
15
|
+
function redactSecretValues(text, replacement) {
|
|
16
|
+
const secrets = scope.getStore();
|
|
17
|
+
if (!secrets?.length) return text;
|
|
18
|
+
const variants = [...new Set(secrets.flatMap(value => [value, JSON.stringify(value).slice(1, -1)]))]
|
|
19
|
+
.sort((a, b) => b.length - a.length);
|
|
20
|
+
// A single pass avoids redacting the replacement itself or missing overlaps.
|
|
21
|
+
const pattern = new RegExp([replacement, ...variants].map(value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
22
|
+
return text.replace(pattern, () => replacement);
|
|
23
|
+
}
|
|
24
|
+
module.exports = { withSecretValues, redactSecretValues, hasSecretValues: () => Boolean(scope.getStore()?.length) };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntlogger",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Structured logging with Winston and Pino backends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"custom logger",
|
|
7
7
|
"discord compatible",
|
|
@@ -38,7 +38,13 @@
|
|
|
38
38
|
"types": "./pino.d.ts",
|
|
39
39
|
"default": "./pino.js"
|
|
40
40
|
},
|
|
41
|
-
"./package.json": "./package.json"
|
|
41
|
+
"./package.json": "./package.json",
|
|
42
|
+
"./eslint": {
|
|
43
|
+
"types": "./eslint.d.ts",
|
|
44
|
+
"default": "./eslint.js"
|
|
45
|
+
},
|
|
46
|
+
"./contract/redaction.json": "./contract/redaction.json",
|
|
47
|
+
"./contract/conformance.json": "./contract/conformance.json"
|
|
42
48
|
},
|
|
43
49
|
"types": "./index.d.ts",
|
|
44
50
|
"files": [
|
|
@@ -48,31 +54,37 @@
|
|
|
48
54
|
"pino.d.ts",
|
|
49
55
|
"lib/*.js",
|
|
50
56
|
"plugins/**/*",
|
|
51
|
-
"transports/"
|
|
57
|
+
"transports/",
|
|
58
|
+
"eslint.js",
|
|
59
|
+
"eslint.d.ts",
|
|
60
|
+
"eslint-rules/",
|
|
61
|
+
"contract/"
|
|
52
62
|
],
|
|
53
63
|
"scripts": {
|
|
54
64
|
"test": "jest",
|
|
55
65
|
"test:coverage": "jest --runInBand --coverage",
|
|
56
|
-
"test:unit": "jest --runInBand --testPathIgnorePatterns=tests/plugins/mysql.test.js tests/plugins/postgres.test.js"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"winston-transport": "^4.9.0"
|
|
66
|
+
"test:unit": "jest --runInBand --testPathIgnorePatterns=tests/plugins/mysql.test.js tests/plugins/postgres.test.js",
|
|
67
|
+
"test:package": "node scripts/test-package.cjs",
|
|
68
|
+
"test:python": "python3 -W error -m unittest discover -s python/tests",
|
|
69
|
+
"test:contract": "node scripts/sync-python-contract.cjs --check"
|
|
61
70
|
},
|
|
62
71
|
"optionalDependencies": {
|
|
63
72
|
"split2": "^4.2.0"
|
|
64
73
|
},
|
|
65
74
|
"peerDependencies": {
|
|
66
|
-
"pino": ">=8.0.0",
|
|
67
|
-
"@sentry/node": "^10.74.0",
|
|
68
|
-
"mysql2": "^3.24.4",
|
|
69
|
-
"pg": "^8.23.0",
|
|
70
75
|
"@opentelemetry/api": ">=1.9.0",
|
|
71
76
|
"@opentelemetry/api-logs": "^0.222.0",
|
|
72
|
-
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
73
77
|
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
74
78
|
"@opentelemetry/resources": "^2.11.0",
|
|
75
|
-
"@opentelemetry/
|
|
79
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
80
|
+
"@opentelemetry/semantic-conventions": ">=1.20.0",
|
|
81
|
+
"@sentry/node": "^10.74.0",
|
|
82
|
+
"mysql2": "^3.24.4",
|
|
83
|
+
"pg": "^8.23.0",
|
|
84
|
+
"pino": ">=9.14.0",
|
|
85
|
+
"winston": "^3.19.0",
|
|
86
|
+
"winston-transport": "^4.9.0",
|
|
87
|
+
"eslint": ">=9.0.0"
|
|
76
88
|
},
|
|
77
89
|
"peerDependenciesMeta": {
|
|
78
90
|
"pino": {
|
|
@@ -104,21 +116,36 @@
|
|
|
104
116
|
},
|
|
105
117
|
"@opentelemetry/semantic-conventions": {
|
|
106
118
|
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"winston": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"winston-transport": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"eslint": {
|
|
127
|
+
"optional": true
|
|
107
128
|
}
|
|
108
129
|
},
|
|
109
130
|
"devDependencies": {
|
|
131
|
+
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
132
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
133
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
110
134
|
"@sentry/node": "^10.74.0",
|
|
111
135
|
"@sentry/profiling-node": "^10.74.0",
|
|
112
|
-
"mysql2": "^3.24.4",
|
|
113
|
-
"pg": "^8.23.0",
|
|
114
136
|
"@testcontainers/mysql": "^12.1.0",
|
|
115
137
|
"@testcontainers/postgresql": "^12.1.0",
|
|
116
138
|
"dotenv": "^17.4.2",
|
|
139
|
+
"eslint": "^10.10.0",
|
|
140
|
+
"fastify": "^5.12.4",
|
|
117
141
|
"jest": "^30.5.1",
|
|
118
|
-
"
|
|
142
|
+
"mysql2": "^3.24.4",
|
|
143
|
+
"pg": "^8.23.0",
|
|
119
144
|
"pino": "^10.3.1",
|
|
120
|
-
"
|
|
121
|
-
"
|
|
145
|
+
"testcontainers": "^12.1.0",
|
|
146
|
+
"typescript": "^5.9.3",
|
|
147
|
+
"winston": "^3.19.0",
|
|
148
|
+
"winston-transport": "^4.9.0"
|
|
122
149
|
},
|
|
123
150
|
"engines": {
|
|
124
151
|
"node": ">=22.22.2"
|
package/pino.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ChildLoggerOptions,
|
|
18
18
|
DestinationStream,
|
|
19
19
|
Logger as PinoLogger,
|
|
20
|
+
LoggerOptions,
|
|
20
21
|
} from 'pino';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -126,6 +127,9 @@ declare namespace ntlPinoTransport {
|
|
|
126
127
|
|
|
127
128
|
/** Snapshot returned by `logger.getStats()`. Members are `null` when unconfigured. */
|
|
128
129
|
export interface NtlPinoStats {
|
|
130
|
+
levels: Record<NtlLevel, number>;
|
|
131
|
+
hookErrors: number;
|
|
132
|
+
onLog: { pending: number; dropped: number; limit: number };
|
|
129
133
|
sampling: SamplingStats | null;
|
|
130
134
|
deduplication: DeduplicationStats | null;
|
|
131
135
|
}
|
|
@@ -175,6 +179,12 @@ declare namespace ntlPinoTransport {
|
|
|
175
179
|
|
|
176
180
|
/** Options for `createLogger()`. */
|
|
177
181
|
export interface CreateLoggerOptions {
|
|
182
|
+
/** Receives a detached redacted record; async work is drained by close(). */
|
|
183
|
+
/** Maximum in-flight callback promises; overflow drops newest callback, not the primary log. */
|
|
184
|
+
onLogMaxPending?: number;
|
|
185
|
+
onLog?: (record: Record<string, any>) => void | Promise<void>;
|
|
186
|
+
/** Use the application's OTel provider; never registers or shuts down a provider. */
|
|
187
|
+
otel?: boolean | { loggerProvider?: { getLogger(name: string): { emit(record: any): void }; forceFlush?(): Promise<void> }; name?: string };
|
|
178
188
|
/** Pino level. Defaults to `LOG_LEVEL`, else `debug` in dev / `info` in production. */
|
|
179
189
|
level?: string;
|
|
180
190
|
/** Module label added to every line and used as the transport's default label. */
|
|
@@ -214,13 +224,13 @@ declare namespace ntlPinoTransport {
|
|
|
214
224
|
* properties, so children inherit them through the prototype chain.
|
|
215
225
|
*/
|
|
216
226
|
export interface NtlPinoExtras {
|
|
217
|
-
/**
|
|
227
|
+
/** Emitted level counts, callback/export hook errors, sampling and deduplication. */
|
|
218
228
|
getStats(): NtlPinoStats;
|
|
219
|
-
/** Zero the
|
|
229
|
+
/** Zero all counters shared by the root and its children. */
|
|
220
230
|
resetStats(): void;
|
|
221
231
|
/**
|
|
222
232
|
* Release sampler/deduplicator timers, then drain the destination.
|
|
223
|
-
*
|
|
233
|
+
* Closing a child leaves its parent open. Calls after close throw, even at disabled levels.
|
|
224
234
|
*/
|
|
225
235
|
close(timeout?: number): Promise<DrainResult>;
|
|
226
236
|
/** Present only when `processHandlers` was enabled. */
|
|
@@ -237,10 +247,24 @@ declare namespace ntlPinoTransport {
|
|
|
237
247
|
export type NtlPinoLogger = NtlPinoExtras & PinoLogger;
|
|
238
248
|
|
|
239
249
|
/** Create a pre-configured Pino logger that follows NightTimeLogger conventions. */
|
|
250
|
+
export function withSecretValues<T>(values: string[], callback: () => T): T;
|
|
251
|
+
|
|
240
252
|
export function createLogger(opts?: CreateLoggerOptions): NtlPinoLogger;
|
|
241
253
|
|
|
254
|
+
/** Framework-owned JSON logger options; no ntlogger lifecycle or timers. */
|
|
255
|
+
export type CreatePinoOptions = Pick<CreateLoggerOptions,
|
|
256
|
+
'level' | 'module' | 'service' | 'context' | 'contextProvider' | 'contextKeys' | 'redact' | 'silent' | 'onLog' | 'onLogMaxPending' | 'otel'
|
|
257
|
+
> & { serializers?: LoggerOptions['serializers'] };
|
|
258
|
+
export function createPinoOptions(opts?: CreatePinoOptions): LoggerOptions;
|
|
259
|
+
|
|
260
|
+
/** Synchronous capture of serialized, redacted records; explicitly silent loggers capture nothing. */
|
|
261
|
+
export function createTestLogger(opts?: Omit<CreateLoggerOptions, 'destination' | 'processHandlers'>): {
|
|
262
|
+
logger: NtlPinoLogger;
|
|
263
|
+
records: Array<Record<string, any>>;
|
|
264
|
+
};
|
|
265
|
+
|
|
242
266
|
/**
|
|
243
|
-
* Resolve silent mode: `opts.silent` -> `NTLOGGER_SILENT` -> `NODE_ENV === 'test'`.
|
|
267
|
+
* Resolve silent mode: `opts.silent` -> `NTLOGGER_SILENT` -> `NODE_ENV === 'test'` or nonempty `NODE_TEST_CONTEXT`.
|
|
244
268
|
*/
|
|
245
269
|
export function isSilent(opts?: { silent?: boolean }): boolean;
|
|
246
270
|
|