@warlock.js/logger 5.29.0 → 5.30.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/CHANGELOG.md +25 -0
- package/README.md +6 -6
- package/cjs/index.cjs +233 -29
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.d.mts +1 -1
- package/esm/channels/console-log.d.mts.map +1 -1
- package/esm/channels/console-log.mjs +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +1 -1
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +1 -1
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/json-file-log.d.mts +1 -1
- package/esm/channels/json-file-log.d.mts.map +1 -1
- package/esm/channels/json-file-log.mjs +1 -1
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.d.mts +1 -1
- package/esm/channels/sentry-log.d.mts.map +1 -1
- package/esm/channels/sentry-log.mjs +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +3 -2
- package/esm/log-channel.d.mts +1 -1
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +1 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +73 -18
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +152 -20
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/default-keys.d.mts +1 -1
- package/esm/redact/default-keys.d.mts.map +1 -1
- package/esm/redact/default-keys.mjs +1 -1
- package/esm/redact/default-keys.mjs.map +1 -1
- package/esm/redact/index.mjs +1 -0
- package/esm/redact/mask-email.d.mts +40 -0
- package/esm/redact/mask-email.d.mts.map +1 -0
- package/esm/redact/mask-email.mjs +70 -0
- package/esm/redact/mask-email.mjs.map +1 -0
- package/esm/redact/redact.d.mts +1 -1
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +9 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/esm/utils/clear-message.d.mts +1 -1
- package/esm/utils/clear-message.d.mts.map +1 -1
- package/esm/utils/clear-message.mjs +1 -1
- package/esm/utils/clear-message.mjs.map +1 -1
- package/esm/utils/safe-json-stringify.d.mts +1 -1
- package/esm/utils/safe-json-stringify.d.mts.map +1 -1
- package/esm/utils/safe-json-stringify.mjs +1 -1
- package/esm/utils/safe-json-stringify.mjs.map +1 -1
- package/llms-full.txt +6 -6
- package/package.json +4 -3
- package/skills/flush-logs-on-shutdown/SKILL.md +2 -2
- package/skills/logger-basics/SKILL.md +3 -3
- package/skills/write-custom-log-channel/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to `@warlock.js/logger` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.30.0 - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Upgrading
|
|
10
|
+
|
|
11
|
+
- **BREAKING (types):** `log.info/warn/error/debug/success/fatal/log/assert` and the function `timer()` returns now return the logger synchronously instead of `Promise<this>`. Remove `await` before log calls (it is harmless but pointless) and any `.then()` chained on one; code that needs the write to finish calls `await log.flush()`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Logging is fire-and-forget: level methods no longer return a promise, so `@typescript-eslint/no-floating-promises` no longer flags every call site (WR-019).
|
|
16
|
+
- `log.flush()` drains every channel concurrently: each waits for its own in-flight async deliveries (including entries logged while the flush runs), then runs its `flush()`, so a hung network channel no longer holds up the file channel.
|
|
17
|
+
- `log.flush({ timeout })` is bounded: it resolves after `timeout` ms (default 5000, `DEFAULT_FLUSH_TIMEOUT_MS`; `Infinity` waits without a limit) and reports each channel still draining to the channel error handler. Before, one delivery that never settled made `flush()` (and a graceful shutdown awaiting it) wait forever.
|
|
18
|
+
- `maskEmailsInText` cuts text longer than `MASK_EMAILS_MAX_LENGTH` (16,384 characters) and marks the cut, so masking a huge cache key or message has a bounded cost.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `log.onChannelError(handler)` / `configure({ onChannelError })` and the `ChannelErrorHandler` type: receives a channel's sync throw or async rejection from `log()`, a redaction that threw, or a timed-out `flush()`. Default: one stderr line. The handler may be async; a rejection is swallowed.
|
|
23
|
+
- `maskEmail(address)` and `maskEmailsInText(text)`: mask email addresses (`m***@example.com`) inside message strings and cache keys, which key-based redaction cannot reach. Core's mail logs and the cache drivers' key logs use them (FW-04).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- A channel whose `log()` rejected became an unhandled rejection (looping through `captureAnyUnhandledRejection`), and one that threw synchronously stopped delivery to the channels after it; both are now isolated and reported to the channel error handler.
|
|
28
|
+
- A redaction that threw (a `censor` function that fails on an unexpected value) threw at the call site and skipped the remaining channels. A channel's own redaction failure is now isolated like any channel failure; a logger-wide one drops the entry (it is never delivered unredacted) and is reported to the channel error handler.
|
|
29
|
+
- An async `onChannelError` handler that rejected became an unhandled rejection.
|
|
30
|
+
- **Security:** `maskEmailsInText` backtracked quadratically on long runs of address-like characters with no domain (about 11 s on 75 KB), and it runs on cache keys built from request input. Its pattern is now bounded and linear. It also masks addresses with letters from any script (`jöhn@exämple.de`), and `maskEmail` no longer keeps half of a surrogate pair.
|
|
31
|
+
|
|
7
32
|
## 5.29.0 - 2026-10-01
|
|
8
33
|
|
|
9
34
|
### Changed
|
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ log.setChannels([
|
|
|
28
28
|
new FileLog({ chunk: "daily" }),
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
log.info("users", "register", "New user created");
|
|
32
|
+
log.error("payments", "charge", new Error("Card declined"));
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
The logger starts with no channels — nothing is printed or written until you register at least one.
|
|
@@ -39,15 +39,15 @@ The logger starts with no channels — nothing is printed or written until you r
|
|
|
39
39
|
Every call carries three pieces of context. Modules and actions become searchable keys in file and JSON channels:
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
log.info("auth", "login", "User signed in");
|
|
43
|
+
log.warn("api", "rateLimitApproaching", "80% of quota used");
|
|
44
|
+
log.success("payments", "captured", "Payment of $49.99 captured");
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Pass an object instead of positional args when you need `context` metadata:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
|
-
|
|
50
|
+
log.error({
|
|
51
51
|
module: "orders",
|
|
52
52
|
action: "checkout",
|
|
53
53
|
message: "Card declined",
|
package/cjs/index.cjs
CHANGED
|
@@ -39,7 +39,7 @@ path = __toESM(path, 1);
|
|
|
39
39
|
let safe_stable_stringify = require("safe-stable-stringify");
|
|
40
40
|
let _mongez_reinforcements = require("@mongez/reinforcements");
|
|
41
41
|
|
|
42
|
-
//#region
|
|
42
|
+
//#region ../../packages/logger/src/log-channel.ts
|
|
43
43
|
var LogChannel = class {
|
|
44
44
|
/**
|
|
45
45
|
* Constructor
|
|
@@ -112,7 +112,7 @@ var LogChannel = class {
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
//#endregion
|
|
115
|
-
//#region
|
|
115
|
+
//#region ../../packages/logger/src/channels/console-log.ts
|
|
116
116
|
/**
|
|
117
117
|
* The longest level name (`success`) sets the tag column width. Padding every
|
|
118
118
|
* level name to this width keeps the timestamp / module / action columns
|
|
@@ -195,7 +195,7 @@ var ConsoleLog = class extends LogChannel {
|
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
//#endregion
|
|
198
|
-
//#region
|
|
198
|
+
//#region ../../packages/logger/src/channels/file-log.ts
|
|
199
199
|
var FileLog = class extends LogChannel {
|
|
200
200
|
constructor(..._args) {
|
|
201
201
|
super(..._args);
|
|
@@ -467,7 +467,7 @@ var FileLog = class extends LogChannel {
|
|
|
467
467
|
};
|
|
468
468
|
|
|
469
469
|
//#endregion
|
|
470
|
-
//#region
|
|
470
|
+
//#region ../../packages/logger/src/utils/safe-json-stringify.ts
|
|
471
471
|
/**
|
|
472
472
|
* Replacer that surfaces Error data — `name`, `message`, and `stack` are
|
|
473
473
|
* non-enumerable on `Error`, so neither default JSON serialization nor an
|
|
@@ -498,7 +498,7 @@ function safeJsonStringify(value, space) {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
//#endregion
|
|
501
|
-
//#region
|
|
501
|
+
//#region ../../packages/logger/src/channels/json-file-log.ts
|
|
502
502
|
var JSONFileLog = class extends FileLog {
|
|
503
503
|
constructor(..._args) {
|
|
504
504
|
super(..._args);
|
|
@@ -626,7 +626,7 @@ var JSONFileLog = class extends FileLog {
|
|
|
626
626
|
};
|
|
627
627
|
|
|
628
628
|
//#endregion
|
|
629
|
-
//#region
|
|
629
|
+
//#region ../../packages/logger/src/channels/sentry-log.ts
|
|
630
630
|
let Sentry;
|
|
631
631
|
let isModuleExists = null;
|
|
632
632
|
let loadingPromise;
|
|
@@ -823,7 +823,7 @@ var SentryLog = class extends LogChannel {
|
|
|
823
823
|
};
|
|
824
824
|
|
|
825
825
|
//#endregion
|
|
826
|
-
//#region
|
|
826
|
+
//#region ../../packages/logger/src/redact/default-keys.ts
|
|
827
827
|
/**
|
|
828
828
|
* Built-in secret-key denylist — the redaction floor that applies with no
|
|
829
829
|
* configuration at all.
|
|
@@ -937,7 +937,7 @@ const DEFAULT_REDACT_KEYS = [
|
|
|
937
937
|
const DEFAULT_REDACT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeRedactKey));
|
|
938
938
|
|
|
939
939
|
//#endregion
|
|
940
|
-
//#region
|
|
940
|
+
//#region ../../packages/logger/src/redact/redact.ts
|
|
941
941
|
const DEFAULT_CENSOR = "[REDACTED]";
|
|
942
942
|
/**
|
|
943
943
|
* Values we copy by reference rather than walking. Expanding these with
|
|
@@ -1201,7 +1201,75 @@ function mergeRedact(base, extra) {
|
|
|
1201
1201
|
}
|
|
1202
1202
|
|
|
1203
1203
|
//#endregion
|
|
1204
|
-
//#region
|
|
1204
|
+
//#region ../../packages/logger/src/redact/mask-email.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* Email addresses inside free text: a local part and a dotted domain, letters
|
|
1207
|
+
* and digits from any script (`jöhn@exämple.de`). Kept deliberately simple (no
|
|
1208
|
+
* quoted local parts, no IP-literal domains, no percent-encoded `@`): it masks
|
|
1209
|
+
* the addresses that show up in log messages and cache keys, not every string
|
|
1210
|
+
* RFC 5322 allows.
|
|
1211
|
+
*
|
|
1212
|
+
* Every repetition is bounded (local part 64, label 63, at most 12 labels), so
|
|
1213
|
+
* matching stays linear in the input length. Cache keys and messages are often
|
|
1214
|
+
* built from request input; an unbounded pattern backtracks quadratically on a
|
|
1215
|
+
* long run of address-like characters with no domain.
|
|
1216
|
+
*/
|
|
1217
|
+
const EMAIL_IN_TEXT = /[\p{L}\p{N}._%+-]{1,64}@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63}){0,10}\.\p{L}{2,63}/gu;
|
|
1218
|
+
/**
|
|
1219
|
+
* Longest text {@link maskEmailsInText} scans. Anything past it is cut off and
|
|
1220
|
+
* replaced by a marker, so a huge cache key or message cannot cost more than a
|
|
1221
|
+
* bounded amount of time per log line, and nothing past the cut can leak.
|
|
1222
|
+
*/
|
|
1223
|
+
const MASK_EMAILS_MAX_LENGTH = 16384;
|
|
1224
|
+
/**
|
|
1225
|
+
* Mask one email address, keeping its first character and its domain:
|
|
1226
|
+
* `member@example.com` → `m***@example.com`. A value without an `@` is
|
|
1227
|
+
* returned as `***`.
|
|
1228
|
+
*
|
|
1229
|
+
* Key-based redaction cannot reach an address that sits inside a message
|
|
1230
|
+
* string or a cache key; mask it before it is logged.
|
|
1231
|
+
*
|
|
1232
|
+
* @example
|
|
1233
|
+
* log.info("mail", "send", `Sending mail to: ${maskEmail(to)}`);
|
|
1234
|
+
*/
|
|
1235
|
+
function maskEmail(address) {
|
|
1236
|
+
const at = address.lastIndexOf("@");
|
|
1237
|
+
if (at <= 0) return "***";
|
|
1238
|
+
return `${String.fromCodePoint(address.codePointAt(0))}***${address.slice(at)}`;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Mask every email address the pattern above recognises in a piece of text
|
|
1242
|
+
* with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}
|
|
1243
|
+
* characters is truncated first (the cut is marked).
|
|
1244
|
+
*
|
|
1245
|
+
* Dots are valid in the local part of an address, so a dotted prefix that
|
|
1246
|
+
* runs into the address is masked with it: in a cache key such as
|
|
1247
|
+
* `email.member@example.com` the whole `email.member` reads as the local part.
|
|
1248
|
+
*
|
|
1249
|
+
* Not recognised (left as they are): quoted local parts (`"john doe"@x.com`),
|
|
1250
|
+
* IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses
|
|
1251
|
+
* (`user%40example.com`).
|
|
1252
|
+
*
|
|
1253
|
+
* @example
|
|
1254
|
+
* maskEmailsInText("sent to member@example.com") // "sent to m***@example.com"
|
|
1255
|
+
* maskEmailsInText("email.member@example.com") // "e***@example.com"
|
|
1256
|
+
*/
|
|
1257
|
+
function maskEmailsInText(text) {
|
|
1258
|
+
let scanned = text;
|
|
1259
|
+
let marker = "";
|
|
1260
|
+
if (scanned.length > 16384) {
|
|
1261
|
+
let cut = MASK_EMAILS_MAX_LENGTH;
|
|
1262
|
+
const last = scanned.charCodeAt(cut - 1);
|
|
1263
|
+
if (last >= 55296 && last <= 56319) cut -= 1;
|
|
1264
|
+
marker = `…[${scanned.length - cut} chars truncated]`;
|
|
1265
|
+
scanned = scanned.slice(0, cut);
|
|
1266
|
+
}
|
|
1267
|
+
if (!scanned.includes("@")) return scanned + marker;
|
|
1268
|
+
return scanned.replace(EMAIL_IN_TEXT, (match) => maskEmail(match)) + marker;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
//#endregion
|
|
1272
|
+
//#region ../../packages/logger/src/utils/clear-message.ts
|
|
1205
1273
|
/**
|
|
1206
1274
|
* Clear message from any terminal codes
|
|
1207
1275
|
*/
|
|
@@ -1211,7 +1279,7 @@ function clearMessage(message) {
|
|
|
1211
1279
|
}
|
|
1212
1280
|
|
|
1213
1281
|
//#endregion
|
|
1214
|
-
//#region
|
|
1282
|
+
//#region ../../packages/logger/src/logger.ts
|
|
1215
1283
|
const SIGNAL_EVENTS = new Set([
|
|
1216
1284
|
"SIGINT",
|
|
1217
1285
|
"SIGTERM",
|
|
@@ -1239,6 +1307,8 @@ var Logger = class {
|
|
|
1239
1307
|
this.channels = [];
|
|
1240
1308
|
this.id = "logger-" + _mongez_reinforcements.Random.string(32);
|
|
1241
1309
|
this.autoFlushHandlers = /* @__PURE__ */ new Map();
|
|
1310
|
+
this.pendingDeliveries = /* @__PURE__ */ new Map();
|
|
1311
|
+
this.channelErrorHandler = reportChannelError;
|
|
1242
1312
|
}
|
|
1243
1313
|
/**
|
|
1244
1314
|
* Add a new channel
|
|
@@ -1255,6 +1325,21 @@ var Logger = class {
|
|
|
1255
1325
|
if (config.autoFlushOn) this.enableAutoFlush(config.autoFlushOn);
|
|
1256
1326
|
if (config.minLevel !== void 0) this.setMinLevel(config.minLevel);
|
|
1257
1327
|
if (config.redact !== void 0) this.setRedact(config.redact);
|
|
1328
|
+
if (config.onChannelError) this.onChannelError(config.onChannelError);
|
|
1329
|
+
return this;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Handle a channel's delivery failure. Logging is fire-and-forget, so a
|
|
1333
|
+
* channel that throws or rejects never reaches the call site and never
|
|
1334
|
+
* becomes an unhandled rejection; it goes to this handler instead. The
|
|
1335
|
+
* default writes one line to stderr. The handler must not log through this
|
|
1336
|
+
* logger (a failing channel would loop).
|
|
1337
|
+
*
|
|
1338
|
+
* @example
|
|
1339
|
+
* log.onChannelError((error, channel) => metrics.increment(`log.${channel.name}.failed`));
|
|
1340
|
+
*/
|
|
1341
|
+
onChannelError(handler) {
|
|
1342
|
+
this.channelErrorHandler = handler;
|
|
1258
1343
|
return this;
|
|
1259
1344
|
}
|
|
1260
1345
|
/**
|
|
@@ -1332,21 +1417,62 @@ var Logger = class {
|
|
|
1332
1417
|
* — each channel sees its own shallow clone so one channel cannot observe
|
|
1333
1418
|
* another's mutations (e.g. a later terminal channel still sees the original
|
|
1334
1419
|
* colored message).
|
|
1420
|
+
*
|
|
1421
|
+
* Synchronous and fire-and-forget: it returns the logger, not a promise.
|
|
1422
|
+
* Async channel deliveries are tracked and awaited by {@link flush}; a
|
|
1423
|
+
* channel that throws or rejects is isolated from the others and reported
|
|
1424
|
+
* to {@link onChannelError}.
|
|
1335
1425
|
*/
|
|
1336
|
-
|
|
1426
|
+
log(data) {
|
|
1337
1427
|
if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) return this;
|
|
1338
|
-
|
|
1339
|
-
|
|
1428
|
+
let baseEntry;
|
|
1429
|
+
try {
|
|
1430
|
+
baseEntry = applyRedact(data, this.redactConfig);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
for (const channel of this.channels) this.reportChannelError(error, channel, redactionFailedEntry(data));
|
|
1433
|
+
return this;
|
|
1434
|
+
}
|
|
1435
|
+
for (const channel of this.channels) this.deliver(channel, data, baseEntry);
|
|
1436
|
+
return this;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Prepare and hand one entry to one channel, isolating its failure (a
|
|
1440
|
+
* channel redaction that throws included) and tracking an async delivery
|
|
1441
|
+
* until it settles.
|
|
1442
|
+
*/
|
|
1443
|
+
deliver(channel, data, baseEntry) {
|
|
1444
|
+
let payload = baseEntry;
|
|
1445
|
+
let result;
|
|
1446
|
+
try {
|
|
1340
1447
|
const channelRedact = channel.getRedactConfig?.();
|
|
1341
|
-
|
|
1342
|
-
let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;
|
|
1448
|
+
if (channelRedact) payload = applyRedact(data, mergeRedact(this.redactConfig, channelRedact));
|
|
1343
1449
|
if (channel.terminal === false) payload = {
|
|
1344
1450
|
...payload,
|
|
1345
1451
|
message: clearMessage(payload.message)
|
|
1346
1452
|
};
|
|
1347
|
-
channel.log(payload);
|
|
1453
|
+
result = channel.log(payload);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
this.reportChannelError(error, channel, payload);
|
|
1456
|
+
return;
|
|
1348
1457
|
}
|
|
1349
|
-
return
|
|
1458
|
+
if (!result || typeof result.then !== "function") return;
|
|
1459
|
+
let pending = this.pendingDeliveries.get(channel);
|
|
1460
|
+
if (!pending) {
|
|
1461
|
+
pending = /* @__PURE__ */ new Set();
|
|
1462
|
+
this.pendingDeliveries.set(channel, pending);
|
|
1463
|
+
}
|
|
1464
|
+
const tracked = pending;
|
|
1465
|
+
const delivery = Promise.resolve(result).catch((error) => this.reportChannelError(error, channel, payload)).finally(() => {
|
|
1466
|
+
tracked.delete(delivery);
|
|
1467
|
+
if (tracked.size === 0 && this.pendingDeliveries.get(channel) === tracked) this.pendingDeliveries.delete(channel);
|
|
1468
|
+
});
|
|
1469
|
+
tracked.add(delivery);
|
|
1470
|
+
}
|
|
1471
|
+
reportChannelError(error, channel, entry) {
|
|
1472
|
+
try {
|
|
1473
|
+
const outcome = this.channelErrorHandler(error, channel, entry);
|
|
1474
|
+
if (outcome && typeof outcome.then === "function") outcome.then(void 0, () => {});
|
|
1475
|
+
} catch {}
|
|
1350
1476
|
}
|
|
1351
1477
|
/**
|
|
1352
1478
|
* Make debug log
|
|
@@ -1442,7 +1568,9 @@ var Logger = class {
|
|
|
1442
1568
|
for (const channel of this.channels) if (channel.flushSync) channel.flushSync();
|
|
1443
1569
|
}
|
|
1444
1570
|
/**
|
|
1445
|
-
*
|
|
1571
|
+
* Wait for every in-flight async delivery, then drain every channel that
|
|
1572
|
+
* implements `flush()` — bounded by `timeout` (default
|
|
1573
|
+
* {@link DEFAULT_FLUSH_TIMEOUT_MS}).
|
|
1446
1574
|
*
|
|
1447
1575
|
* Unlike {@link flushSync}, this awaits each channel's async I/O — the
|
|
1448
1576
|
* correct call for a graceful shutdown that can afford to wait
|
|
@@ -1450,24 +1578,68 @@ var Logger = class {
|
|
|
1450
1578
|
* `process.exit`). A channel whose delivery is async (a network transport,
|
|
1451
1579
|
* an async disk write) implements `flush()`, not `flushSync()`.
|
|
1452
1580
|
*
|
|
1453
|
-
* Channels
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1581
|
+
* Channels drain concurrently and independently: each waits for its own
|
|
1582
|
+
* pending deliveries (including ones logged while the flush runs), then
|
|
1583
|
+
* runs its `flush()`. A channel whose delivery hangs therefore cannot hold
|
|
1584
|
+
* up another channel's drain, and a channel whose flush rejects can neither
|
|
1585
|
+
* stop the others nor escape as an unhandled rejection. When `timeout`
|
|
1586
|
+
* elapses, `flush()` resolves anyway and each channel still draining is
|
|
1587
|
+
* reported to {@link onChannelError}. Channels without `flush()` only wait
|
|
1588
|
+
* for their deliveries.
|
|
1456
1589
|
*
|
|
1457
1590
|
* @example
|
|
1458
1591
|
* async function shutdown() {
|
|
1459
1592
|
* await httpServer.close();
|
|
1460
|
-
* await log.flush();
|
|
1593
|
+
* await log.flush({ timeout: 3000 });
|
|
1461
1594
|
* process.exit(0);
|
|
1462
1595
|
* }
|
|
1463
1596
|
*/
|
|
1464
|
-
async flush() {
|
|
1465
|
-
|
|
1466
|
-
|
|
1597
|
+
async flush(options = {}) {
|
|
1598
|
+
const timeout = options.timeout ?? 5e3;
|
|
1599
|
+
const draining = new Set(this.channels);
|
|
1600
|
+
for (const channel of this.pendingDeliveries.keys()) draining.add(channel);
|
|
1601
|
+
const unfinished = new Set(draining);
|
|
1602
|
+
const drains = [...draining].map(async (channel) => {
|
|
1467
1603
|
try {
|
|
1468
|
-
await
|
|
1469
|
-
} catch {}
|
|
1470
|
-
|
|
1604
|
+
await this.drainChannel(channel);
|
|
1605
|
+
} catch {} finally {
|
|
1606
|
+
unfinished.delete(channel);
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
const settled = Promise.allSettled(drains).then(() => false);
|
|
1610
|
+
if (!Number.isFinite(timeout)) {
|
|
1611
|
+
await settled;
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
let resolveTimeout;
|
|
1615
|
+
const timeoutReached = new Promise((resolve) => {
|
|
1616
|
+
resolveTimeout = resolve;
|
|
1617
|
+
});
|
|
1618
|
+
const timer = setTimeout(() => resolveTimeout(true), Math.max(0, timeout));
|
|
1619
|
+
timer.unref?.();
|
|
1620
|
+
const timedOut = await Promise.race([settled, timeoutReached]);
|
|
1621
|
+
clearTimeout(timer);
|
|
1622
|
+
if (!timedOut) return;
|
|
1623
|
+
for (const channel of unfinished) this.reportChannelError(/* @__PURE__ */ new Error(`flush timed out after ${timeout}ms; entries still in flight may be lost`), channel, {
|
|
1624
|
+
type: "warn",
|
|
1625
|
+
module: "logger",
|
|
1626
|
+
action: "flush",
|
|
1627
|
+
message: "flush timed out"
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Wait until the channel has no delivery in flight, run its `flush()`, and
|
|
1632
|
+
* repeat while that flush let new deliveries in.
|
|
1633
|
+
*/
|
|
1634
|
+
async drainChannel(channel) {
|
|
1635
|
+
do {
|
|
1636
|
+
let pending = this.pendingDeliveries.get(channel);
|
|
1637
|
+
while (pending && pending.size > 0) {
|
|
1638
|
+
await Promise.allSettled([...pending]);
|
|
1639
|
+
pending = this.pendingDeliveries.get(channel);
|
|
1640
|
+
}
|
|
1641
|
+
if (channel.flush) await channel.flush();
|
|
1642
|
+
} while ((this.pendingDeliveries.get(channel)?.size ?? 0) > 0);
|
|
1471
1643
|
}
|
|
1472
1644
|
/**
|
|
1473
1645
|
* Register one process-level handler per event that calls `flushSync()`
|
|
@@ -1513,6 +1685,34 @@ var Logger = class {
|
|
|
1513
1685
|
}
|
|
1514
1686
|
};
|
|
1515
1687
|
/**
|
|
1688
|
+
* Default `timeout` of {@link Logger.flush}: long enough for a healthy network
|
|
1689
|
+
* channel, short enough that a hung one cannot eat a shutdown budget.
|
|
1690
|
+
*/
|
|
1691
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1692
|
+
/**
|
|
1693
|
+
* What the error handler receives when the logger-wide redaction itself
|
|
1694
|
+
* failed: the entry's identity only, never the unredacted message or context.
|
|
1695
|
+
*/
|
|
1696
|
+
function redactionFailedEntry(data) {
|
|
1697
|
+
return {
|
|
1698
|
+
type: data.type,
|
|
1699
|
+
module: data.module,
|
|
1700
|
+
action: data.action,
|
|
1701
|
+
message: "[entry dropped: redaction failed]"
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Default {@link ChannelErrorHandler}: one stderr line per failure. Written
|
|
1706
|
+
* directly to the stream, never through a logger, so a broken channel cannot
|
|
1707
|
+
* recurse.
|
|
1708
|
+
*/
|
|
1709
|
+
function reportChannelError(error, channel) {
|
|
1710
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1711
|
+
try {
|
|
1712
|
+
process.stderr.write(`[@warlock.js/logger] channel "${channel.name}" failed to log: ${reason}\n`);
|
|
1713
|
+
} catch {}
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1516
1716
|
* The package singleton. Use this for everyday logging — `log.info(...)`,
|
|
1517
1717
|
* `log.error(...)`, `log.configure(...)`. Custom logger instances can be
|
|
1518
1718
|
* created by instantiating `Logger` directly.
|
|
@@ -1529,7 +1729,7 @@ var Logger = class {
|
|
|
1529
1729
|
const log = new Logger();
|
|
1530
1730
|
|
|
1531
1731
|
//#endregion
|
|
1532
|
-
//#region
|
|
1732
|
+
//#region ../../packages/logger/src/utils/capture-unhandled-errors.ts
|
|
1533
1733
|
/**
|
|
1534
1734
|
* Best-effort budget (ms) for draining async channels (file, Sentry, …) before
|
|
1535
1735
|
* the forced exit. Bounded so a single stuck channel can't hang the process —
|
|
@@ -1579,17 +1779,21 @@ function captureAnyUnhandledRejection(options = {}) {
|
|
|
1579
1779
|
|
|
1580
1780
|
//#endregion
|
|
1581
1781
|
exports.ConsoleLog = ConsoleLog;
|
|
1782
|
+
exports.DEFAULT_FLUSH_TIMEOUT_MS = DEFAULT_FLUSH_TIMEOUT_MS;
|
|
1582
1783
|
exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
|
|
1583
1784
|
exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
|
|
1584
1785
|
exports.FileLog = FileLog;
|
|
1585
1786
|
exports.JSONFileLog = JSONFileLog;
|
|
1586
1787
|
exports.LogChannel = LogChannel;
|
|
1587
1788
|
exports.Logger = Logger;
|
|
1789
|
+
exports.MASK_EMAILS_MAX_LENGTH = MASK_EMAILS_MAX_LENGTH;
|
|
1588
1790
|
exports.SentryLog = SentryLog;
|
|
1589
1791
|
exports.applyRedact = applyRedact;
|
|
1590
1792
|
exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
|
|
1591
1793
|
exports.clearMessage = clearMessage;
|
|
1592
1794
|
exports.log = log;
|
|
1795
|
+
exports.maskEmail = maskEmail;
|
|
1796
|
+
exports.maskEmailsInText = maskEmailsInText;
|
|
1593
1797
|
exports.mergeRedact = mergeRedact;
|
|
1594
1798
|
exports.normalizeRedactKey = normalizeRedactKey;
|
|
1595
1799
|
exports.resolveRedactKeys = resolveRedactKeys;
|