@zudojs/logger 1.1.0 → 1.3.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 +63 -12
- 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.dispatch.js +62 -17
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.d.ts +6 -1
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +8 -3
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.js +23 -2
- 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/loggerEntryCreate.js +2 -1
- package/dist/loggerEntry/loggerEntryHelpers/index.d.ts +1 -1
- package/dist/loggerEntry/loggerEntryHelpers/index.js +1 -1
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +29 -12
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +89 -31
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.js +2 -1
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.d.ts +5 -0
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js +43 -12
- package/dist/loggerErrors/loggerError.helpers.d.ts +11 -0
- package/dist/loggerErrors/loggerError.helpers.js +21 -0
- package/dist/loggerFactory/loggerFactory.core.d.ts +7 -0
- package/dist/loggerFactory/loggerFactory.core.js +10 -0
- package/dist/loggerFormatter/loggerFormatter.core.js +3 -2
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js +1 -1
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.js +26 -13
- 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/loggerLevel/loggerLevel.type.js +3 -2
- package/dist/loggerManager/loggerManager.core.d.ts +10 -0
- package/dist/loggerManager/loggerManager.core.js +24 -7
- package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +66 -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,8 +45,21 @@ 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
|
-
transport that stops responding cannot hang `flush()` or `close()`.
|
|
58
|
+
transport that stops responding cannot hang `flush()` or `close()`. A
|
|
59
|
+
write that exceeds it fails with `LoggerTimeoutError`, carrying
|
|
60
|
+
`transportName` and `timeout`; other write failures are reported as
|
|
61
|
+
`LoggerTransportError` with `transportName` set, and formatter failures
|
|
62
|
+
as `LoggerFormatterError` with `formatterName` set.
|
|
44
63
|
|
|
45
64
|
`throwTransportErrors` (default `false`) rethrows transport and formatter
|
|
46
65
|
failures instead of dropping them. A synchronous transport throws from the
|
|
@@ -48,21 +67,41 @@ log call itself; a failure from an asynchronous transport (or with
|
|
|
48
67
|
`asynchronous: true`) cannot, so it is rethrown by the next `flush()` or
|
|
49
68
|
`close()`, which still flush and close the transports first.
|
|
50
69
|
|
|
70
|
+
`flush()` and `close()` isolate each transport: one that throws does not
|
|
71
|
+
stop the rest from being flushed and closed, and `close()` always leaves
|
|
72
|
+
the logger disposed. The failures are rethrown afterwards (several as one
|
|
73
|
+
`AggregateError`).
|
|
74
|
+
|
|
51
75
|
## Flushing
|
|
52
76
|
|
|
53
77
|
Dispatch completes synchronously when every transport is synchronous.
|
|
54
78
|
With an asynchronous transport — or with `asynchronous: true`, which
|
|
55
79
|
always defers so the caller stays off the transport's critical path —
|
|
56
80
|
writes are in flight until drained. `flush()` and `close()` drain them,
|
|
57
|
-
|
|
81
|
+
including writes started by child loggers (`child()`, `withContext()`),
|
|
82
|
+
so nothing is lost at exit. A child reports itself disposed once its root
|
|
83
|
+
logger is closed.
|
|
58
84
|
|
|
59
85
|
## Secret redaction
|
|
60
86
|
|
|
61
87
|
Redaction is **on by default**. Metadata and context fields whose NAME
|
|
62
|
-
looks like a secret
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
looks like a secret are replaced with `"[REDACTED]"` before the entry
|
|
89
|
+
reaches any formatter or transport. Names are split into words
|
|
90
|
+
(`x-api-key`, `api_key` and `apiKey` all read as `api key`) and matched
|
|
91
|
+
against `DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret,
|
|
92
|
+
token, jwt, bearer, auth, authorization, cookie, session, sid,
|
|
93
|
+
credential, api key, private key, client secret, card number, cvv, ssn,
|
|
94
|
+
pin, otp and more — so `sessionId` and `cardNumber` are redacted while
|
|
95
|
+
`passenger` and `authorId` are not. Nested objects, arrays, `Map`,
|
|
96
|
+
`Set` and getters are all covered — a `Map` is redacted per key and a
|
|
97
|
+
`Set` becomes an array. A getter that THROWS yields `"[Unreadable]"`
|
|
98
|
+
for that field: the rest of the entry is logged and the read failure is
|
|
99
|
+
reported through the same path as transport and formatter failures
|
|
100
|
+
(dropped by default, rethrown with `throwTransportErrors`), so a lazy
|
|
101
|
+
ORM relation can never abort the caller's log statement. Passing
|
|
102
|
+
`redact.pattern` replaces the word matcher with
|
|
103
|
+
your own RegExp (the old substring default is still exported as
|
|
104
|
+
`DEFAULT_LOGGER_SECRET_PATTERN`).
|
|
66
105
|
|
|
67
106
|
```typescript
|
|
68
107
|
logger.info("login", { user: "alice", password: "hunter2" });
|
|
@@ -74,16 +113,22 @@ createLogger({ redact: { enabled: false } }); // opt out
|
|
|
74
113
|
|
|
75
114
|
## Log injection
|
|
76
115
|
|
|
77
|
-
Text-shaped formatters escape control characters
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
becomes `\n` / `\u001b`
|
|
81
|
-
|
|
116
|
+
Text-shaped formatters escape control characters (C0, DEL, C1 and
|
|
117
|
+
U+2028/U+2029) in the message, the level, the logger name, metadata keys
|
|
118
|
+
and values, context values, source locations and error stacks. A newline
|
|
119
|
+
or ANSI escape inside attacker-supplied text becomes `\n` / `\u001b`
|
|
120
|
+
rather than forging an extra log record or driving the operator's
|
|
121
|
+
terminal. In a stack trace only the frame lines break the line: the
|
|
122
|
+
error's name and message are escaped as one line, and every frame line is
|
|
123
|
+
indented so none can start at column 0 and pass for a record. The JSON formatter relies on
|
|
82
124
|
`JSON.stringify`, which escapes the same characters.
|
|
83
125
|
|
|
84
126
|
Metadata is normalized before serialization, so circular references
|
|
85
127
|
(`"[Circular]"`), BigInt values and functions never make a formatter
|
|
86
|
-
throw and silently drop the record.
|
|
128
|
+
throw and silently drop the record. Only a genuine back-edge becomes
|
|
129
|
+
`"[Circular]"` — the walk tracks the ancestor path, so an object
|
|
130
|
+
referenced from two places in one payload (`{ actor: user, target: user }`)
|
|
131
|
+
is logged in full both times.
|
|
87
132
|
|
|
88
133
|
## Context
|
|
89
134
|
|
|
@@ -118,6 +163,12 @@ Pass `{ colors: true }` in the formatter context to colourize the level
|
|
|
118
163
|
tag of text output. Colour codes are emitted only around the fixed level
|
|
119
164
|
name, never around user-supplied text.
|
|
120
165
|
|
|
166
|
+
A formatter returns either a string or an object
|
|
167
|
+
(`LoggerFormattedOutput`). A string becomes the payload's `message`; an
|
|
168
|
+
object is merged OVER the entry, so `createStructuredLoggerFormatter()`
|
|
169
|
+
hands the transport its structured record (with an ISO-string
|
|
170
|
+
`timestamp` and serialized metadata) rather than the raw entry.
|
|
171
|
+
|
|
121
172
|
## Use Cases
|
|
122
173
|
|
|
123
174
|
- Application logging
|
|
@@ -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) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { formatLoggerEntry } from "../../../loggerFormatter/loggerFormatter.core.js";
|
|
5
5
|
import { createLoggerTransport, writeLoggerTransport, } from "../../../loggerTransport/loggerTransport.core.js";
|
|
6
6
|
import { isLoggerTransport } from "../../../loggerTransport/loggerTransportGuard.js";
|
|
7
|
-
import { LoggerFormatterError, LoggerTransportError, } from "../../../loggerErrors/loggerError.base.js";
|
|
8
|
-
import {
|
|
7
|
+
import { LoggerFormatterError, LoggerTimeoutError, LoggerTransportError, } from "../../../loggerErrors/loggerError.base.js";
|
|
8
|
+
import { createLoggerFormatterError, createLoggerTransportError, } from "../../../loggerErrors/loggerError.helpers.js";
|
|
9
9
|
/**
|
|
10
10
|
* Default formatter used when no formatter is configured.
|
|
11
11
|
*/
|
|
@@ -33,7 +33,11 @@ async function withTransportTimeout(timeoutMs, transportName, operation) {
|
|
|
33
33
|
let timer;
|
|
34
34
|
const expiry = new Promise((_resolve, reject) => {
|
|
35
35
|
timer = setTimeout(() => {
|
|
36
|
-
|
|
36
|
+
// LoggerTimeoutError carries `transportName` and `timeout`, which
|
|
37
|
+
// the bare LoggerTransportError raised here before did not — so
|
|
38
|
+
// `catch (e) { if (e instanceof LoggerTimeoutError) retry() }`
|
|
39
|
+
// could never match.
|
|
40
|
+
reject(new LoggerTimeoutError(transportName, timeoutMs));
|
|
37
41
|
}, timeoutMs);
|
|
38
42
|
// This timer bounds a write; it is not work in its own right. Left
|
|
39
43
|
// referenced it keeps the event loop alive, so a process that has
|
|
@@ -53,6 +57,50 @@ async function withTransportTimeout(timeoutMs, transportName, operation) {
|
|
|
53
57
|
void operation.catch(() => { });
|
|
54
58
|
}
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Builds the payload handed to a transport.
|
|
62
|
+
*
|
|
63
|
+
* `LoggerFormattedOutput` is `string | Record<string, unknown>`. A
|
|
64
|
+
* string replaces the message; an OBJECT is the formatted record and is
|
|
65
|
+
* merged over the entry, so a transport sees the formatter's fields.
|
|
66
|
+
* The object branch used to be computed and then discarded, which made
|
|
67
|
+
* `createStructuredLoggerFormatter()` pure overhead on every log call
|
|
68
|
+
* and handed the transport the raw, unformatted entry instead.
|
|
69
|
+
*/
|
|
70
|
+
function toTransportPayload(entry, formatted) {
|
|
71
|
+
if (typeof formatted === "string") {
|
|
72
|
+
return { ...entry, message: formatted };
|
|
73
|
+
}
|
|
74
|
+
if (formatted !== null &&
|
|
75
|
+
typeof formatted === "object" &&
|
|
76
|
+
!Array.isArray(formatted)) {
|
|
77
|
+
return {
|
|
78
|
+
...entry,
|
|
79
|
+
...formatted,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
/** Name of the configured formatter, for error reporting. */
|
|
85
|
+
function resolveFormatterName(configuration) {
|
|
86
|
+
const formatter = configuration.formatter;
|
|
87
|
+
if (typeof formatter === "string") {
|
|
88
|
+
return formatter;
|
|
89
|
+
}
|
|
90
|
+
return formatter.name ?? "formatter";
|
|
91
|
+
}
|
|
92
|
+
/** Wraps a transport failure, keeping an already-typed logger error. */
|
|
93
|
+
function toTransportError(transportName, error) {
|
|
94
|
+
return error instanceof LoggerTransportError
|
|
95
|
+
? error
|
|
96
|
+
: createLoggerTransportError(transportName, error);
|
|
97
|
+
}
|
|
98
|
+
/** Wraps a formatter failure, keeping an already-typed logger error. */
|
|
99
|
+
function toFormatterError(formatterName, error) {
|
|
100
|
+
return error instanceof LoggerFormatterError
|
|
101
|
+
? error
|
|
102
|
+
: createLoggerFormatterError(formatterName, error);
|
|
103
|
+
}
|
|
56
104
|
/**
|
|
57
105
|
* Writes to a transport.
|
|
58
106
|
*/
|
|
@@ -61,12 +109,7 @@ async function writeTransport(configuration, transport, entry, formatted) {
|
|
|
61
109
|
loggerName: configuration.name,
|
|
62
110
|
environment: configuration.environment,
|
|
63
111
|
};
|
|
64
|
-
|
|
65
|
-
const formattedEntry = { ...entry, message: formatted };
|
|
66
|
-
await writeLoggerTransport(transport.transport, formattedEntry, transportContext);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
await writeLoggerTransport(transport.transport, entry, transportContext);
|
|
112
|
+
await writeLoggerTransport(transport.transport, toTransportPayload(entry, formatted), transportContext);
|
|
70
113
|
}
|
|
71
114
|
/**
|
|
72
115
|
* Formats an entry with the configured formatter.
|
|
@@ -94,7 +137,7 @@ function writeTransportMaybeSync(configuration, transport, entry, formatted) {
|
|
|
94
137
|
loggerName: configuration.name,
|
|
95
138
|
environment: configuration.environment,
|
|
96
139
|
};
|
|
97
|
-
const payload =
|
|
140
|
+
const payload = toTransportPayload(entry, formatted);
|
|
98
141
|
const target = transport.transport;
|
|
99
142
|
return typeof target === "function"
|
|
100
143
|
? target(payload, transportContext)
|
|
@@ -109,24 +152,24 @@ export async function dispatchEntry(configuration, entry, handleError) {
|
|
|
109
152
|
formatted = formatEntry(configuration, entry);
|
|
110
153
|
}
|
|
111
154
|
catch (error) {
|
|
112
|
-
|
|
113
|
-
handleError(formatterError);
|
|
155
|
+
handleError(toFormatterError(resolveFormatterName(configuration), error));
|
|
114
156
|
return;
|
|
115
157
|
}
|
|
116
158
|
for (const transport of configuration.transports) {
|
|
159
|
+
let transportName = "transport";
|
|
117
160
|
try {
|
|
118
161
|
if (!isLoggerTransport(transport)) {
|
|
119
162
|
continue;
|
|
120
163
|
}
|
|
121
164
|
const registered = createLoggerTransport(transport);
|
|
165
|
+
transportName = registered.name;
|
|
122
166
|
if (!registered.enabled) {
|
|
123
167
|
continue;
|
|
124
168
|
}
|
|
125
169
|
await withTransportTimeout(configuration.transportTimeout, registered.name, writeTransport(configuration, registered, entry, formatted));
|
|
126
170
|
}
|
|
127
171
|
catch (error) {
|
|
128
|
-
|
|
129
|
-
handleError(transportError);
|
|
172
|
+
handleError(toTransportError(transportName, error));
|
|
130
173
|
}
|
|
131
174
|
}
|
|
132
175
|
}
|
|
@@ -153,28 +196,30 @@ export function dispatchEntrySync(configuration, entry, handleError) {
|
|
|
153
196
|
formatted = formatEntry(configuration, entry);
|
|
154
197
|
}
|
|
155
198
|
catch (error) {
|
|
156
|
-
handleError(
|
|
199
|
+
handleError(toFormatterError(resolveFormatterName(configuration), error));
|
|
157
200
|
return;
|
|
158
201
|
}
|
|
159
202
|
const pending = [];
|
|
160
203
|
for (const transport of configuration.transports) {
|
|
204
|
+
let transportName = "transport";
|
|
161
205
|
try {
|
|
162
206
|
if (!isLoggerTransport(transport)) {
|
|
163
207
|
continue;
|
|
164
208
|
}
|
|
165
209
|
const registered = createLoggerTransport(transport);
|
|
210
|
+
transportName = registered.name;
|
|
166
211
|
if (!registered.enabled) {
|
|
167
212
|
continue;
|
|
168
213
|
}
|
|
169
214
|
const result = writeTransportMaybeSync(configuration, registered, entry, formatted);
|
|
170
215
|
if (result instanceof Promise) {
|
|
171
216
|
pending.push(withTransportTimeout(configuration.transportTimeout, registered.name, result).catch((error) => {
|
|
172
|
-
handleError(
|
|
217
|
+
handleError(toTransportError(registered.name, error));
|
|
173
218
|
}));
|
|
174
219
|
}
|
|
175
220
|
}
|
|
176
221
|
catch (error) {
|
|
177
|
-
handleError(
|
|
222
|
+
handleError(toTransportError(transportName, error));
|
|
178
223
|
}
|
|
179
224
|
}
|
|
180
225
|
if (pending.length === 0) {
|
|
@@ -6,8 +6,13 @@ import type { LoggerEntry } from "../../../loggerEntry/loggerEntry.type.js";
|
|
|
6
6
|
import type { LoggerConfiguration, LogOptions } from "../../../loggerOptions/loggerOptions.type.js";
|
|
7
7
|
/**
|
|
8
8
|
* Creates a normalized log entry.
|
|
9
|
+
*
|
|
10
|
+
* @param onMetadataError - Notified when a metadata property could not
|
|
11
|
+
* be read (a throwing getter). The field is replaced with a marker
|
|
12
|
+
* and the entry is still produced, so the caller's log statement
|
|
13
|
+
* never aborts; reporting is the caller's job.
|
|
9
14
|
*/
|
|
10
15
|
export declare function createEntry(configuration: LoggerConfiguration, contextStorage: {
|
|
11
16
|
get(): import("../../../loggerContext/loggerContext.core.js").LoggerContext | undefined;
|
|
12
|
-
}, level: LoggerLevel, message: string, options: LogOptions): LoggerEntry;
|
|
17
|
+
}, level: LoggerLevel, message: string, options: LogOptions, onMetadataError?: (key: string, error: unknown) => void): LoggerEntry;
|
|
13
18
|
//# sourceMappingURL=loggerCoreMethods.entry.d.ts.map
|
|
@@ -6,8 +6,13 @@ import { createSecretMatcher, redactLogValue, LOGGER_REDACTION_TOKEN, } from "..
|
|
|
6
6
|
import { contextToLogMetadata, createLoggerContext, } from "../../../loggerContext/loggerContext.core.js";
|
|
7
7
|
/**
|
|
8
8
|
* Creates a normalized log entry.
|
|
9
|
+
*
|
|
10
|
+
* @param onMetadataError - Notified when a metadata property could not
|
|
11
|
+
* be read (a throwing getter). The field is replaced with a marker
|
|
12
|
+
* and the entry is still produced, so the caller's log statement
|
|
13
|
+
* never aborts; reporting is the caller's job.
|
|
9
14
|
*/
|
|
10
|
-
export function createEntry(configuration, contextStorage, level, message, options) {
|
|
15
|
+
export function createEntry(configuration, contextStorage, level, message, options, onMetadataError) {
|
|
11
16
|
const activeContext = configuration.inheritContext
|
|
12
17
|
? contextStorage.get()
|
|
13
18
|
: undefined;
|
|
@@ -29,7 +34,7 @@ export function createEntry(configuration, contextStorage, level, message, optio
|
|
|
29
34
|
// path can bypass it — including nested objects, arrays and getters.
|
|
30
35
|
const isSecret = createSecretMatcher(configuration.redact);
|
|
31
36
|
const replacement = configuration.redact.replacement ?? LOGGER_REDACTION_TOKEN;
|
|
32
|
-
const metadata = redactLogValue(rawMetadata, isSecret, replacement);
|
|
37
|
+
const metadata = redactLogValue(rawMetadata, isSecret, replacement, undefined, onMetadataError);
|
|
33
38
|
const context = options.context
|
|
34
39
|
? createLoggerContext({
|
|
35
40
|
parent: activeContext,
|
|
@@ -46,7 +51,7 @@ export function createEntry(configuration, contextStorage, level, message, optio
|
|
|
46
51
|
context: context
|
|
47
52
|
? {
|
|
48
53
|
...context.identifiers,
|
|
49
|
-
metadata: redactLogValue(context.metadata, isSecret, replacement),
|
|
54
|
+
metadata: redactLogValue(context.metadata, isSecret, replacement, undefined, onMetadataError),
|
|
50
55
|
}
|
|
51
56
|
: undefined,
|
|
52
57
|
source: options.source,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* ZudojsLogger level methods.
|
|
3
3
|
*/
|
|
4
4
|
import { LoggerLevel, shouldLog, } from "../../../loggerLevel/loggerLevel.type.js";
|
|
5
|
-
import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
|
|
5
|
+
import { InvalidLoggerEntryError, LoggerConfigurationError, } from "../../../loggerErrors/loggerError.base.js";
|
|
6
|
+
import { toLoggerError } from "../../../loggerErrors/loggerError.helpers.js";
|
|
6
7
|
import { createEntry } from "./loggerCoreMethods.entry.js";
|
|
7
8
|
import { dispatchEntrySync } from "./loggerCoreMethods.dispatch.js";
|
|
8
9
|
/**
|
|
@@ -17,7 +18,24 @@ export function logAtLevel(ctx, level, message, options = {}) {
|
|
|
17
18
|
if (typeof message !== "string") {
|
|
18
19
|
throw new LoggerConfigurationError("Logger message must be a string.");
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
// Entry construction reads caller-supplied metadata, including
|
|
22
|
+
// getters. A throwing accessor used to propagate straight out of
|
|
23
|
+
// logger.info(...) and abort the caller — and only when the level
|
|
24
|
+
// let the call through, so the same code was a silent no-op at one
|
|
25
|
+
// log level and a crash at another. The offending field becomes a
|
|
26
|
+
// marker and the failure is reported like every other infrastructure
|
|
27
|
+
// failure, AFTER the line has been dispatched.
|
|
28
|
+
const metadataFailures = [];
|
|
29
|
+
let entry;
|
|
30
|
+
try {
|
|
31
|
+
entry = createEntry(ctx.configuration, ctx.contextStorage, level, message, options, (key, error) => {
|
|
32
|
+
metadataFailures.push(new InvalidLoggerEntryError(`Failed to read log metadata field "${key}": ${toLoggerError(error).message}`, { cause: error }));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
ctx.handleInfrastructureError(new InvalidLoggerEntryError(`Failed to build log entry: ${toLoggerError(error).message}`, { cause: error }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
21
39
|
// Dispatch is asynchronous. It used to be fired and forgotten, so
|
|
22
40
|
// flush() and close() could return while entries were still in
|
|
23
41
|
// flight — messages were lost on process exit. Registering the
|
|
@@ -26,5 +44,8 @@ export function logAtLevel(ctx, level, message, options = {}) {
|
|
|
26
44
|
if (dispatch) {
|
|
27
45
|
ctx.trackDispatch(dispatch);
|
|
28
46
|
}
|
|
47
|
+
for (const failure of metadataFailures) {
|
|
48
|
+
ctx.handleInfrastructureError(failure);
|
|
49
|
+
}
|
|
29
50
|
}
|
|
30
51
|
//# sourceMappingURL=loggerCoreMethods.level.js.map
|
|
@@ -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
|