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.
@@ -0,0 +1,115 @@
1
+ {
2
+ "version": 1,
3
+ "keys": [
4
+ "password",
5
+ "passwd",
6
+ "pass",
7
+ "pwd",
8
+ "secret",
9
+ "token",
10
+ "apikey",
11
+ "api_key",
12
+ "x-api-key",
13
+ "authorization",
14
+ "proxy-authorization",
15
+ "cookie",
16
+ "set-cookie",
17
+ "x-device-installation-secret",
18
+ "installationsecret",
19
+ "privatekey",
20
+ "private_key",
21
+ "clientsecret",
22
+ "client_secret",
23
+ "accesstoken",
24
+ "access_token",
25
+ "refreshtoken",
26
+ "refresh_token",
27
+ "idtoken",
28
+ "id_token",
29
+ "sessiontoken",
30
+ "credential",
31
+ "credentials",
32
+ "webhookurl",
33
+ "snmpcommunity",
34
+ "community_string",
35
+ "community",
36
+ "authPassword",
37
+ "privPassword",
38
+ "passphrase"
39
+ ],
40
+ "patterns": [
41
+ {
42
+ "source": "-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY-----",
43
+ "flags": "g"
44
+ },
45
+ {
46
+ "source": "\\bAKIA[0-9A-Z]{16}\\b",
47
+ "flags": "g"
48
+ },
49
+ {
50
+ "source": "\\beyJ[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}",
51
+ "flags": "g"
52
+ },
53
+ {
54
+ "source": "\\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})",
55
+ "flags": "g"
56
+ },
57
+ {
58
+ "source": "\\bxox[abpors]-[A-Za-z0-9-]{10,}",
59
+ "flags": "gi"
60
+ },
61
+ {
62
+ "source": "\\b(?:[sr]k_(?:live|test)_[A-Za-z0-9]{10,}|whsec_[A-Za-z0-9]{10,})",
63
+ "flags": "g"
64
+ },
65
+ {
66
+ "source": "(?<pre>\\bBearer\\s+)[A-Za-z0-9\\-._~+/]{8,}={0,2}",
67
+ "flags": "gi"
68
+ },
69
+ {
70
+ "source": "(?<pre>\\bBasic\\s+)(?=[A-Za-z0-9+/]*[A-Z0-9])[A-Za-z0-9+/]{16,}={0,2}",
71
+ "flags": "g"
72
+ },
73
+ {
74
+ "source": "(?<pre>https?:\\/\\/(?:[a-z]+\\.)?discord(?:app)?\\.com\\/api\\/(?:v\\d+\\/)?webhooks\\/\\d+\\/)[A-Za-z0-9_-]+",
75
+ "flags": "gi"
76
+ },
77
+ {
78
+ "source": "(?<pre>[\"']?\\b(?:snmp[_-]?community|community(?:[_-]?string)?|auth[_-]?password|priv[_-]?password|passphrase)[\"']?\\s*[:=]\\s*)(?:\"(?:\\\\.|[^\"\\\\])*(?:\"|$)|'(?:\\\\.|[^'\\\\])*(?:'|$)|[^\\s,;}]+)",
79
+ "flags": "gi"
80
+ },
81
+ {
82
+ "source": "(?<pre>\\b(?:api[_-]?key|apikey|access[_-]?key|access[_-]?token|refresh[_-]?token|secret[_-]?key|client[_-]?secret|password|passwd|pwd|auth[_-]?token|private[_-]?key|token|secret)\\s*[:=]\\s*[\"']?)[^\\s'\",;]{4,}",
83
+ "flags": "gi"
84
+ },
85
+ {
86
+ "source": "(?<pre>\\b(?:password|passwd|secret|token|api[_-]?key)\\s*[=:]\\s*)(?![\"'[])\\S+",
87
+ "flags": "gi"
88
+ },
89
+ {
90
+ "source": "(?<pre>\\b[a-z][a-z0-9+.-]*:\\/\\/[^\\s:/?#@]*:)[^\\s/?#@]+(?<post>@)",
91
+ "flags": "gi"
92
+ }
93
+ ],
94
+ "pythonLevels": {
95
+ "5": 10,
96
+ "10": 20,
97
+ "20": 30,
98
+ "30": 40,
99
+ "40": 50,
100
+ "50": 60
101
+ },
102
+ "contextAliases": {
103
+ "agent_uuid": "agentUuid",
104
+ "tenant_id": "tenantId",
105
+ "command_id": "commandId",
106
+ "collection_cycle_id": "collectionCycleId",
107
+ "correlation_id": "correlationId",
108
+ "run_id": "runId",
109
+ "work_id": "workId",
110
+ "stage_id": "stageId",
111
+ "device_id": "deviceId",
112
+ "trace_id": "traceId",
113
+ "span_id": "spanId"
114
+ }
115
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ function receiverName(node) {
4
+ if (node.type === 'Identifier') return node.name;
5
+ if (node.type === 'ThisExpression') return 'this';
6
+ if (node.type === 'MemberExpression' && !node.computed) {
7
+ const prefix = receiverName(node.object);
8
+ return prefix && `${prefix}.${node.property.name}`;
9
+ }
10
+ return null;
11
+ }
12
+ module.exports = {
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: { description: 'Keep interpolated Pino log fields searchable with an object-first call.' },
16
+ fixable: 'code',
17
+ schema: [{ type: 'object', properties: {
18
+ loggerNames: { type: 'array', items: { type: 'string' }, uniqueItems: true },
19
+ }, additionalProperties: false }],
20
+ messages: { structured: 'Pass searchable fields as the first argument to this logger.' },
21
+ },
22
+ create(context) {
23
+ const names = new Set(context.options[0]?.loggerNames || ['log', 'logger', 'req.log', 'request.log', 'app.log', 'fastify.log']);
24
+ const methods = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
25
+ const source = context.sourceCode;
26
+ function constantPrimitive(identifier) {
27
+ let scope = source.getScope(identifier);
28
+ let variable;
29
+ while (scope && !variable) { variable = scope.set.get(identifier.name); scope = scope.upper; }
30
+ const def = variable?.defs[0];
31
+ return variable?.defs.length === 1 && def?.parent?.kind === 'const'
32
+ && def.node.id.type === 'Identifier' && def.node.init?.type === 'Literal'
33
+ && !def.node.init.regex && typeof def.node.init.value !== 'object';
34
+ }
35
+ return {
36
+ CallExpression(node) {
37
+ const callee = node.callee;
38
+ if (callee.type !== 'MemberExpression' || callee.computed
39
+ || !methods.has(callee.property.name) || !names.has(receiverName(callee.object))) return;
40
+ const template = node.arguments[0];
41
+ if (template?.type !== 'TemplateLiteral' || !template.expressions.length) return;
42
+ const canFix = node.arguments.length === 1 && template.expressions.every(expr =>
43
+ expr.type === 'Identifier' && expr.name !== '__proto__' && constantPrimitive(expr));
44
+ context.report({ node: template, messageId: 'structured',
45
+ // Keep the original message and its formatting. Only duplicate reads
46
+ // of known primitive consts; calls/getters/mutable bindings need review.
47
+ fix: canFix ? fixer => fixer.insertTextBefore(template,
48
+ `{ ${[...new Set(template.expressions.map(expr => expr.name))].join(', ')} }, `) : undefined,
49
+ });
50
+ },
51
+ };
52
+ },
53
+ };
package/eslint.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ESLint } from 'eslint';
2
+ declare const plugin: ESLint.Plugin;
3
+ export = plugin;
package/eslint.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ meta: { name: 'ntlogger' },
4
+ rules: { 'prefer-object-first': require('./eslint-rules/prefer-object-first') },
5
+ };
package/index.d.ts CHANGED
@@ -3,214 +3,227 @@
3
3
  * @file index.d.ts
4
4
  */
5
5
 
6
- /**
7
- * Log levels supported by NightTimeLogger
8
- */
9
- export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'internal';
10
-
11
- /**
12
- * Sampling configuration for log levels
13
- */
14
- export interface SamplingConfig {
15
- [level: string]: number; // 0.0 to 1.0, where 1.0 = log all, 0.1 = log 10%
16
- }
17
-
18
- /**
19
- * Rate limit configuration for a log level
20
- */
21
- export interface RateLimitConfig {
22
- max: number; // Maximum number of logs
23
- window: number; // Time window in milliseconds
24
- }
25
-
26
- /**
27
- * Rate limiting configuration for log levels
28
- */
29
- export interface RateLimitConfigMap {
30
- [level: string]: RateLimitConfig;
31
- }
32
-
33
- /**
34
- * Deduplication configuration
35
- */
36
- export interface DeduplicationConfig {
37
- enabled: boolean;
38
- threshold: number; // Number of duplicates before squishing (default: 3)
39
- window: number; // Time window in milliseconds (default: 60000)
40
- }
41
-
42
- /**
43
- * Plugin configuration
44
- */
45
- export interface PluginConfig {
46
- name: string;
47
- enabled?: boolean;
48
- config?: Record<string, any>;
49
- }
50
-
51
- /**
52
- * Logger configuration options
53
- */
54
- export interface LoggerConfig {
55
- /** Minimum log level to be logged */
56
- level?: LogLevel;
57
- /** Enable console logging (default: true) */
58
- console?: boolean;
59
- /** Enable file logging (default: true) */
60
- file?: boolean;
61
- /** Filename for log file (default: `${location}.log`) */
62
- filename?: string;
63
- /** Directory path where log files will be saved (default: './logs') */
64
- path?: string;
65
- /** Maximum size of log file in bytes (default: 1048576 = 1MB) */
66
- maxSize?: number;
67
- /** Maximum number of log files to retain (default: 5) */
68
- maxFiles?: number;
69
- /** Include timestamp in log messages (default: true) */
70
- timestamp?: boolean;
71
- /** Skip cache and create new logger instance (default: false) */
72
- skipCache?: boolean;
73
- /** Enable call site path reporting (default: false) */
74
- reportPath?: boolean;
75
- /** Enable debug mode for logger itself (default: false) */
76
- debug?: boolean;
77
- /** Log sampling rates per level */
78
- sampling?: SamplingConfig;
79
- /** Rate limiting configuration per level */
80
- rateLimit?: RateLimitConfigMap;
81
- /** Log deduplication configuration */
82
- deduplication?: DeduplicationConfig;
83
- /** Enable performance metrics (default: NODE_ENV === 'development') */
84
- performanceMetrics?: boolean;
85
- /** Interval in milliseconds to log statistics (default: 0 = disabled) */
86
- statsInterval?: number;
87
- /** Plugin configurations */
88
- plugins?: PluginConfig[];
89
- }
90
-
91
- /**
92
- * Statistics object returned by getStats()
93
- */
94
- export interface LoggerStats {
95
- sampling: {
96
- total: Record<string, number>;
97
- sampled: Record<string, number>;
98
- rateLimited: Record<string, number>;
99
- } | null;
100
- deduplication: {
101
- totalDeduplicated: number;
102
- uniqueMessages: number;
103
- activeEntries: number;
104
- } | null;
105
- performance: {
106
- enabled: boolean;
107
- avgLogProcessingTime?: number;
108
- avgTransportTime?: number;
109
- logProcessingSamples?: number;
110
- transportSamples?: number;
111
- } | null;
112
- }
113
-
114
- /**
115
- * Logger instance interface
116
- */
117
- export interface Logger {
118
- /**
119
- * Log an informational message
120
- * @param message - The message to log
121
- * @param meta - Optional metadata object
122
- */
123
- info(message: string, meta?: Record<string, any>): void;
124
- info(message: Record<string, any>): void;
125
-
6
+ declare namespace logger {
126
7
  /**
127
- * Log a warning message
128
- * @param message - The message to log
129
- * @param meta - Optional metadata object
8
+ * Log levels supported by NightTimeLogger
130
9
  */
131
- warn(message: string, meta?: Record<string, any>): void;
132
- warn(message: Record<string, any>): void;
10
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'internal';
133
11
 
134
12
  /**
135
- * Log an error message
136
- * @param message - The message to log
137
- * @param meta - Optional metadata object
13
+ * Sampling configuration for log levels
138
14
  */
139
- error(message: string, meta?: Record<string, any>): void;
140
- error(message: Record<string, any>): void;
15
+ export interface SamplingConfig {
16
+ [level: string]: number; // 0.0 to 1.0, where 1.0 = log all, 0.1 = log 10%
17
+ }
141
18
 
142
19
  /**
143
- * Log a debug message
144
- * @param message - The message to log
145
- * @param meta - Optional metadata object
20
+ * Rate limit configuration for a log level
146
21
  */
147
- debug(message: string, meta?: Record<string, any>): void;
148
- debug(message: Record<string, any>): void;
22
+ export interface RateLimitConfig {
23
+ max: number; // Maximum number of logs
24
+ window: number; // Time window in milliseconds
25
+ }
149
26
 
150
27
  /**
151
- * Log a trace message
152
- * @param message - The message to log
153
- * @param meta - Optional metadata object
28
+ * Rate limiting configuration for log levels
154
29
  */
155
- trace(message: string, meta?: Record<string, any>): void;
156
- trace(message: Record<string, any>): void;
30
+ export interface RateLimitConfigMap {
31
+ [level: string]: RateLimitConfig;
32
+ }
157
33
 
158
34
  /**
159
- * Log a fatal message
160
- * @param message - The message to log
161
- * @param meta - Optional metadata object
35
+ * Deduplication configuration
162
36
  */
163
- fatal(message: string, meta?: Record<string, any>): void;
164
- fatal(message: Record<string, any>): void;
165
-
166
- /**
167
- * Log an internal message (logger system messages)
168
- * @param message - The message to log
169
- * @param meta - Optional metadata object
170
- */
171
- internal(message: string, meta?: Record<string, any>): void;
172
- internal(message: Record<string, any>): void;
173
-
174
- /**
175
- * Create a child logger with persistent context
176
- * @param context - Context object to merge into all logs
177
- * @returns Child logger instance
178
- */
179
- child(context: Record<string, any>): Logger;
180
-
181
- /**
182
- * Start a performance timer (development mode only)
183
- * @param label - Timer label
184
- */
185
- time?(label: string): void;
37
+ export interface DeduplicationConfig {
38
+ enabled: boolean;
39
+ threshold: number; // Number of duplicates before squishing (default: 3)
40
+ window: number; // Time window in milliseconds (default: 60000)
41
+ }
186
42
 
187
43
  /**
188
- * End a performance timer and log duration (development mode only)
189
- * @param label - Timer label
190
- * @returns Duration in milliseconds or null if timer not found
44
+ * Plugin configuration
191
45
  */
192
- timeEnd?(label: string): number | null;
46
+ export interface PluginConfig {
47
+ name: string;
48
+ enabled?: boolean;
49
+ config?: Record<string, any>;
50
+ }
193
51
 
194
52
  /**
195
- * Get logging statistics
196
- * @returns Statistics object
53
+ * Logger configuration options
197
54
  */
198
- getStats(): LoggerStats;
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 };
61
+ /** Minimum log level to be logged */
62
+ level?: LogLevel;
63
+ /** Enable console logging (default: true) */
64
+ console?: boolean;
65
+ /** Enable file logging (default: true) */
66
+ file?: boolean;
67
+ /** Filename for combined log file (default: combined.log) */
68
+ filename?: string;
69
+ /** Directory path where log files will be saved (default: './logs') */
70
+ path?: string;
71
+ /** Maximum size of log file in bytes (default: 1048576 = 1MB) */
72
+ maxSize?: number;
73
+ /** Maximum number of log files to retain (default: 5) */
74
+ maxFiles?: number;
75
+ /** Include timestamp in log messages (default: true) */
76
+ timestamp?: boolean;
77
+ /** Skip cache and create new logger instance (default: false) */
78
+ skipCache?: boolean;
79
+ /** Enable call site path reporting (default: false) */
80
+ reportPath?: boolean;
81
+ /** Enable debug mode for logger itself (default: false) */
82
+ debug?: boolean;
83
+ /** Log sampling rates per level */
84
+ sampling?: SamplingConfig;
85
+ /** Rate limiting configuration per level */
86
+ rateLimit?: RateLimitConfigMap;
87
+ /** Log deduplication configuration */
88
+ deduplication?: DeduplicationConfig;
89
+ /** Enable performance metrics (default: NODE_ENV === 'development') */
90
+ performanceMetrics?: boolean;
91
+ /** Interval in milliseconds to log statistics (default: 0 = disabled) */
92
+ statsInterval?: number;
93
+ /** Maximum flush/stream shutdown wait in milliseconds (default: 30000). */
94
+ shutdownTimeout?: number;
95
+ /** Plugin configurations */
96
+ plugins?: PluginConfig[];
97
+ }
199
98
 
200
99
  /**
201
- * Flush all pending logs (returns Promise)
202
- * @returns Promise that resolves when all logs are flushed
100
+ * Statistics object returned by getStats()
203
101
  */
204
- flush(): Promise<void>;
102
+ export interface LoggerStats {
103
+ levels: Record<string, number>;
104
+ hookErrors: number;
105
+ onLog: { pending: number; dropped: number; limit: number };
106
+ sampling: {
107
+ total: Record<string, number>;
108
+ sampled: Record<string, number>;
109
+ rateLimited: Record<string, number>;
110
+ } | null;
111
+ deduplication: {
112
+ totalDeduplicated: number;
113
+ uniqueMessages: number;
114
+ activeEntries: number;
115
+ } | null;
116
+ performance: {
117
+ enabled: boolean;
118
+ avgLogProcessingTime?: number;
119
+ avgTransportTime?: number;
120
+ logProcessingSamples?: number;
121
+ transportSamples?: number;
122
+ } | null;
123
+ }
205
124
 
206
125
  /**
207
- * Close the logger and flush all pending logs (returns Promise)
208
- * @returns Promise that resolves when logger is closed
126
+ * Logger instance interface
209
127
  */
210
- close(): Promise<void>;
211
-
212
- /** Current log level */
213
- level: string;
128
+ export interface Logger {
129
+ /**
130
+ * Log an informational message
131
+ * @param message - The message to log
132
+ * @param meta - Optional metadata object
133
+ */
134
+ info(message: string, meta?: Record<string, any>): void;
135
+ info(message: Record<string, any>): void;
136
+
137
+ /**
138
+ * Log a warning message
139
+ * @param message - The message to log
140
+ * @param meta - Optional metadata object
141
+ */
142
+ warn(message: string, meta?: Record<string, any>): void;
143
+ warn(message: Record<string, any>): void;
144
+
145
+ /**
146
+ * Log an error message
147
+ * @param message - The message to log
148
+ * @param meta - Optional metadata object
149
+ */
150
+ error(message: string, meta?: Record<string, any>): void;
151
+ error(message: Record<string, any>): void;
152
+
153
+ /**
154
+ * Log a debug message
155
+ * @param message - The message to log
156
+ * @param meta - Optional metadata object
157
+ */
158
+ debug(message: string, meta?: Record<string, any>): void;
159
+ debug(message: Record<string, any>): void;
160
+
161
+ /**
162
+ * Log a trace message
163
+ * @param message - The message to log
164
+ * @param meta - Optional metadata object
165
+ */
166
+ trace(message: string, meta?: Record<string, any>): void;
167
+ trace(message: Record<string, any>): void;
168
+
169
+ /**
170
+ * Log a fatal message
171
+ * @param message - The message to log
172
+ * @param meta - Optional metadata object
173
+ */
174
+ fatal(message: string, meta?: Record<string, any>): void;
175
+ fatal(message: Record<string, any>): void;
176
+
177
+ /**
178
+ * Log an internal message (logger system messages)
179
+ * @param message - The message to log
180
+ * @param meta - Optional metadata object
181
+ */
182
+ internal(message: string, meta?: Record<string, any>): void;
183
+ internal(message: Record<string, any>): void;
184
+
185
+ /**
186
+ * Create a child logger with persistent context
187
+ * @param context - Context object to merge into all logs
188
+ * @returns Child logger instance
189
+ */
190
+ child(context: Record<string, any>): Logger;
191
+
192
+ /**
193
+ * Start a performance timer (development mode only)
194
+ * @param label - Timer label
195
+ */
196
+ time?(label: string): void;
197
+
198
+ /**
199
+ * End a performance timer and log duration (development mode only)
200
+ * @param label - Timer label
201
+ * @returns Duration in milliseconds or null if timer not found
202
+ */
203
+ timeEnd?(label: string): number | null;
204
+
205
+ /**
206
+ * Get logging statistics
207
+ * @returns Statistics object
208
+ */
209
+ getStats(): LoggerStats;
210
+ resetStats(): void;
211
+
212
+ /**
213
+ * Flush all pending logs (returns Promise)
214
+ * @returns Promise that resolves when all logs are flushed
215
+ */
216
+ flush(): Promise<void>;
217
+
218
+ /**
219
+ * Close the logger and flush all pending logs (returns Promise)
220
+ * @returns Promise that resolves when logger is closed
221
+ */
222
+ close(): Promise<void>;
223
+
224
+ /** Current log level */
225
+ level: string;
226
+ }
214
227
  }
215
228
 
216
229
  /**
@@ -219,7 +232,13 @@ export interface Logger {
219
232
  * @param config - Configuration options for the logger
220
233
  * @returns Logger instance
221
234
  */
222
- declare function logger(location?: string, config?: LoggerConfig): Logger;
235
+ declare function logger(location?: string, config?: logger.LoggerConfig): logger.Logger;
236
+
237
+ declare namespace logger {
238
+ /** Opt in to process-wide signal handlers; returns an unregister function. */
239
+ function withSecretValues<T>(values: string[], callback: () => T): T;
240
+ function setupSignalHandlers(options?: { timeout?: number }): () => void;
241
+ }
223
242
 
224
243
  export = logger;
225
244
 
package/index.js CHANGED
@@ -114,4 +114,21 @@
114
114
  * Initial release
115
115
  */
116
116
 
117
- module.exports = require('./lib/logger');
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,53 @@
1
+ 'use strict';
2
+
3
+ const delay = () => new Promise(resolve => setTimeout(resolve, 1));
4
+
5
+ function withTimeout(operation, timeout, label) {
6
+ let timer;
7
+ return Promise.race([
8
+ operation,
9
+ new Promise((_, reject) => {
10
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeout}ms`)), timeout);
11
+ }),
12
+ ]).finally(() => clearTimeout(timer));
13
+ }
14
+
15
+ // Winston uses readable-stream, so inspect both its buffers and file sinks.
16
+ function busy(stream) {
17
+ return !!(stream && (stream._writableState?.length || stream._readableState?.length));
18
+ }
19
+
20
+ async function drain(logger, state, timeout = 5000) {
21
+ const deadline = Date.now() + timeout;
22
+ while (state.pending || busy(logger) || logger.transports.some(t =>
23
+ busy(t) || busy(t._stream) || busy(t._dest) || t._opening || t._rotate)) {
24
+ if (state.error) throw state.error;
25
+ if (Date.now() >= deadline) throw new Error('Logger drain timed out');
26
+ await delay();
27
+ }
28
+ if (state.error) throw state.error;
29
+ }
30
+
31
+ // Winston calls close on unpipe but doesn't await the result. Retain that promise.
32
+ function trackClose(transport) {
33
+ if (transport._ntlClose) return;
34
+ const close = transport.close?.bind(transport);
35
+ let promise;
36
+ transport._ntlClose = () => {
37
+ if (!promise) {
38
+ // File._final already ended and drained this stream before unpipe.
39
+ // Calling File.close(callback) on that finished stream never calls back.
40
+ if (transport instanceof require('winston').transports.File && transport._stream?._writableState.finished) {
41
+ return (promise = Promise.resolve());
42
+ }
43
+ promise = close && close.length > 0
44
+ ? new Promise((resolve, reject) => close(err => err ? reject(err) : resolve()))
45
+ : Promise.resolve().then(() => close?.());
46
+ promise.catch(() => {}); // unpipe cannot consume a rejected promise
47
+ }
48
+ return promise;
49
+ };
50
+ transport.close = transport._ntlClose;
51
+ }
52
+
53
+ module.exports = { withTimeout, drain, trackClose };