@zudojs/logger 1.0.0 → 1.1.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 +12 -1
- package/dist/loggerCore/core/loggerCore.core.d.ts +3 -0
- package/dist/loggerCore/core/loggerCore.core.js +37 -5
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +9 -0
- package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js +28 -3
- package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +7 -1
- package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.context.js +6 -0
- package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +4 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -42,6 +42,12 @@ included — implement the `LoggerTransport` interface for those.
|
|
|
42
42
|
`transportTimeout` (default 10s) bounds every transport write, so a
|
|
43
43
|
transport that stops responding cannot hang `flush()` or `close()`.
|
|
44
44
|
|
|
45
|
+
`throwTransportErrors` (default `false`) rethrows transport and formatter
|
|
46
|
+
failures instead of dropping them. A synchronous transport throws from the
|
|
47
|
+
log call itself; a failure from an asynchronous transport (or with
|
|
48
|
+
`asynchronous: true`) cannot, so it is rethrown by the next `flush()` or
|
|
49
|
+
`close()`, which still flush and close the transports first.
|
|
50
|
+
|
|
45
51
|
## Flushing
|
|
46
52
|
|
|
47
53
|
Dispatch completes synchronously when every transport is synchronous.
|
|
@@ -71,7 +77,7 @@ createLogger({ redact: { enabled: false } }); // opt out
|
|
|
71
77
|
Text-shaped formatters escape control characters in the message, the
|
|
72
78
|
logger name, metadata keys and values, context values and source
|
|
73
79
|
locations. A newline or ANSI escape inside attacker-supplied text
|
|
74
|
-
becomes `\n` /
|
|
80
|
+
becomes `\n` / `\u001b` rather than forging an extra log record or
|
|
75
81
|
driving the operator's terminal. The JSON formatter relies on
|
|
76
82
|
`JSON.stringify`, which escapes the same characters.
|
|
77
83
|
|
|
@@ -94,6 +100,11 @@ withLoggerContext(logger, createLoggerContext({ traceId }), (scoped) => {
|
|
|
94
100
|
});
|
|
95
101
|
```
|
|
96
102
|
|
|
103
|
+
Per-call context is merged into the entry's metadata the same way:
|
|
104
|
+
`logger.log(LoggerLevel.INFO, "handled", { context: { tenant: "acme" } })`.
|
|
105
|
+
The entry's `context` also carries the identifiers (`requestId`, `traceId`,
|
|
106
|
+
...) of the active context for transports that read them directly.
|
|
107
|
+
|
|
97
108
|
Child loggers inherit name, level, formatter, transports, metadata and
|
|
98
109
|
redaction settings: `logger.child({ name: "api.db" })`.
|
|
99
110
|
|
|
@@ -31,7 +31,10 @@ export declare class ZudojsLogger implements Logger, ZudojsLoggerContext {
|
|
|
31
31
|
private _configuration;
|
|
32
32
|
private readonly _contextStorage;
|
|
33
33
|
private _disposed;
|
|
34
|
+
private _closing;
|
|
34
35
|
private readonly _pending;
|
|
36
|
+
private readonly _dispatchFailures;
|
|
37
|
+
private _droppedFailures;
|
|
35
38
|
constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage);
|
|
36
39
|
get configuration(): LoggerConfiguration;
|
|
37
40
|
get contextStorage(): LoggerContextStorage;
|
|
@@ -7,6 +7,8 @@ import { resolveLoggerOptions } from "../../loggerOptions/loggerOptions.type.js"
|
|
|
7
7
|
import { normalizeConfiguration, assertActive as assertActiveHelper, assertMutable as assertMutableHelper, handleInfrastructureError as handleInfrastructureErrorHelper, } from "../helpers/loggerCoreHelpers.js";
|
|
8
8
|
import { logAtLevel, childLogger, withContextLogger, setLoggerLevel, enableLogger, disableLogger, flushLogger, closeLogger, } from "../helpers/loggerCoreMethods/index.js";
|
|
9
9
|
import { getLoggerName, getLoggerLevel, getLoggerEnabled, } from "../helpers/loggerCoreMethods.loggerProps.js";
|
|
10
|
+
/** Upper bound on dispatch failures retained between two flushes. */
|
|
11
|
+
const MAX_RETAINED_DISPATCH_FAILURES = 32;
|
|
10
12
|
/**
|
|
11
13
|
* Logger implementation.
|
|
12
14
|
*/
|
|
@@ -14,7 +16,10 @@ export class ZudojsLogger {
|
|
|
14
16
|
_configuration;
|
|
15
17
|
_contextStorage;
|
|
16
18
|
_disposed = false;
|
|
19
|
+
_closing;
|
|
17
20
|
_pending = new Set();
|
|
21
|
+
_dispatchFailures = [];
|
|
22
|
+
_droppedFailures = 0;
|
|
18
23
|
constructor(options = {}, contextStorage) {
|
|
19
24
|
this._configuration = resolveLoggerOptions(options);
|
|
20
25
|
this._contextStorage = contextStorage ?? createLoggerContextStorage();
|
|
@@ -75,7 +80,14 @@ export class ZudojsLogger {
|
|
|
75
80
|
return flushLogger(this);
|
|
76
81
|
}
|
|
77
82
|
close() {
|
|
78
|
-
|
|
83
|
+
// Concurrent callers share one closure: two overlapping close() calls
|
|
84
|
+
// used to drain and close every transport twice.
|
|
85
|
+
if (this._closing)
|
|
86
|
+
return this._closing;
|
|
87
|
+
this._closing = closeLogger(this).finally(() => {
|
|
88
|
+
this._closing = undefined;
|
|
89
|
+
});
|
|
90
|
+
return this._closing;
|
|
79
91
|
}
|
|
80
92
|
assertActive() {
|
|
81
93
|
assertActiveHelper(this._disposed, this._configuration.name);
|
|
@@ -99,11 +111,23 @@ export class ZudojsLogger {
|
|
|
99
111
|
return new ZudojsLogger(options, this._contextStorage);
|
|
100
112
|
}
|
|
101
113
|
trackDispatch(dispatch) {
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
114
|
+
// A dispatch rejects only when handleError threw — i.e. when
|
|
115
|
+
// `throwTransportErrors` is on and an asynchronous transport failed.
|
|
116
|
+
// Nothing can throw from the log call that started it, so the failure
|
|
117
|
+
// is kept (bounded) and surfaced by the next flush()/close(); it was
|
|
118
|
+
// previously swallowed outright, which made the option a no-op for
|
|
119
|
+
// every asynchronous transport.
|
|
105
120
|
const tracked = dispatch
|
|
106
|
-
.
|
|
121
|
+
.then(() => { }, (error) => {
|
|
122
|
+
if (!this._configuration.throwTransportErrors)
|
|
123
|
+
return;
|
|
124
|
+
if (this._dispatchFailures.length < MAX_RETAINED_DISPATCH_FAILURES) {
|
|
125
|
+
this._dispatchFailures.push(error);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
this._droppedFailures += 1;
|
|
129
|
+
}
|
|
130
|
+
})
|
|
107
131
|
.finally(() => {
|
|
108
132
|
this._pending.delete(tracked);
|
|
109
133
|
});
|
|
@@ -115,6 +139,14 @@ export class ZudojsLogger {
|
|
|
115
139
|
while (this._pending.size > 0) {
|
|
116
140
|
await Promise.all([...this._pending]);
|
|
117
141
|
}
|
|
142
|
+
if (this._dispatchFailures.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const failures = this._dispatchFailures.splice(0);
|
|
145
|
+
const dropped = this._droppedFailures;
|
|
146
|
+
this._droppedFailures = 0;
|
|
147
|
+
if (failures.length === 1 && dropped === 0)
|
|
148
|
+
throw failures[0];
|
|
149
|
+
throw new AggregateError(failures, `${failures.length + dropped} log dispatch(es) failed since the last flush.`);
|
|
118
150
|
}
|
|
119
151
|
}
|
|
120
152
|
/**
|
|
@@ -14,9 +14,14 @@ export function createEntry(configuration, contextStorage, level, message, optio
|
|
|
14
14
|
const contextMetadata = activeContext
|
|
15
15
|
? contextToLogMetadata(activeContext)
|
|
16
16
|
: {};
|
|
17
|
+
// Per-call `options.context` flows into metadata exactly like the ambient
|
|
18
|
+
// context does. It used to reach only `entry.context.metadata`, which the
|
|
19
|
+
// text formatters never print, so `log(level, msg, { context })` silently
|
|
20
|
+
// dropped the data from every text-shaped line.
|
|
17
21
|
const rawMetadata = {
|
|
18
22
|
...configuration.metadata,
|
|
19
23
|
...contextMetadata,
|
|
24
|
+
...(options.context ?? {}),
|
|
20
25
|
...(options.metadata ?? {}),
|
|
21
26
|
};
|
|
22
27
|
// Redaction is applied HERE, before the entry is frozen, so every
|
|
@@ -35,8 +40,12 @@ export function createEntry(configuration, contextStorage, level, message, optio
|
|
|
35
40
|
level,
|
|
36
41
|
message,
|
|
37
42
|
metadata,
|
|
43
|
+
// `LoggerEntryContext` declares the correlation identifiers, but they
|
|
44
|
+
// were never copied here, so a transport reading `entry.context.requestId`
|
|
45
|
+
// always saw `undefined`.
|
|
38
46
|
context: context
|
|
39
47
|
? {
|
|
48
|
+
...context.identifiers,
|
|
40
49
|
metadata: redactLogValue(context.metadata, isSecret, replacement),
|
|
41
50
|
}
|
|
42
51
|
: undefined,
|
|
@@ -50,8 +50,18 @@ export async function flushLogger(ctx) {
|
|
|
50
50
|
ctx.assertActive();
|
|
51
51
|
// In-flight dispatches must land in the transports before those
|
|
52
52
|
// transports are asked to flush, otherwise flush() is a no-op for
|
|
53
|
-
// everything logged in the same tick.
|
|
54
|
-
|
|
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
|
+
}
|
|
55
65
|
for (const transport of ctx.configuration.transports) {
|
|
56
66
|
if (!isLoggerTransport(transport)) {
|
|
57
67
|
continue;
|
|
@@ -64,6 +74,8 @@ export async function flushLogger(ctx) {
|
|
|
64
74
|
await registered.flush();
|
|
65
75
|
}
|
|
66
76
|
}
|
|
77
|
+
if (failed)
|
|
78
|
+
throw failure;
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
69
81
|
* Closes all transports and marks logger as disposed.
|
|
@@ -72,7 +84,18 @@ export async function closeLogger(ctx) {
|
|
|
72
84
|
if (ctx.isDisposed()) {
|
|
73
85
|
return;
|
|
74
86
|
}
|
|
75
|
-
|
|
87
|
+
// Closing is terminal: transports are flushed and closed and the logger
|
|
88
|
+
// is marked disposed even when a dispatch failed; the failure is rethrown
|
|
89
|
+
// afterwards.
|
|
90
|
+
let failure;
|
|
91
|
+
let failed = false;
|
|
92
|
+
try {
|
|
93
|
+
await ctx.drainDispatches();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
failure = error;
|
|
97
|
+
failed = true;
|
|
98
|
+
}
|
|
76
99
|
for (const transport of ctx.configuration.transports) {
|
|
77
100
|
if (!isLoggerTransport(transport)) {
|
|
78
101
|
continue;
|
|
@@ -86,5 +109,7 @@ export async function closeLogger(ctx) {
|
|
|
86
109
|
}
|
|
87
110
|
}
|
|
88
111
|
ctx.markDisposed();
|
|
112
|
+
if (failed)
|
|
113
|
+
throw failure;
|
|
89
114
|
}
|
|
90
115
|
//# sourceMappingURL=loggerCoreMethods.lifecycle.js.map
|
|
@@ -62,7 +62,13 @@ export function createSecretMatcher(options = {}) {
|
|
|
62
62
|
if (options.enabled === false) {
|
|
63
63
|
return () => false;
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
// Copy the pattern without `g`/`y`: those flags make `test()` advance
|
|
66
|
+
// `lastIndex`, so a shared pattern would match a secret-named field on
|
|
67
|
+
// one entry and let it through unredacted on the next.
|
|
68
|
+
const configured = options.pattern ?? DEFAULT_LOGGER_SECRET_PATTERN;
|
|
69
|
+
const pattern = configured.global || configured.sticky
|
|
70
|
+
? new RegExp(configured.source, configured.flags.replace(/[gy]/gu, ""))
|
|
71
|
+
: configured;
|
|
66
72
|
const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
|
|
67
73
|
return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
|
|
68
74
|
}
|
|
@@ -17,6 +17,12 @@ export function formatContext(entry) {
|
|
|
17
17
|
if (typeof value === "object") {
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
+
// Context identifiers are also flattened into metadata; skip the ones
|
|
21
|
+
// the metadata block already prints so a line never repeats itself.
|
|
22
|
+
if (Object.hasOwn(entry.metadata, key) &&
|
|
23
|
+
entry.metadata[key] === value) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
20
26
|
values.push(`${escapeLogText(key)}=${escapeLogText(String(value))}`);
|
|
21
27
|
}
|
|
22
28
|
return values.length > 0 ? `[${values.join(" ")}]` : "";
|
|
@@ -34,6 +34,10 @@ export function createBufferedLoggerTransport(transport, options = {}) {
|
|
|
34
34
|
/* deliberate no-op */
|
|
35
35
|
}
|
|
36
36
|
}, flushInterval);
|
|
37
|
+
// A pending flush is housekeeping, not work: left referenced it kept a
|
|
38
|
+
// finished process alive for a full `flushInterval`. close() flushes
|
|
39
|
+
// whatever is buffered, so nothing is lost by letting the loop exit.
|
|
40
|
+
timer.unref?.();
|
|
37
41
|
};
|
|
38
42
|
const buffered = {
|
|
39
43
|
name: options.name ?? "buffered",
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/logger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Structured logging with transports, log levels, and context propagation for Zudojs applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
"node": ">=24.0.0"
|
|
25
29
|
},
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@zudojs/errors": "1.0.
|
|
31
|
+
"@zudojs/errors": "1.0.1"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"typescript": "7.0.2",
|