@zudojs/logger 1.1.0 → 1.2.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 +42 -10
- package/dist/loggerCore/core/loggerCore.core.d.ts +7 -1
- package/dist/loggerCore/core/loggerCore.core.js +18 -4
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.d.ts +9 -0
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js +51 -49
- package/dist/loggerEntry/index.d.ts +1 -0
- package/dist/loggerEntry/index.js +1 -0
- package/dist/loggerEntry/loggerEntry.secretFields.d.ts +19 -0
- package/dist/loggerEntry/loggerEntry.secretFields.js +90 -0
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +11 -7
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +23 -10
- package/dist/loggerErrors/loggerError.helpers.d.ts +11 -0
- package/dist/loggerErrors/loggerError.helpers.js +21 -0
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js +1 -1
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.d.ts +6 -0
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js +43 -8
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.js +1 -1
- package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +57 -10
- package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.d.ts +9 -0
- package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.js +29 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Structured logging with transports, formatters, log levels, secret
|
|
4
4
|
redaction, and context propagation for Zudojs applications.
|
|
5
5
|
|
|
6
|
+
<!-- zudo-docs:start -->
|
|
7
|
+
|
|
8
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-logger](https://zudojs.oyinlola.site/docs/packages-logger) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-logger.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
9
|
+
|
|
10
|
+
<!-- zudo-docs:end -->
|
|
11
|
+
|
|
6
12
|
## Installation
|
|
7
13
|
|
|
8
14
|
```bash
|
|
@@ -39,6 +45,15 @@ Built in: `createConsoleLoggerTransport`, and the composites
|
|
|
39
45
|
`createBufferedLoggerTransport`. File and HTTP transports are not
|
|
40
46
|
included — implement the `LoggerTransport` interface for those.
|
|
41
47
|
|
|
48
|
+
The multi and conditional composites forward `flush()` and `close()` to
|
|
49
|
+
the transports they wrap, so a buffered or file transport nested inside
|
|
50
|
+
is drained and released by the logger's own `flush()`/`close()`. The
|
|
51
|
+
multi transport writes to every sink even when one throws; the failures
|
|
52
|
+
are reported afterwards (several as one `AggregateError`). The buffered
|
|
53
|
+
transport writes each entry independently, so a failing write loses only
|
|
54
|
+
that entry, and a failure from a timer-triggered flush is rethrown by the
|
|
55
|
+
next `flush()` or `close()`.
|
|
56
|
+
|
|
42
57
|
`transportTimeout` (default 10s) bounds every transport write, so a
|
|
43
58
|
transport that stops responding cannot hang `flush()` or `close()`.
|
|
44
59
|
|
|
@@ -48,21 +63,35 @@ log call itself; a failure from an asynchronous transport (or with
|
|
|
48
63
|
`asynchronous: true`) cannot, so it is rethrown by the next `flush()` or
|
|
49
64
|
`close()`, which still flush and close the transports first.
|
|
50
65
|
|
|
66
|
+
`flush()` and `close()` isolate each transport: one that throws does not
|
|
67
|
+
stop the rest from being flushed and closed, and `close()` always leaves
|
|
68
|
+
the logger disposed. The failures are rethrown afterwards (several as one
|
|
69
|
+
`AggregateError`).
|
|
70
|
+
|
|
51
71
|
## Flushing
|
|
52
72
|
|
|
53
73
|
Dispatch completes synchronously when every transport is synchronous.
|
|
54
74
|
With an asynchronous transport — or with `asynchronous: true`, which
|
|
55
75
|
always defers so the caller stays off the transport's critical path —
|
|
56
76
|
writes are in flight until drained. `flush()` and `close()` drain them,
|
|
57
|
-
|
|
77
|
+
including writes started by child loggers (`child()`, `withContext()`),
|
|
78
|
+
so nothing is lost at exit. A child reports itself disposed once its root
|
|
79
|
+
logger is closed.
|
|
58
80
|
|
|
59
81
|
## Secret redaction
|
|
60
82
|
|
|
61
83
|
Redaction is **on by default**. Metadata and context fields whose NAME
|
|
62
|
-
looks like a secret
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
looks like a secret are replaced with `"[REDACTED]"` before the entry
|
|
85
|
+
reaches any formatter or transport. Names are split into words
|
|
86
|
+
(`x-api-key`, `api_key` and `apiKey` all read as `api key`) and matched
|
|
87
|
+
against `DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret,
|
|
88
|
+
token, jwt, bearer, auth, authorization, cookie, session, sid,
|
|
89
|
+
credential, api key, private key, client secret, card number, cvv, ssn,
|
|
90
|
+
pin, otp and more — so `sessionId` and `cardNumber` are redacted while
|
|
91
|
+
`passenger` and `authorId` are not. Nested objects, arrays and getters
|
|
92
|
+
are all covered. Passing `redact.pattern` replaces the word matcher with
|
|
93
|
+
your own RegExp (the old substring default is still exported as
|
|
94
|
+
`DEFAULT_LOGGER_SECRET_PATTERN`).
|
|
66
95
|
|
|
67
96
|
```typescript
|
|
68
97
|
logger.info("login", { user: "alice", password: "hunter2" });
|
|
@@ -74,11 +103,14 @@ createLogger({ redact: { enabled: false } }); // opt out
|
|
|
74
103
|
|
|
75
104
|
## Log injection
|
|
76
105
|
|
|
77
|
-
Text-shaped formatters escape control characters
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
becomes `\n` / `\u001b`
|
|
81
|
-
|
|
106
|
+
Text-shaped formatters escape control characters (C0, DEL, C1 and
|
|
107
|
+
U+2028/U+2029) in the message, the level, the logger name, metadata keys
|
|
108
|
+
and values, context values, source locations and error stacks. A newline
|
|
109
|
+
or ANSI escape inside attacker-supplied text becomes `\n` / `\u001b`
|
|
110
|
+
rather than forging an extra log record or driving the operator's
|
|
111
|
+
terminal. In a stack trace only the frame lines break the line: the
|
|
112
|
+
error's name and message are escaped as one line, and every frame line is
|
|
113
|
+
indented so none can start at column 0 and pass for a record. The JSON formatter relies on
|
|
82
114
|
`JSON.stringify`, which escapes the same characters.
|
|
83
115
|
|
|
84
116
|
Metadata is normalized before serialization, so circular references
|
|
@@ -35,7 +35,13 @@ export declare class ZudojsLogger implements Logger, ZudojsLoggerContext {
|
|
|
35
35
|
private readonly _pending;
|
|
36
36
|
private readonly _dispatchFailures;
|
|
37
37
|
private _droppedFailures;
|
|
38
|
-
|
|
38
|
+
private readonly _root;
|
|
39
|
+
/**
|
|
40
|
+
* @param root - The logger this one was derived from. A child's
|
|
41
|
+
* dispatches are tracked by its root, so the root's flush()/close()
|
|
42
|
+
* drains them, and a child reports itself disposed once its root is.
|
|
43
|
+
*/
|
|
44
|
+
constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage, root?: ZudojsLogger);
|
|
39
45
|
get configuration(): LoggerConfiguration;
|
|
40
46
|
get contextStorage(): LoggerContextStorage;
|
|
41
47
|
get name(): string;
|
|
@@ -20,7 +20,14 @@ export class ZudojsLogger {
|
|
|
20
20
|
_pending = new Set();
|
|
21
21
|
_dispatchFailures = [];
|
|
22
22
|
_droppedFailures = 0;
|
|
23
|
-
|
|
23
|
+
_root;
|
|
24
|
+
/**
|
|
25
|
+
* @param root - The logger this one was derived from. A child's
|
|
26
|
+
* dispatches are tracked by its root, so the root's flush()/close()
|
|
27
|
+
* drains them, and a child reports itself disposed once its root is.
|
|
28
|
+
*/
|
|
29
|
+
constructor(options = {}, contextStorage, root) {
|
|
30
|
+
this._root = root;
|
|
24
31
|
this._configuration = resolveLoggerOptions(options);
|
|
25
32
|
this._contextStorage = contextStorage ?? createLoggerContextStorage();
|
|
26
33
|
this._configuration = normalizeConfiguration(this._configuration);
|
|
@@ -90,7 +97,7 @@ export class ZudojsLogger {
|
|
|
90
97
|
return this._closing;
|
|
91
98
|
}
|
|
92
99
|
assertActive() {
|
|
93
|
-
assertActiveHelper(this.
|
|
100
|
+
assertActiveHelper(this.isDisposed(), this._configuration.name);
|
|
94
101
|
}
|
|
95
102
|
assertMutable() {
|
|
96
103
|
assertMutableHelper(this._configuration.mutable);
|
|
@@ -99,7 +106,7 @@ export class ZudojsLogger {
|
|
|
99
106
|
handleInfrastructureErrorHelper(this._configuration.throwTransportErrors, error);
|
|
100
107
|
}
|
|
101
108
|
isDisposed() {
|
|
102
|
-
return this._disposed;
|
|
109
|
+
return this._disposed || (this._root?.isDisposed() ?? false);
|
|
103
110
|
}
|
|
104
111
|
markDisposed() {
|
|
105
112
|
this._disposed = true;
|
|
@@ -108,9 +115,13 @@ export class ZudojsLogger {
|
|
|
108
115
|
this._configuration = Object.freeze(config);
|
|
109
116
|
}
|
|
110
117
|
createChildLogger(options) {
|
|
111
|
-
return new ZudojsLogger(options, this._contextStorage);
|
|
118
|
+
return new ZudojsLogger(options, this._contextStorage, this._root ?? this);
|
|
112
119
|
}
|
|
113
120
|
trackDispatch(dispatch) {
|
|
121
|
+
if (this._root) {
|
|
122
|
+
this._root.trackDispatch(dispatch);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
114
125
|
// A dispatch rejects only when handleError threw — i.e. when
|
|
115
126
|
// `throwTransportErrors` is on and an asynchronous transport failed.
|
|
116
127
|
// Nothing can throw from the log call that started it, so the failure
|
|
@@ -134,6 +145,9 @@ export class ZudojsLogger {
|
|
|
134
145
|
this._pending.add(tracked);
|
|
135
146
|
}
|
|
136
147
|
async drainDispatches() {
|
|
148
|
+
if (this._root) {
|
|
149
|
+
return this._root.drainDispatches();
|
|
150
|
+
}
|
|
137
151
|
// A dispatch can start further dispatches (a transport that logs),
|
|
138
152
|
// so drain until the set is genuinely empty.
|
|
139
153
|
while (this._pending.size > 0) {
|
|
@@ -17,10 +17,19 @@ export declare function enableLogger(ctx: ZudojsLoggerContext): void;
|
|
|
17
17
|
export declare function disableLogger(ctx: ZudojsLoggerContext): void;
|
|
18
18
|
/**
|
|
19
19
|
* Flushes all transport buffers.
|
|
20
|
+
*
|
|
21
|
+
* In-flight dispatches land in the transports before they are flushed.
|
|
22
|
+
* Every transport is flushed even when a dispatch or another transport
|
|
23
|
+
* failed; the failures are rethrown afterwards (several as one
|
|
24
|
+
* AggregateError).
|
|
20
25
|
*/
|
|
21
26
|
export declare function flushLogger(ctx: ZudojsLoggerContext): Promise<void>;
|
|
22
27
|
/**
|
|
23
28
|
* Closes all transports and marks logger as disposed.
|
|
29
|
+
*
|
|
30
|
+
* Closing is terminal: every transport is flushed and closed and the
|
|
31
|
+
* logger is marked disposed even when a dispatch or a transport failed;
|
|
32
|
+
* the failures are rethrown afterwards.
|
|
24
33
|
*/
|
|
25
34
|
export declare function closeLogger(ctx: ZudojsLoggerContext): Promise<void>;
|
|
26
35
|
//# sourceMappingURL=loggerCoreMethods.lifecycle.d.ts.map
|
|
@@ -5,6 +5,7 @@ import { LoggerLevel } from "../../../loggerLevel/loggerLevel.type.js";
|
|
|
5
5
|
import { createLoggerTransport } from "../../../loggerTransport/loggerTransport.core.js";
|
|
6
6
|
import { isLoggerTransport } from "../../../loggerTransport/loggerTransportGuard.js";
|
|
7
7
|
import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
|
|
8
|
+
import { throwCollectedFailures } from "../../../loggerErrors/loggerError.helpers.js";
|
|
8
9
|
/**
|
|
9
10
|
* Sets the logger level.
|
|
10
11
|
*/
|
|
@@ -44,72 +45,73 @@ export function disableLogger(ctx) {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
* Flushes
|
|
48
|
+
* Flushes (and optionally closes) every configured transport, isolating
|
|
49
|
+
* each one: a failing sink is collected and the walk continues, so one
|
|
50
|
+
* bad transport cannot leave every later transport unflushed/unclosed.
|
|
48
51
|
*/
|
|
49
|
-
|
|
50
|
-
ctx.assertActive();
|
|
51
|
-
// In-flight dispatches must land in the transports before those
|
|
52
|
-
// transports are asked to flush, otherwise flush() is a no-op for
|
|
53
|
-
// everything logged in the same tick. A dispatch failure (surfaced
|
|
54
|
-
// when `throwTransportErrors` is on) is rethrown only after the
|
|
55
|
-
// transports have still been flushed.
|
|
56
|
-
let failure;
|
|
57
|
-
let failed = false;
|
|
58
|
-
try {
|
|
59
|
-
await ctx.drainDispatches();
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
failure = error;
|
|
63
|
-
failed = true;
|
|
64
|
-
}
|
|
52
|
+
async function settleTransports(ctx, close, failures) {
|
|
65
53
|
for (const transport of ctx.configuration.transports) {
|
|
66
|
-
if (!isLoggerTransport(transport))
|
|
54
|
+
if (!isLoggerTransport(transport))
|
|
67
55
|
continue;
|
|
68
|
-
}
|
|
69
56
|
const registered = createLoggerTransport(transport);
|
|
70
|
-
if (!registered.enabled)
|
|
57
|
+
if (!close && !registered.enabled)
|
|
71
58
|
continue;
|
|
59
|
+
const steps = close
|
|
60
|
+
? [() => registered.flush?.(), () => registered.close?.()]
|
|
61
|
+
: [() => registered.flush?.()];
|
|
62
|
+
for (const step of steps) {
|
|
63
|
+
try {
|
|
64
|
+
await step();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
failures.push(error);
|
|
68
|
+
}
|
|
72
69
|
}
|
|
73
|
-
if (registered.flush) {
|
|
74
|
-
await registered.flush();
|
|
75
|
-
}
|
|
76
70
|
}
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
}
|
|
72
|
+
/** Drains in-flight dispatches, collecting (not throwing) a failure. */
|
|
73
|
+
async function drainInto(ctx, failures) {
|
|
74
|
+
try {
|
|
75
|
+
await ctx.drainDispatches();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
failures.push(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Flushes all transport buffers.
|
|
83
|
+
*
|
|
84
|
+
* In-flight dispatches land in the transports before they are flushed.
|
|
85
|
+
* Every transport is flushed even when a dispatch or another transport
|
|
86
|
+
* failed; the failures are rethrown afterwards (several as one
|
|
87
|
+
* AggregateError).
|
|
88
|
+
*/
|
|
89
|
+
export async function flushLogger(ctx) {
|
|
90
|
+
ctx.assertActive();
|
|
91
|
+
const failures = [];
|
|
92
|
+
await drainInto(ctx, failures);
|
|
93
|
+
await settleTransports(ctx, false, failures);
|
|
94
|
+
throwCollectedFailures(failures, "Logger flush failed.");
|
|
79
95
|
}
|
|
80
96
|
/**
|
|
81
97
|
* Closes all transports and marks logger as disposed.
|
|
98
|
+
*
|
|
99
|
+
* Closing is terminal: every transport is flushed and closed and the
|
|
100
|
+
* logger is marked disposed even when a dispatch or a transport failed;
|
|
101
|
+
* the failures are rethrown afterwards.
|
|
82
102
|
*/
|
|
83
103
|
export async function closeLogger(ctx) {
|
|
84
104
|
if (ctx.isDisposed()) {
|
|
85
105
|
return;
|
|
86
106
|
}
|
|
87
|
-
|
|
88
|
-
// is marked disposed even when a dispatch failed; the failure is rethrown
|
|
89
|
-
// afterwards.
|
|
90
|
-
let failure;
|
|
91
|
-
let failed = false;
|
|
107
|
+
const failures = [];
|
|
92
108
|
try {
|
|
93
|
-
await ctx
|
|
109
|
+
await drainInto(ctx, failures);
|
|
110
|
+
await settleTransports(ctx, true, failures);
|
|
94
111
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
failed = true;
|
|
98
|
-
}
|
|
99
|
-
for (const transport of ctx.configuration.transports) {
|
|
100
|
-
if (!isLoggerTransport(transport)) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const registered = createLoggerTransport(transport);
|
|
104
|
-
if (registered.flush) {
|
|
105
|
-
await registered.flush();
|
|
106
|
-
}
|
|
107
|
-
if (registered.close) {
|
|
108
|
-
await registered.close();
|
|
109
|
-
}
|
|
112
|
+
finally {
|
|
113
|
+
ctx.markDisposed();
|
|
110
114
|
}
|
|
111
|
-
|
|
112
|
-
if (failed)
|
|
113
|
-
throw failure;
|
|
115
|
+
throwCollectedFailures(failures, "Logger close failed.");
|
|
114
116
|
}
|
|
115
117
|
//# sourceMappingURL=loggerCoreMethods.lifecycle.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default secret-field matching for logger redaction.
|
|
3
|
+
*
|
|
4
|
+
* Field names are split into words (`x-api-key`, `api_key` and `apiKey`
|
|
5
|
+
* all become `api key`) and matched on whole-word runs, the same scheme
|
|
6
|
+
* @zudojs/observability's `isSensitiveField` uses. A raw substring test
|
|
7
|
+
* redacted `passenger`, `compass` and `bypassCache` (they contain `pass`)
|
|
8
|
+
* while missing `auth`, `sessionId`, `sid`, `jwt`, `ssn`, `cardNumber`,
|
|
9
|
+
* `cvv` and `otp` entirely.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Secret field names matched on whole words by default.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_LOGGER_SECRET_FIELDS: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Builds the default secret-field predicate from a word list.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createDefaultSecretFieldMatcher(fields?: readonly string[]): (key: string) => boolean;
|
|
19
|
+
//# sourceMappingURL=loggerEntry.secretFields.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default secret-field matching for logger redaction.
|
|
3
|
+
*
|
|
4
|
+
* Field names are split into words (`x-api-key`, `api_key` and `apiKey`
|
|
5
|
+
* all become `api key`) and matched on whole-word runs, the same scheme
|
|
6
|
+
* @zudojs/observability's `isSensitiveField` uses. A raw substring test
|
|
7
|
+
* redacted `passenger`, `compass` and `bypassCache` (they contain `pass`)
|
|
8
|
+
* while missing `auth`, `sessionId`, `sid`, `jwt`, `ssn`, `cardNumber`,
|
|
9
|
+
* `cvv` and `otp` entirely.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Secret field names matched on whole words by default.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_LOGGER_SECRET_FIELDS = Object.freeze([
|
|
15
|
+
"password",
|
|
16
|
+
"passwd",
|
|
17
|
+
"passphrase",
|
|
18
|
+
"pwd",
|
|
19
|
+
"secret",
|
|
20
|
+
"token",
|
|
21
|
+
"jwt",
|
|
22
|
+
"bearer",
|
|
23
|
+
"authorization",
|
|
24
|
+
"auth",
|
|
25
|
+
"cookie",
|
|
26
|
+
"session",
|
|
27
|
+
"sid",
|
|
28
|
+
"credential",
|
|
29
|
+
"credentials",
|
|
30
|
+
"api_key",
|
|
31
|
+
"private_key",
|
|
32
|
+
"client_secret",
|
|
33
|
+
"credit_card",
|
|
34
|
+
"card_number",
|
|
35
|
+
"cvv",
|
|
36
|
+
"cvc",
|
|
37
|
+
"ssn",
|
|
38
|
+
"social_security",
|
|
39
|
+
"pin",
|
|
40
|
+
"otp",
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Unambiguous secret words that are also matched as a substring of a
|
|
44
|
+
* lowercase run-together name (`userpassword`, `accesstoken`), which the
|
|
45
|
+
* word split alone cannot see.
|
|
46
|
+
*/
|
|
47
|
+
const SUBSTRING_SECRET_WORDS = Object.freeze([
|
|
48
|
+
"password",
|
|
49
|
+
"passwd",
|
|
50
|
+
"secret",
|
|
51
|
+
"token",
|
|
52
|
+
"apikey",
|
|
53
|
+
"privatekey",
|
|
54
|
+
"credential",
|
|
55
|
+
"authorization",
|
|
56
|
+
"cookie",
|
|
57
|
+
]);
|
|
58
|
+
/** Splits a field name into lowercase words. */
|
|
59
|
+
function splitWords(key) {
|
|
60
|
+
return key
|
|
61
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
|
|
62
|
+
.replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2")
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.split(/[^a-z0-9]+/u)
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
/** Normalizes a field-list entry the way word runs are joined. */
|
|
68
|
+
function normalizeField(field) {
|
|
69
|
+
return field.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Builds the default secret-field predicate from a word list.
|
|
73
|
+
*/
|
|
74
|
+
export function createDefaultSecretFieldMatcher(fields = DEFAULT_LOGGER_SECRET_FIELDS) {
|
|
75
|
+
const targets = new Set(fields.map(normalizeField).filter(Boolean));
|
|
76
|
+
return (key) => {
|
|
77
|
+
const words = splitWords(key);
|
|
78
|
+
for (let start = 0; start < words.length; start += 1) {
|
|
79
|
+
let joined = "";
|
|
80
|
+
for (let end = start; end < words.length; end += 1) {
|
|
81
|
+
joined += words[end] ?? "";
|
|
82
|
+
if (targets.has(joined))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const compact = key.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
87
|
+
return SUBSTRING_SECRET_WORDS.some((word) => compact.includes(word));
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=loggerEntry.secretFields.js.map
|
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const LOGGER_REDACTION_TOKEN = "[REDACTED]";
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Legacy substring pattern for secret field names.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
18
|
+
* @deprecated No longer the default: it redacted `passenger`/`compass`
|
|
19
|
+
* and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
|
|
20
|
+
* The default is now the word-based matcher over
|
|
21
|
+
* `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
|
|
22
|
+
* back into the old behaviour.
|
|
20
23
|
*/
|
|
21
24
|
export declare const DEFAULT_LOGGER_SECRET_PATTERN: RegExp;
|
|
22
25
|
/**
|
|
@@ -32,8 +35,9 @@ export interface LoggerRedactionOptions {
|
|
|
32
35
|
*/
|
|
33
36
|
readonly keys?: readonly string[];
|
|
34
37
|
/**
|
|
35
|
-
* Field-name pattern.
|
|
36
|
-
* Pass a pattern that never
|
|
38
|
+
* Field-name pattern. When omitted, the word-based default matcher over
|
|
39
|
+
* `DEFAULT_LOGGER_SECRET_FIELDS` is used. Pass a pattern that never
|
|
40
|
+
* matches to rely on `keys` alone.
|
|
37
41
|
*/
|
|
38
42
|
readonly pattern?: RegExp;
|
|
39
43
|
/**
|
|
@@ -45,8 +49,8 @@ export interface LoggerRedactionOptions {
|
|
|
45
49
|
* Escapes control characters that could forge log records.
|
|
46
50
|
*
|
|
47
51
|
* CR, LF, TAB and the ANSI escape byte become printable escapes; every
|
|
48
|
-
* other C0 control character and DEL becomes `\xNN
|
|
49
|
-
*
|
|
52
|
+
* other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
|
|
53
|
+
* U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
|
|
50
54
|
*/
|
|
51
55
|
export declare function escapeLogText(value: string): string;
|
|
52
56
|
/**
|
|
@@ -8,28 +8,33 @@
|
|
|
8
8
|
* terminal. Every string that reaches a text-shaped formatter is
|
|
9
9
|
* therefore escaped here first.
|
|
10
10
|
*/
|
|
11
|
+
import { createDefaultSecretFieldMatcher } from "../loggerEntry.secretFields.js";
|
|
11
12
|
/**
|
|
12
|
-
* Matches C0
|
|
13
|
-
*
|
|
13
|
+
* Matches C0 controls, DEL, C1 controls (NEL, CSI) and the Unicode
|
|
14
|
+
* line/paragraph separators — everything that can forge a record
|
|
15
|
+
* boundary or drive a terminal.
|
|
14
16
|
*/
|
|
15
|
-
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
|
|
17
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
16
18
|
/**
|
|
17
19
|
* Replacement token written in place of a redacted value.
|
|
18
20
|
*/
|
|
19
21
|
export const LOGGER_REDACTION_TOKEN = "[REDACTED]";
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* Legacy substring pattern for secret field names.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
25
|
+
* @deprecated No longer the default: it redacted `passenger`/`compass`
|
|
26
|
+
* and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
|
|
27
|
+
* The default is now the word-based matcher over
|
|
28
|
+
* `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
|
|
29
|
+
* back into the old behaviour.
|
|
25
30
|
*/
|
|
26
31
|
export const DEFAULT_LOGGER_SECRET_PATTERN = /(pass(word|wd)?|secret|token|api[-_.]?key|private[-_.]?key|credential|authorization|cookie)/i;
|
|
27
32
|
/**
|
|
28
33
|
* Escapes control characters that could forge log records.
|
|
29
34
|
*
|
|
30
35
|
* CR, LF, TAB and the ANSI escape byte become printable escapes; every
|
|
31
|
-
* other C0 control character and DEL becomes `\xNN
|
|
32
|
-
*
|
|
36
|
+
* other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
|
|
37
|
+
* U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
|
|
33
38
|
*/
|
|
34
39
|
export function escapeLogText(value) {
|
|
35
40
|
return value.replace(CONTROL_CHARACTERS, (character) => {
|
|
@@ -42,6 +47,10 @@ export function escapeLogText(value) {
|
|
|
42
47
|
return "\\t";
|
|
43
48
|
case "\u001b":
|
|
44
49
|
return "\\u001b";
|
|
50
|
+
case "\u2028":
|
|
51
|
+
return "\\u2028";
|
|
52
|
+
case "\u2029":
|
|
53
|
+
return "\\u2029";
|
|
45
54
|
default: {
|
|
46
55
|
const code = character.charCodeAt(0);
|
|
47
56
|
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
@@ -62,14 +71,18 @@ export function createSecretMatcher(options = {}) {
|
|
|
62
71
|
if (options.enabled === false) {
|
|
63
72
|
return () => false;
|
|
64
73
|
}
|
|
74
|
+
const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
|
|
75
|
+
const configured = options.pattern;
|
|
76
|
+
if (configured === undefined) {
|
|
77
|
+
const isDefaultSecret = createDefaultSecretFieldMatcher();
|
|
78
|
+
return (key) => exact.has(key.toLowerCase()) || isDefaultSecret(key);
|
|
79
|
+
}
|
|
65
80
|
// Copy the pattern without `g`/`y`: those flags make `test()` advance
|
|
66
81
|
// `lastIndex`, so a shared pattern would match a secret-named field on
|
|
67
82
|
// one entry and let it through unredacted on the next.
|
|
68
|
-
const configured = options.pattern ?? DEFAULT_LOGGER_SECRET_PATTERN;
|
|
69
83
|
const pattern = configured.global || configured.sticky
|
|
70
84
|
? new RegExp(configured.source, configured.flags.replace(/[gy]/gu, ""))
|
|
71
85
|
: configured;
|
|
72
|
-
const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
|
|
73
86
|
return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
|
|
74
87
|
}
|
|
75
88
|
/**
|
|
@@ -12,4 +12,15 @@ export declare function getLoggerErrorCause(error: unknown): unknown;
|
|
|
12
12
|
export declare function createLoggerTransportError(transportName: string, error: unknown): LoggerTransportError;
|
|
13
13
|
/** Creates a formatter error while preserving the original failure. */
|
|
14
14
|
export declare function createLoggerFormatterError(formatterName: string, error: unknown): LoggerFormatterError;
|
|
15
|
+
/**
|
|
16
|
+
* Rethrows collected failures: the single failure itself, or an
|
|
17
|
+
* AggregateError when more than one step failed. Does nothing when the
|
|
18
|
+
* list is empty.
|
|
19
|
+
*/
|
|
20
|
+
export declare function throwCollectedFailures(failures: readonly unknown[], message: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Runs every step even when earlier ones fail, then rethrows the
|
|
23
|
+
* collected failures via {@link throwCollectedFailures}.
|
|
24
|
+
*/
|
|
25
|
+
export declare function settleAllOrThrow(steps: readonly (() => unknown)[], message: string): Promise<void>;
|
|
15
26
|
//# sourceMappingURL=loggerError.helpers.d.ts.map
|
|
@@ -38,4 +38,25 @@ export function createLoggerFormatterError(formatterName, error) {
|
|
|
38
38
|
const message = error instanceof Error ? error.message : String(error);
|
|
39
39
|
return new LoggerFormatterError(`Logger formatter "${formatterName}" failed: ${message}`, { formatterName, cause });
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Rethrows collected failures: the single failure itself, or an
|
|
43
|
+
* AggregateError when more than one step failed. Does nothing when the
|
|
44
|
+
* list is empty.
|
|
45
|
+
*/
|
|
46
|
+
export function throwCollectedFailures(failures, message) {
|
|
47
|
+
if (failures.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
if (failures.length === 1)
|
|
50
|
+
throw failures[0];
|
|
51
|
+
throw new AggregateError(failures, message);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Runs every step even when earlier ones fail, then rethrows the
|
|
55
|
+
* collected failures via {@link throwCollectedFailures}.
|
|
56
|
+
*/
|
|
57
|
+
export async function settleAllOrThrow(steps, message) {
|
|
58
|
+
const results = await Promise.allSettled(steps.map(async (step) => step()));
|
|
59
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
60
|
+
throwCollectedFailures(failures, message);
|
|
61
|
+
}
|
|
41
62
|
//# sourceMappingURL=loggerError.helpers.js.map
|
|
@@ -11,7 +11,7 @@ import { createTextLoggerFormatter } from "./loggerFormatterFormatters.text.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export function createCompactLoggerFormatter(options = {}) {
|
|
13
13
|
return createLoggerFormatter((entry) => {
|
|
14
|
-
const level = entry.levelName.toUpperCase();
|
|
14
|
+
const level = escapeLogText(entry.levelName.toUpperCase());
|
|
15
15
|
const logger = entry.logger ? ` ${escapeLogText(entry.logger)}:` : "";
|
|
16
16
|
return `${level}${logger} ${escapeLogText(entry.message)}`;
|
|
17
17
|
}, {
|
package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare function formatMetadata(metadata: Record<string, unknown>, separa
|
|
|
11
11
|
export declare function formatValue(value: unknown): string;
|
|
12
12
|
/**
|
|
13
13
|
* Formats an Error.
|
|
14
|
+
*
|
|
15
|
+
* With a stack trace the output is intentionally multi-line, but only
|
|
16
|
+
* the frame lines break the line: the header (which carries the
|
|
17
|
+
* attacker-influenceable message) is escaped as a single line, and every
|
|
18
|
+
* frame line is escaped and indented so none of them can start at
|
|
19
|
+
* column 0 and parse as a record of its own.
|
|
14
20
|
*/
|
|
15
21
|
export declare function formatError(error: Error, includeStackTrace: boolean): string;
|
|
16
22
|
//# sourceMappingURL=loggerFormatterFormatters.metadata.d.ts.map
|
package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js
CHANGED
|
@@ -23,28 +23,63 @@ export function formatValue(value) {
|
|
|
23
23
|
}
|
|
24
24
|
if (typeof value === "string") {
|
|
25
25
|
if (/\s/.test(value)) {
|
|
26
|
-
|
|
26
|
+
// JSON escapes C0 controls but not DEL, C1 or U+2028/U+2029.
|
|
27
|
+
return escapeLogText(JSON.stringify(value));
|
|
27
28
|
}
|
|
28
29
|
// A value with no whitespace can still carry ANSI escapes or other
|
|
29
30
|
// C0 controls, which used to reach the sink verbatim.
|
|
30
31
|
return escapeLogText(value);
|
|
31
32
|
}
|
|
32
33
|
if (typeof value === "object") {
|
|
33
|
-
return JSON.stringify(serializeLoggerValue(value));
|
|
34
|
+
return escapeLogText(JSON.stringify(serializeLoggerValue(value)));
|
|
34
35
|
}
|
|
35
36
|
return escapeLogText(String(value));
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Splits an error's stack into its header (name and message) and its
|
|
40
|
+
* frame lines.
|
|
41
|
+
*
|
|
42
|
+
* The header is derived from the error itself rather than from the first
|
|
43
|
+
* stack line, because the message can contain newlines of its own and
|
|
44
|
+
* would otherwise spill across several "lines" of the stack.
|
|
45
|
+
*/
|
|
46
|
+
function splitStack(error, stack) {
|
|
47
|
+
const message = String(error.message ?? "");
|
|
48
|
+
const header = message ? `${error.name}: ${message}` : String(error.name);
|
|
49
|
+
if (stack.startsWith(header)) {
|
|
50
|
+
const rest = stack.slice(header.length).replace(/^\r?\n/u, "");
|
|
51
|
+
return { header, frames: rest ? rest.split("\n") : [] };
|
|
52
|
+
}
|
|
53
|
+
const lines = stack.split("\n");
|
|
54
|
+
const firstFrame = lines.findIndex((line) => /^\s+at\s/u.test(line));
|
|
55
|
+
if (firstFrame === -1) {
|
|
56
|
+
return { header: stack, frames: [] };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
header: lines.slice(0, firstFrame).join("\n"),
|
|
60
|
+
frames: lines.slice(firstFrame),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
37
63
|
/**
|
|
38
64
|
* Formats an Error.
|
|
65
|
+
*
|
|
66
|
+
* With a stack trace the output is intentionally multi-line, but only
|
|
67
|
+
* the frame lines break the line: the header (which carries the
|
|
68
|
+
* attacker-influenceable message) is escaped as a single line, and every
|
|
69
|
+
* frame line is escaped and indented so none of them can start at
|
|
70
|
+
* column 0 and parse as a record of its own.
|
|
39
71
|
*/
|
|
40
72
|
export function formatError(error, includeStackTrace) {
|
|
41
|
-
if (includeStackTrace && error.stack) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
if (includeStackTrace && typeof error.stack === "string" && error.stack) {
|
|
74
|
+
const { header, frames } = splitStack(error, error.stack);
|
|
75
|
+
const lines = [escapeLogText(header)];
|
|
76
|
+
for (const frame of frames) {
|
|
77
|
+
const escaped = escapeLogText(frame);
|
|
78
|
+
lines.push(/^\s/u.test(escaped) ? escaped : ` ${escaped}`);
|
|
79
|
+
}
|
|
80
|
+
return `\n${lines.join("\n")}`;
|
|
46
81
|
}
|
|
47
82
|
const serialized = serializeLoggerError(error);
|
|
48
|
-
return `error=${JSON.stringify(serialized)}`;
|
|
83
|
+
return `error=${escapeLogText(JSON.stringify(serialized))}`;
|
|
49
84
|
}
|
|
50
85
|
//# sourceMappingURL=loggerFormatterFormatters.metadata.js.map
|
|
@@ -43,7 +43,7 @@ export function createTextLoggerFormatter(options = {}) {
|
|
|
43
43
|
// output. Colour codes are emitted only on explicit opt-in, and
|
|
44
44
|
// only around the fixed level name — never around user text,
|
|
45
45
|
// which stays escaped.
|
|
46
|
-
const levelTag = `[${entry.levelName.toUpperCase()}]`;
|
|
46
|
+
const levelTag = `[${escapeLogText(entry.levelName.toUpperCase())}]`;
|
|
47
47
|
parts.push(context.colors ? colorizeLevel(entry.levelName, levelTag) : levelTag);
|
|
48
48
|
if (includeLogger && entry.logger) {
|
|
49
49
|
parts.push(`[${escapeLogText(entry.logger)}]`);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Buffered logger transport.
|
|
3
3
|
*/
|
|
4
4
|
import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
|
|
5
|
-
import {
|
|
5
|
+
import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
|
|
6
|
+
import { throwCollectedFailures } from "../../loggerErrors/loggerError.helpers.js";
|
|
6
7
|
/**
|
|
7
8
|
* Creates a transport that buffers entries before forwarding
|
|
8
9
|
* them to another transport.
|
|
@@ -12,14 +13,50 @@ export function createBufferedLoggerTransport(transport, options = {}) {
|
|
|
12
13
|
const maxSize = options.maxSize ?? 100;
|
|
13
14
|
const flushInterval = options.flushInterval ?? 0;
|
|
14
15
|
let timer;
|
|
15
|
-
|
|
16
|
+
let deferredFailure;
|
|
17
|
+
// Each entry is written on its own: one failing write used to abort the
|
|
18
|
+
// loop after the whole batch had already been spliced out, losing every
|
|
19
|
+
// entry behind it. Only the entries that actually failed are dropped.
|
|
20
|
+
const drain = async () => {
|
|
16
21
|
if (buffer.length === 0) {
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
const entries = buffer.splice(0, buffer.length);
|
|
25
|
+
const failures = [];
|
|
20
26
|
for (const entry of entries) {
|
|
21
|
-
|
|
27
|
+
try {
|
|
28
|
+
await writeLoggerTransport(transport, entry);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
failures.push(error);
|
|
32
|
+
}
|
|
22
33
|
}
|
|
34
|
+
throwCollectedFailures(failures, `${failures.length} buffered log entries failed to write.`);
|
|
35
|
+
};
|
|
36
|
+
// A failure from a timer-triggered drain has no caller to reach, so it
|
|
37
|
+
// is kept and rethrown by the next explicit flush()/close().
|
|
38
|
+
const takeDeferredFailure = () => {
|
|
39
|
+
if (!deferredFailure)
|
|
40
|
+
return [];
|
|
41
|
+
const { error } = deferredFailure;
|
|
42
|
+
deferredFailure = undefined;
|
|
43
|
+
return [error];
|
|
44
|
+
};
|
|
45
|
+
const flush = async () => {
|
|
46
|
+
const failures = takeDeferredFailure();
|
|
47
|
+
try {
|
|
48
|
+
await drain();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
failures.push(error);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await flushLoggerTransport(transport);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
failures.push(error);
|
|
58
|
+
}
|
|
59
|
+
throwCollectedFailures(failures, "Buffered logger transport flush failed.");
|
|
23
60
|
};
|
|
24
61
|
const scheduleFlush = () => {
|
|
25
62
|
if (flushInterval <= 0 || timer) {
|
|
@@ -28,10 +65,10 @@ export function createBufferedLoggerTransport(transport, options = {}) {
|
|
|
28
65
|
timer = setTimeout(async () => {
|
|
29
66
|
timer = undefined;
|
|
30
67
|
try {
|
|
31
|
-
await
|
|
68
|
+
await drain();
|
|
32
69
|
}
|
|
33
|
-
catch {
|
|
34
|
-
|
|
70
|
+
catch (error) {
|
|
71
|
+
deferredFailure ??= { error };
|
|
35
72
|
}
|
|
36
73
|
}, flushInterval);
|
|
37
74
|
// A pending flush is housekeeping, not work: left referenced it kept a
|
|
@@ -45,7 +82,7 @@ export function createBufferedLoggerTransport(transport, options = {}) {
|
|
|
45
82
|
async write(entry) {
|
|
46
83
|
buffer.push(entry);
|
|
47
84
|
if (buffer.length >= maxSize) {
|
|
48
|
-
await
|
|
85
|
+
await drain();
|
|
49
86
|
}
|
|
50
87
|
else {
|
|
51
88
|
scheduleFlush();
|
|
@@ -57,10 +94,20 @@ export function createBufferedLoggerTransport(transport, options = {}) {
|
|
|
57
94
|
clearTimeout(timer);
|
|
58
95
|
timer = undefined;
|
|
59
96
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await
|
|
97
|
+
const failures = [];
|
|
98
|
+
try {
|
|
99
|
+
await flush();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
failures.push(error);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await closeLoggerTransport(transport);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
failures.push(error);
|
|
63
109
|
}
|
|
110
|
+
throwCollectedFailures(failures, "Buffered logger transport close failed.");
|
|
64
111
|
},
|
|
65
112
|
};
|
|
66
113
|
return createLoggerTransport(buffered, options);
|
|
@@ -7,11 +7,20 @@ import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffer
|
|
|
7
7
|
/**
|
|
8
8
|
* Creates a transport that forwards entries to another
|
|
9
9
|
* transport only when a predicate passes.
|
|
10
|
+
*
|
|
11
|
+
* `flush()` and `close()` are forwarded to the inner transport, so a
|
|
12
|
+
* buffered or file transport nested inside is drained and released by
|
|
13
|
+
* the logger's own `flush()`/`close()`.
|
|
10
14
|
*/
|
|
11
15
|
export declare function createConditionalLoggerTransport(transport: LoggerTransportLike, predicate: (entry: LoggerEntry) => boolean | Promise<boolean>, options?: LoggerTransportOptions): RegisteredLoggerTransport;
|
|
12
16
|
/**
|
|
13
17
|
* Creates a transport that forwards entries to multiple
|
|
14
18
|
* transports.
|
|
19
|
+
*
|
|
20
|
+
* Every sink receives every entry even when another sink throws: writes
|
|
21
|
+
* are settled independently and the failures are rethrown afterwards
|
|
22
|
+
* (one failure as itself, several as an AggregateError). `flush()` and
|
|
23
|
+
* `close()` fan out to every inner transport the same way.
|
|
15
24
|
*/
|
|
16
25
|
export declare function createMultiLoggerTransport(transports: readonly LoggerTransportLike[], options?: LoggerTransportOptions): RegisteredLoggerTransport;
|
|
17
26
|
export { createBufferedLoggerTransport };
|
|
@@ -2,27 +2,48 @@
|
|
|
2
2
|
* Composite logger transports.
|
|
3
3
|
*/
|
|
4
4
|
import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
|
|
5
|
+
import { createLoggerTransportId } from "../loggerTransportGuard.js";
|
|
6
|
+
import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
|
|
7
|
+
import { settleAllOrThrow } from "../../loggerErrors/loggerError.helpers.js";
|
|
5
8
|
import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffered.js";
|
|
6
9
|
/**
|
|
7
10
|
* Creates a transport that forwards entries to another
|
|
8
11
|
* transport only when a predicate passes.
|
|
12
|
+
*
|
|
13
|
+
* `flush()` and `close()` are forwarded to the inner transport, so a
|
|
14
|
+
* buffered or file transport nested inside is drained and released by
|
|
15
|
+
* the logger's own `flush()`/`close()`.
|
|
9
16
|
*/
|
|
10
17
|
export function createConditionalLoggerTransport(transport, predicate, options = {}) {
|
|
11
|
-
return createLoggerTransport(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
return createLoggerTransport({
|
|
19
|
+
name: options.name ?? createLoggerTransportId(),
|
|
20
|
+
enabled: options.enabled ?? true,
|
|
21
|
+
async write(entry, context) {
|
|
22
|
+
if (await predicate(entry)) {
|
|
23
|
+
await writeLoggerTransport(transport, entry, context);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
flush: () => flushLoggerTransport(transport),
|
|
27
|
+
close: () => closeLoggerTransport(transport),
|
|
15
28
|
}, options);
|
|
16
29
|
}
|
|
17
30
|
/**
|
|
18
31
|
* Creates a transport that forwards entries to multiple
|
|
19
32
|
* transports.
|
|
33
|
+
*
|
|
34
|
+
* Every sink receives every entry even when another sink throws: writes
|
|
35
|
+
* are settled independently and the failures are rethrown afterwards
|
|
36
|
+
* (one failure as itself, several as an AggregateError). `flush()` and
|
|
37
|
+
* `close()` fan out to every inner transport the same way.
|
|
20
38
|
*/
|
|
21
39
|
export function createMultiLoggerTransport(transports, options = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
const sinks = [...transports];
|
|
41
|
+
return createLoggerTransport({
|
|
42
|
+
name: options.name ?? createLoggerTransportId(),
|
|
43
|
+
enabled: options.enabled ?? true,
|
|
44
|
+
write: (entry, context) => settleAllOrThrow(sinks.map((sink) => () => writeLoggerTransport(sink, entry, context)), "Multiple logger transports failed to write an entry."),
|
|
45
|
+
flush: () => settleAllOrThrow(sinks.map((sink) => () => flushLoggerTransport(sink)), "Multiple logger transports failed to flush."),
|
|
46
|
+
close: () => settleAllOrThrow(sinks.map((sink) => () => closeLoggerTransport(sink)), "Multiple logger transports failed to close."),
|
|
26
47
|
}, options);
|
|
27
48
|
}
|
|
28
49
|
export { createBufferedLoggerTransport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/logger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Structured logging with transports, log levels, and context propagation for Zudojs applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=24.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@zudojs/errors": "1.0
|
|
31
|
+
"@zudojs/errors": "1.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "7.0.2",
|