@warlock.js/logger 5.29.0 → 5.31.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 +48 -2
- package/README.md +14 -17
- package/cjs/index.cjs +569 -57
- 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 +129 -13
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +204 -23
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/file-name-pattern.d.mts +16 -0
- package/esm/channels/file-name-pattern.d.mts.map +1 -0
- package/esm/channels/file-name-pattern.mjs +41 -0
- package/esm/channels/file-name-pattern.mjs.map +1 -0
- package/esm/channels/index.mjs +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 +25 -6
- 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 +8 -5
- package/esm/index.mjs +5 -3
- package/esm/log-channel.d.mts +24 -2
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +23 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +78 -18
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +163 -22
- 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/escape-log-value.d.mts +26 -0
- package/esm/redact/escape-log-value.d.mts.map +1 -0
- package/esm/redact/escape-log-value.mjs +37 -0
- package/esm/redact/escape-log-value.mjs.map +1 -0
- package/esm/redact/index.mjs +2 -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 +10 -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 +247 -147
- package/llms.txt +2 -2
- package/package.json +4 -3
- package/skills/capture-unhandled-errors/SKILL.md +9 -10
- package/skills/configure-logger/SKILL.md +7 -10
- package/skills/filter-log-entries/SKILL.md +2 -2
- package/skills/flush-logs-on-shutdown/SKILL.md +34 -15
- package/skills/index.md +4 -1
- package/skills/logger-basics/SKILL.md +21 -24
- package/skills/overview/SKILL.md +17 -6
- package/skills/pick-log-channel/SKILL.md +36 -24
- package/skills/redact-sensitive-log-fields/SKILL.md +48 -18
- package/skills/ship-logs-to-sentry/SKILL.md +18 -18
- package/skills/test-logging-code/SKILL.md +4 -2
- package/skills/write-custom-log-channel/SKILL.md +51 -18
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
|
|
@@ -56,6 +56,28 @@ var LogChannel = class {
|
|
|
56
56
|
}, 0);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
+
* Route this channel's own failures (those outside a `log()` call) to the
|
|
60
|
+
* given reporter. `Logger` calls this when the channel is added, so the
|
|
61
|
+
* failures reach `log.onChannelError(...)`.
|
|
62
|
+
*/
|
|
63
|
+
setErrorReporter(reporter) {
|
|
64
|
+
this.errorReporter = reporter;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Report a failure that happened outside a `log()` call. Never throws:
|
|
68
|
+
* without a reporter it writes one line to stderr.
|
|
69
|
+
*/
|
|
70
|
+
reportError(error, entry) {
|
|
71
|
+
try {
|
|
72
|
+
if (this.errorReporter) {
|
|
73
|
+
this.errorReporter(error, entry);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
77
|
+
process.stderr.write(`[@warlock.js/logger] channel "${this.name}" failed: ${reason}\n`);
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
59
81
|
* Get config value
|
|
60
82
|
*/
|
|
61
83
|
config(key) {
|
|
@@ -112,7 +134,7 @@ var LogChannel = class {
|
|
|
112
134
|
};
|
|
113
135
|
|
|
114
136
|
//#endregion
|
|
115
|
-
//#region
|
|
137
|
+
//#region ../../packages/logger/src/channels/console-log.ts
|
|
116
138
|
/**
|
|
117
139
|
* The longest level name (`success`) sets the tag column width. Padding every
|
|
118
140
|
* level name to this width keeps the timestamp / module / action columns
|
|
@@ -195,7 +217,53 @@ var ConsoleLog = class extends LogChannel {
|
|
|
195
217
|
};
|
|
196
218
|
|
|
197
219
|
//#endregion
|
|
198
|
-
//#region
|
|
220
|
+
//#region ../../packages/logger/src/channels/file-name-pattern.ts
|
|
221
|
+
const TOKEN = /\{(YYYY|MM|DD|HH|mm|name)\}/g;
|
|
222
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
223
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
224
|
+
/**
|
|
225
|
+
* Render a file name option for the given date.
|
|
226
|
+
*/
|
|
227
|
+
function renderFileName(option, date, name) {
|
|
228
|
+
if (typeof option === "function") return option(date);
|
|
229
|
+
return option.replace(TOKEN, (_match, token) => {
|
|
230
|
+
switch (token) {
|
|
231
|
+
case "YYYY": return String(date.getFullYear());
|
|
232
|
+
case "MM": return pad(date.getMonth() + 1);
|
|
233
|
+
case "DD": return pad(date.getDate());
|
|
234
|
+
case "HH": return pad(date.getHours());
|
|
235
|
+
case "mm": return pad(date.getMinutes());
|
|
236
|
+
default: return name;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Regular-expression source matching every name a pattern string can render:
|
|
242
|
+
* literal text is escaped, date tokens become digit runs and `{name}` the
|
|
243
|
+
* escaped channel name. The source is not anchored.
|
|
244
|
+
*/
|
|
245
|
+
function fileNamePatternSource(pattern, name) {
|
|
246
|
+
let source = "";
|
|
247
|
+
let lastIndex = 0;
|
|
248
|
+
for (const match of pattern.matchAll(TOKEN)) {
|
|
249
|
+
const index = match.index ?? 0;
|
|
250
|
+
const token = match[1];
|
|
251
|
+
source += escapeRegExp(pattern.slice(lastIndex, index));
|
|
252
|
+
source += token === "YYYY" ? "\\d{4}" : token === "name" ? escapeRegExp(name) : "\\d{2}";
|
|
253
|
+
lastIndex = index + match[0].length;
|
|
254
|
+
}
|
|
255
|
+
return source + escapeRegExp(pattern.slice(lastIndex));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region ../../packages/logger/src/channels/file-log.ts
|
|
260
|
+
/** Default file name of a `daily` chunk: sortable ISO date. */
|
|
261
|
+
const DAILY_FILE_NAME = "{YYYY}-{MM}-{DD}";
|
|
262
|
+
/** Default file name of an `hourly` chunk: sortable ISO date and hour. */
|
|
263
|
+
const HOURLY_FILE_NAME = "{YYYY}-{MM}-{DD}-{HH}";
|
|
264
|
+
/** Default date part of a size-rotated file's name. */
|
|
265
|
+
const ROTATE_FILE_NAME = "{YYYY}-{MM}-{DD}";
|
|
266
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
199
267
|
var FileLog = class extends LogChannel {
|
|
200
268
|
constructor(..._args) {
|
|
201
269
|
super(..._args);
|
|
@@ -211,9 +279,7 @@ var FileLog = class extends LogChannel {
|
|
|
211
279
|
maxMessagesToWrite: 100,
|
|
212
280
|
filter: () => true,
|
|
213
281
|
maxFileSize: 10 * 1024 * 1024,
|
|
214
|
-
|
|
215
|
-
return (0, dayjs.default)().format("DD-MM-YYYY");
|
|
216
|
-
},
|
|
282
|
+
rotateFileName: ROTATE_FILE_NAME,
|
|
217
283
|
dateFormat: {
|
|
218
284
|
date: "DD-MM-YYYY",
|
|
219
285
|
time: "HH:mm:ss"
|
|
@@ -221,6 +287,15 @@ var FileLog = class extends LogChannel {
|
|
|
221
287
|
};
|
|
222
288
|
this.lastWriteTime = Date.now();
|
|
223
289
|
this.isWriting = false;
|
|
290
|
+
this.retentionDue = false;
|
|
291
|
+
this.missingMatchReported = false;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* {@inheritdoc}
|
|
295
|
+
*/
|
|
296
|
+
setConfigurations(configurations) {
|
|
297
|
+
assertRetention(configurations.retention);
|
|
298
|
+
return super.setConfigurations(configurations);
|
|
224
299
|
}
|
|
225
300
|
/**
|
|
226
301
|
* Check file size for file rotation
|
|
@@ -228,19 +303,24 @@ var FileLog = class extends LogChannel {
|
|
|
228
303
|
async checkAndRotateFile(filePath = this.filePath) {
|
|
229
304
|
if (!this.config("rotate")) return;
|
|
230
305
|
try {
|
|
231
|
-
if ((await fs.default.promises.stat(filePath)).size >= this.config("maxFileSize")) await this.rotateLogFile();
|
|
306
|
+
if ((await fs.default.promises.stat(filePath)).size >= this.config("maxFileSize")) await this.rotateLogFile(filePath);
|
|
232
307
|
} catch (error) {
|
|
233
308
|
if (error.code !== "ENOENT") console.error("Error checking log file:", error);
|
|
234
309
|
}
|
|
235
310
|
}
|
|
236
311
|
/**
|
|
237
|
-
* Rotate log file
|
|
312
|
+
* Rotate the given log file (the active one by default) aside as
|
|
313
|
+
* `<file name>-<rotateFileName>-<epoch ms>.<extension>` in its directory.
|
|
238
314
|
*/
|
|
239
|
-
async rotateLogFile() {
|
|
240
|
-
const
|
|
315
|
+
async rotateLogFile(filePath = this.filePath) {
|
|
316
|
+
const now = /* @__PURE__ */ new Date();
|
|
241
317
|
const extension = this.extension;
|
|
242
|
-
const
|
|
243
|
-
|
|
318
|
+
const baseName = path.default.basename(filePath, `.${extension}`);
|
|
319
|
+
const rotateName = renderFileName(this.config("rotateFileName") ?? "{YYYY}-{MM}-{DD}", now, this.baseName);
|
|
320
|
+
const rotatedFilePath = path.default.join(path.default.dirname(filePath), `${baseName}-${rotateName}-${now.getTime()}.${extension}`);
|
|
321
|
+
await fs.default.promises.rename(filePath, rotatedFilePath).then(() => {
|
|
322
|
+
this.retentionDue = true;
|
|
323
|
+
}, (error) => {
|
|
244
324
|
console.error("Error rotating file:", error);
|
|
245
325
|
});
|
|
246
326
|
}
|
|
@@ -286,17 +366,32 @@ var FileLog = class extends LogChannel {
|
|
|
286
366
|
return this.config("maxMessagesToWrite");
|
|
287
367
|
}
|
|
288
368
|
/**
|
|
289
|
-
*
|
|
369
|
+
* The configured `name` option.
|
|
290
370
|
*/
|
|
291
|
-
get
|
|
371
|
+
get baseName() {
|
|
372
|
+
return this.config("name") ?? "app";
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The `fileName` option, or the chunk's default. `undefined` means the
|
|
376
|
+
* plain `name` of a `single` chunk, used as is (braces included).
|
|
377
|
+
*/
|
|
378
|
+
get fileNamePattern() {
|
|
379
|
+
const configured = this.config("fileName");
|
|
380
|
+
if (configured !== void 0) return configured;
|
|
292
381
|
switch (this.config("chunk")) {
|
|
293
|
-
case "
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
case "hourly": return (0, dayjs.default)().format("DD-MM-YYYY-HH-00-00-a");
|
|
382
|
+
case "daily": return DAILY_FILE_NAME;
|
|
383
|
+
case "hourly": return HOURLY_FILE_NAME;
|
|
384
|
+
default: return;
|
|
297
385
|
}
|
|
298
386
|
}
|
|
299
387
|
/**
|
|
388
|
+
* Current file name, without extension
|
|
389
|
+
*/
|
|
390
|
+
get fileName() {
|
|
391
|
+
const pattern = this.fileNamePattern;
|
|
392
|
+
return pattern === void 0 ? this.baseName : renderFileName(pattern, /* @__PURE__ */ new Date(), this.baseName);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
300
395
|
* Get file extension
|
|
301
396
|
*/
|
|
302
397
|
get extension() {
|
|
@@ -321,6 +416,138 @@ var FileLog = class extends LogChannel {
|
|
|
321
416
|
const logsDirectory = this.storagePath;
|
|
322
417
|
await (0, _warlock_js_fs.ensureDirectoryAsync)(logsDirectory);
|
|
323
418
|
this.initMessageFlush();
|
|
419
|
+
await this.prune();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Remove old log files as configured by `retention`. Runs on boot and
|
|
423
|
+
* whenever a new file is started; call it yourself to prune on demand.
|
|
424
|
+
* Never rejects: failures go to the channel error handler.
|
|
425
|
+
*/
|
|
426
|
+
prune() {
|
|
427
|
+
const run = (this.pruning ?? Promise.resolve()).then(() => this.pruneFiles());
|
|
428
|
+
this.pruning = run;
|
|
429
|
+
return run;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Prune once a write started a new file: after a size rotation, or when a
|
|
433
|
+
* dated file name rolled over. Never throws.
|
|
434
|
+
*/
|
|
435
|
+
async afterWrite() {
|
|
436
|
+
let fileName;
|
|
437
|
+
try {
|
|
438
|
+
fileName = this.fileName;
|
|
439
|
+
} catch {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const rolledOver = this.lastFileName !== void 0 && this.lastFileName !== fileName;
|
|
443
|
+
this.lastFileName = fileName;
|
|
444
|
+
if (!rolledOver && !this.retentionDue) return;
|
|
445
|
+
this.retentionDue = false;
|
|
446
|
+
await this.prune();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Apply `retention` to every directory this channel writes to.
|
|
450
|
+
*/
|
|
451
|
+
async pruneFiles() {
|
|
452
|
+
const retention = this.config("retention");
|
|
453
|
+
if (!retention || retention.maxFiles === void 0 && retention.maxAgeDays === void 0) return;
|
|
454
|
+
try {
|
|
455
|
+
const matcher = this.retentionMatcher(retention);
|
|
456
|
+
if (!matcher) return;
|
|
457
|
+
const activeFile = `${this.fileName}.${this.extension}`;
|
|
458
|
+
for (const directory of await this.retentionDirectories()) await this.pruneDirectory(directory, matcher, retention, activeFile);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.reportRetentionError(error, this.storagePath);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The expression a file name must match to be pruned, or `undefined` when
|
|
465
|
+
* none can be derived (a function `fileName` without `retention.match`).
|
|
466
|
+
*/
|
|
467
|
+
retentionMatcher(retention) {
|
|
468
|
+
if (retention.match) return new RegExp(retention.match.source, retention.match.flags.replace(/[gy]/g, ""));
|
|
469
|
+
const pattern = this.fileNamePattern;
|
|
470
|
+
if (typeof pattern === "function") {
|
|
471
|
+
if (!this.missingMatchReported) {
|
|
472
|
+
this.missingMatchReported = true;
|
|
473
|
+
this.reportRetentionError(/* @__PURE__ */ new Error("FileLog retention needs `retention.match` when `fileName` is a function; no files were pruned"), this.storagePath);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const name = this.baseName;
|
|
478
|
+
const fileSource = pattern === void 0 ? escapeRegExp(name) : fileNamePatternSource(pattern, name);
|
|
479
|
+
const rotateName = this.config("rotateFileName") ?? "{YYYY}-{MM}-{DD}";
|
|
480
|
+
const rotateSource = typeof rotateName === "function" ? ".+" : fileNamePatternSource(rotateName, name);
|
|
481
|
+
return new RegExp(`^${fileSource}(?:-${rotateSource}-\\d{13,})?\\.${escapeRegExp(this.extension)}$`);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The log directory, or each group directory when `groupBy` is set.
|
|
485
|
+
*/
|
|
486
|
+
async retentionDirectories() {
|
|
487
|
+
let directories = [this.storagePath];
|
|
488
|
+
const depth = this.config("groupBy")?.length ?? 0;
|
|
489
|
+
for (let level = 0; level < depth; level++) {
|
|
490
|
+
const next = [];
|
|
491
|
+
for (const directory of directories) {
|
|
492
|
+
const entries = await fs.default.promises.readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
493
|
+
if (error.code !== "ENOENT") this.reportRetentionError(error, directory);
|
|
494
|
+
return [];
|
|
495
|
+
});
|
|
496
|
+
for (const entry of entries) if (entry.isDirectory()) next.push(path.default.join(directory, entry.name));
|
|
497
|
+
}
|
|
498
|
+
directories = next;
|
|
499
|
+
}
|
|
500
|
+
return directories;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Remove the matching files of one directory that `retention` no longer
|
|
504
|
+
* keeps. Each failure is reported and the rest still run.
|
|
505
|
+
*/
|
|
506
|
+
async pruneDirectory(directory, matcher, retention, activeFile) {
|
|
507
|
+
let names;
|
|
508
|
+
try {
|
|
509
|
+
names = await fs.default.promises.readdir(directory);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error.code !== "ENOENT") this.reportRetentionError(error, directory);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const candidates = [];
|
|
515
|
+
for (const name of names) {
|
|
516
|
+
if (name === activeFile || !matcher.test(name)) continue;
|
|
517
|
+
const filePath = path.default.join(directory, name);
|
|
518
|
+
try {
|
|
519
|
+
const stats = await fs.default.promises.stat(filePath);
|
|
520
|
+
if (stats.isFile()) candidates.push({
|
|
521
|
+
filePath,
|
|
522
|
+
name,
|
|
523
|
+
modifiedAt: stats.mtimeMs
|
|
524
|
+
});
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (error.code !== "ENOENT") this.reportRetentionError(error, filePath);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
candidates.sort((a, b) => b.modifiedAt - a.modifiedAt || (a.name < b.name ? 1 : -1));
|
|
530
|
+
const keep = retention.maxFiles === void 0 ? Infinity : retention.maxFiles - (names.includes(activeFile) ? 1 : 0);
|
|
531
|
+
const oldestKept = retention.maxAgeDays === void 0 ? -Infinity : Date.now() - retention.maxAgeDays * DAY_MS;
|
|
532
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
533
|
+
if (index < keep && candidate.modifiedAt >= oldestKept) continue;
|
|
534
|
+
try {
|
|
535
|
+
await fs.default.promises.unlink(candidate.filePath);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error.code !== "ENOENT") this.reportRetentionError(error, candidate.filePath);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Send a retention failure to the channel error handler.
|
|
543
|
+
*/
|
|
544
|
+
reportRetentionError(error, target) {
|
|
545
|
+
this.reportError(error, {
|
|
546
|
+
type: "error",
|
|
547
|
+
module: "logger",
|
|
548
|
+
action: "retention",
|
|
549
|
+
message: `Log retention failed for ${target}`
|
|
550
|
+
});
|
|
324
551
|
}
|
|
325
552
|
/**
|
|
326
553
|
* Synchronously flush messages
|
|
@@ -412,14 +639,17 @@ var FileLog = class extends LogChannel {
|
|
|
412
639
|
if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;
|
|
413
640
|
this.isWriting = true;
|
|
414
641
|
if (this.messagedShouldBeGrouped) return await this.writeGroupedMessagesToFile();
|
|
415
|
-
await this.checkAndRotateFile();
|
|
416
642
|
try {
|
|
417
|
-
|
|
643
|
+
const filePath = this.filePath;
|
|
644
|
+
await this.checkAndRotateFile(filePath);
|
|
645
|
+
await this.write(filePath, this.content);
|
|
418
646
|
this.onSave();
|
|
419
647
|
} catch (error) {
|
|
420
648
|
console.error("Failed to write log:", error);
|
|
421
649
|
this.isWriting = false;
|
|
650
|
+
return;
|
|
422
651
|
}
|
|
652
|
+
await this.afterWrite();
|
|
423
653
|
}
|
|
424
654
|
/**
|
|
425
655
|
* Write grouped messages to the file
|
|
@@ -428,11 +658,11 @@ var FileLog = class extends LogChannel {
|
|
|
428
658
|
this.prepareGroupedMessages();
|
|
429
659
|
for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {
|
|
430
660
|
const directoryPath = path.default.join(this.storagePath, key);
|
|
431
|
-
await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
|
|
432
|
-
const filePath = path.default.join(directoryPath, `${this.fileName}.${this.extension}`);
|
|
433
|
-
await this.checkAndRotateFile(filePath);
|
|
434
|
-
const content = groupMessages.map((message) => message.content).join(os.EOL) + os.EOL;
|
|
435
661
|
try {
|
|
662
|
+
await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
|
|
663
|
+
const filePath = path.default.join(directoryPath, `${this.fileName}.${this.extension}`);
|
|
664
|
+
await this.checkAndRotateFile(filePath);
|
|
665
|
+
const content = groupMessages.map((message) => message.content).join(os.EOL) + os.EOL;
|
|
436
666
|
await this.write(filePath, content);
|
|
437
667
|
} catch (error) {
|
|
438
668
|
console.error("Failed to write log:", error);
|
|
@@ -440,6 +670,7 @@ var FileLog = class extends LogChannel {
|
|
|
440
670
|
}
|
|
441
671
|
this.onSave();
|
|
442
672
|
this.isWriting = false;
|
|
673
|
+
await this.afterWrite();
|
|
443
674
|
}
|
|
444
675
|
/**
|
|
445
676
|
* Prepare grouped messages
|
|
@@ -465,9 +696,19 @@ var FileLog = class extends LogChannel {
|
|
|
465
696
|
});
|
|
466
697
|
}
|
|
467
698
|
};
|
|
699
|
+
/**
|
|
700
|
+
* Reject a `retention` that could never keep or remove anything sensibly.
|
|
701
|
+
*/
|
|
702
|
+
function assertRetention(retention) {
|
|
703
|
+
if (!retention) return;
|
|
704
|
+
const { maxFiles, maxAgeDays, match } = retention;
|
|
705
|
+
if (maxFiles !== void 0 && (!Number.isInteger(maxFiles) || maxFiles < 1)) throw new TypeError("FileLog retention.maxFiles must be a positive integer");
|
|
706
|
+
if (maxAgeDays !== void 0 && (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0)) throw new TypeError("FileLog retention.maxAgeDays must be a positive number");
|
|
707
|
+
if (match !== void 0 && !(match instanceof RegExp)) throw new TypeError("FileLog retention.match must be a RegExp");
|
|
708
|
+
}
|
|
468
709
|
|
|
469
710
|
//#endregion
|
|
470
|
-
//#region
|
|
711
|
+
//#region ../../packages/logger/src/utils/safe-json-stringify.ts
|
|
471
712
|
/**
|
|
472
713
|
* Replacer that surfaces Error data — `name`, `message`, and `stack` are
|
|
473
714
|
* non-enumerable on `Error`, so neither default JSON serialization nor an
|
|
@@ -498,7 +739,7 @@ function safeJsonStringify(value, space) {
|
|
|
498
739
|
}
|
|
499
740
|
|
|
500
741
|
//#endregion
|
|
501
|
-
//#region
|
|
742
|
+
//#region ../../packages/logger/src/channels/json-file-log.ts
|
|
502
743
|
var JSONFileLog = class extends FileLog {
|
|
503
744
|
constructor(..._args) {
|
|
504
745
|
super(..._args);
|
|
@@ -577,10 +818,18 @@ var JSONFileLog = class extends FileLog {
|
|
|
577
818
|
if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;
|
|
578
819
|
this.isWriting = true;
|
|
579
820
|
if (this.messagedShouldBeGrouped) return await this.writeGroupedMessagesToFile();
|
|
580
|
-
|
|
821
|
+
let filePath;
|
|
822
|
+
try {
|
|
823
|
+
filePath = this.filePath;
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.error("Failed to write log:", error);
|
|
826
|
+
this.isWriting = false;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
await this.checkAndRotateFile(filePath);
|
|
581
830
|
let fileContents = { messages: [] };
|
|
582
|
-
if (await (0, _warlock_js_fs.fileExistsAsync)(
|
|
583
|
-
fileContents = await (0, _warlock_js_fs.getJsonFileAsync)(
|
|
831
|
+
if (await (0, _warlock_js_fs.fileExistsAsync)(filePath)) try {
|
|
832
|
+
fileContents = await (0, _warlock_js_fs.getJsonFileAsync)(filePath);
|
|
584
833
|
} catch (error) {
|
|
585
834
|
console.error("Error reading log file, reinitializing:", error);
|
|
586
835
|
fileContents = { messages: [] };
|
|
@@ -588,22 +837,32 @@ var JSONFileLog = class extends FileLog {
|
|
|
588
837
|
else fileContents = { messages: [] };
|
|
589
838
|
fileContents.messages.push(...this.messages);
|
|
590
839
|
try {
|
|
591
|
-
await fs.default.promises.writeFile(
|
|
840
|
+
await fs.default.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));
|
|
592
841
|
this.onSave();
|
|
593
842
|
} catch (error) {
|
|
594
843
|
console.error("Failed to write log:", error);
|
|
595
844
|
this.isWriting = false;
|
|
845
|
+
return;
|
|
596
846
|
}
|
|
847
|
+
await this.afterWrite();
|
|
597
848
|
}
|
|
598
849
|
/**
|
|
599
850
|
* Write grouped messages to the file
|
|
600
851
|
*/
|
|
601
852
|
async writeGroupedMessagesToFile() {
|
|
853
|
+
let fileName;
|
|
854
|
+
try {
|
|
855
|
+
fileName = this.fileName;
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error("Failed to write log:", error);
|
|
858
|
+
this.isWriting = false;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
602
861
|
this.prepareGroupedMessages();
|
|
603
862
|
for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {
|
|
604
863
|
const directoryPath = path.default.join(this.storagePath, key);
|
|
605
864
|
await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
|
|
606
|
-
const filePath = path.default.join(directoryPath, `${
|
|
865
|
+
const filePath = path.default.join(directoryPath, `${fileName}.${this.extension}`);
|
|
607
866
|
await this.checkAndRotateFile(filePath);
|
|
608
867
|
let fileContents = { messages: [] };
|
|
609
868
|
if (await (0, _warlock_js_fs.fileExistsAsync)(filePath)) try {
|
|
@@ -622,11 +881,12 @@ var JSONFileLog = class extends FileLog {
|
|
|
622
881
|
}
|
|
623
882
|
}
|
|
624
883
|
this.onSave();
|
|
884
|
+
await this.afterWrite();
|
|
625
885
|
}
|
|
626
886
|
};
|
|
627
887
|
|
|
628
888
|
//#endregion
|
|
629
|
-
//#region
|
|
889
|
+
//#region ../../packages/logger/src/channels/sentry-log.ts
|
|
630
890
|
let Sentry;
|
|
631
891
|
let isModuleExists = null;
|
|
632
892
|
let loadingPromise;
|
|
@@ -823,7 +1083,7 @@ var SentryLog = class extends LogChannel {
|
|
|
823
1083
|
};
|
|
824
1084
|
|
|
825
1085
|
//#endregion
|
|
826
|
-
//#region
|
|
1086
|
+
//#region ../../packages/logger/src/redact/default-keys.ts
|
|
827
1087
|
/**
|
|
828
1088
|
* Built-in secret-key denylist — the redaction floor that applies with no
|
|
829
1089
|
* configuration at all.
|
|
@@ -937,7 +1197,7 @@ const DEFAULT_REDACT_KEYS = [
|
|
|
937
1197
|
const DEFAULT_REDACT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeRedactKey));
|
|
938
1198
|
|
|
939
1199
|
//#endregion
|
|
940
|
-
//#region
|
|
1200
|
+
//#region ../../packages/logger/src/redact/redact.ts
|
|
941
1201
|
const DEFAULT_CENSOR = "[REDACTED]";
|
|
942
1202
|
/**
|
|
943
1203
|
* Values we copy by reference rather than walking. Expanding these with
|
|
@@ -1201,7 +1461,110 @@ function mergeRedact(base, extra) {
|
|
|
1201
1461
|
}
|
|
1202
1462
|
|
|
1203
1463
|
//#endregion
|
|
1204
|
-
//#region
|
|
1464
|
+
//#region ../../packages/logger/src/redact/escape-log-value.ts
|
|
1465
|
+
/**
|
|
1466
|
+
* ANSI escape sequences: CSI (`ESC [ … final`) and the two-character
|
|
1467
|
+
* `ESC x` escapes (`ESC c` resets a terminal). Bounded so matching stays linear on long input.
|
|
1468
|
+
*/
|
|
1469
|
+
const ANSI_ESCAPE = /\u001b(?:\[[0-?]{0,32}[ -/]{0,8}[@-~]|[0-~])/g;
|
|
1470
|
+
/** Control characters that are not already handled as `\r`, `\n` or `\t`. */
|
|
1471
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
1472
|
+
/**
|
|
1473
|
+
* Make client-controlled text safe to put inside a log line: line breaks and
|
|
1474
|
+
* tabs become the visible escapes `\r`, `\n`, `\t`, other control characters
|
|
1475
|
+
* (including NUL, DEL, C1 controls and the Unicode line/paragraph separators)
|
|
1476
|
+
* become `\uXXXX`, and ANSI terminal escape sequences are removed.
|
|
1477
|
+
*
|
|
1478
|
+
* Without it, a user agent or form field containing `\n2026-01-01 [info] …`
|
|
1479
|
+
* writes a forged entry into a text log, and an ANSI sequence can rewrite what
|
|
1480
|
+
* a terminal shows. Structured channels (JSON) are not affected, but console
|
|
1481
|
+
* and file channels are.
|
|
1482
|
+
*
|
|
1483
|
+
* @example
|
|
1484
|
+
* log.info("auth", "login", `Login from ${escapeLogValue(request.userAgent)}`);
|
|
1485
|
+
*/
|
|
1486
|
+
function escapeLogValue(value, options = {}) {
|
|
1487
|
+
let text = typeof value === "string" ? value : String(value);
|
|
1488
|
+
const maxLength = options.maxLength;
|
|
1489
|
+
let truncated = false;
|
|
1490
|
+
if (maxLength !== void 0 && maxLength >= 0 && text.length > maxLength) {
|
|
1491
|
+
text = text.slice(0, maxLength);
|
|
1492
|
+
truncated = true;
|
|
1493
|
+
}
|
|
1494
|
+
const escaped = text.replace(ANSI_ESCAPE, "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(CONTROL, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
1495
|
+
return truncated ? `${escaped}…[truncated]` : escaped;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region ../../packages/logger/src/redact/mask-email.ts
|
|
1500
|
+
/**
|
|
1501
|
+
* Email addresses inside free text: a local part and a dotted domain, letters
|
|
1502
|
+
* and digits from any script (`jöhn@exämple.de`). Kept deliberately simple (no
|
|
1503
|
+
* quoted local parts, no IP-literal domains, no percent-encoded `@`): it masks
|
|
1504
|
+
* the addresses that show up in log messages and cache keys, not every string
|
|
1505
|
+
* RFC 5322 allows.
|
|
1506
|
+
*
|
|
1507
|
+
* Every repetition is bounded (local part 64, label 63, at most 12 labels), so
|
|
1508
|
+
* matching stays linear in the input length. Cache keys and messages are often
|
|
1509
|
+
* built from request input; an unbounded pattern backtracks quadratically on a
|
|
1510
|
+
* long run of address-like characters with no domain.
|
|
1511
|
+
*/
|
|
1512
|
+
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;
|
|
1513
|
+
/**
|
|
1514
|
+
* Longest text {@link maskEmailsInText} scans. Anything past it is cut off and
|
|
1515
|
+
* replaced by a marker, so a huge cache key or message cannot cost more than a
|
|
1516
|
+
* bounded amount of time per log line, and nothing past the cut can leak.
|
|
1517
|
+
*/
|
|
1518
|
+
const MASK_EMAILS_MAX_LENGTH = 16384;
|
|
1519
|
+
/**
|
|
1520
|
+
* Mask one email address, keeping its first character and its domain:
|
|
1521
|
+
* `member@example.com` → `m***@example.com`. A value without an `@` is
|
|
1522
|
+
* returned as `***`.
|
|
1523
|
+
*
|
|
1524
|
+
* Key-based redaction cannot reach an address that sits inside a message
|
|
1525
|
+
* string or a cache key; mask it before it is logged.
|
|
1526
|
+
*
|
|
1527
|
+
* @example
|
|
1528
|
+
* log.info("mail", "send", `Sending mail to: ${maskEmail(to)}`);
|
|
1529
|
+
*/
|
|
1530
|
+
function maskEmail(address) {
|
|
1531
|
+
const at = address.lastIndexOf("@");
|
|
1532
|
+
if (at <= 0) return "***";
|
|
1533
|
+
return `${String.fromCodePoint(address.codePointAt(0))}***${address.slice(at)}`;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Mask every email address the pattern above recognises in a piece of text
|
|
1537
|
+
* with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}
|
|
1538
|
+
* characters is truncated first (the cut is marked).
|
|
1539
|
+
*
|
|
1540
|
+
* Dots are valid in the local part of an address, so a dotted prefix that
|
|
1541
|
+
* runs into the address is masked with it: in a cache key such as
|
|
1542
|
+
* `email.member@example.com` the whole `email.member` reads as the local part.
|
|
1543
|
+
*
|
|
1544
|
+
* Not recognised (left as they are): quoted local parts (`"john doe"@x.com`),
|
|
1545
|
+
* IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses
|
|
1546
|
+
* (`user%40example.com`).
|
|
1547
|
+
*
|
|
1548
|
+
* @example
|
|
1549
|
+
* maskEmailsInText("sent to member@example.com") // "sent to m***@example.com"
|
|
1550
|
+
* maskEmailsInText("email.member@example.com") // "e***@example.com"
|
|
1551
|
+
*/
|
|
1552
|
+
function maskEmailsInText(text) {
|
|
1553
|
+
let scanned = text;
|
|
1554
|
+
let marker = "";
|
|
1555
|
+
if (scanned.length > 16384) {
|
|
1556
|
+
let cut = MASK_EMAILS_MAX_LENGTH;
|
|
1557
|
+
const last = scanned.charCodeAt(cut - 1);
|
|
1558
|
+
if (last >= 55296 && last <= 56319) cut -= 1;
|
|
1559
|
+
marker = `…[${scanned.length - cut} chars truncated]`;
|
|
1560
|
+
scanned = scanned.slice(0, cut);
|
|
1561
|
+
}
|
|
1562
|
+
if (!scanned.includes("@")) return scanned + marker;
|
|
1563
|
+
return scanned.replace(EMAIL_IN_TEXT, (match) => maskEmail(match)) + marker;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region ../../packages/logger/src/utils/clear-message.ts
|
|
1205
1568
|
/**
|
|
1206
1569
|
* Clear message from any terminal codes
|
|
1207
1570
|
*/
|
|
@@ -1211,7 +1574,7 @@ function clearMessage(message) {
|
|
|
1211
1574
|
}
|
|
1212
1575
|
|
|
1213
1576
|
//#endregion
|
|
1214
|
-
//#region
|
|
1577
|
+
//#region ../../packages/logger/src/logger.ts
|
|
1215
1578
|
const SIGNAL_EVENTS = new Set([
|
|
1216
1579
|
"SIGINT",
|
|
1217
1580
|
"SIGTERM",
|
|
@@ -1239,22 +1602,39 @@ var Logger = class {
|
|
|
1239
1602
|
this.channels = [];
|
|
1240
1603
|
this.id = "logger-" + _mongez_reinforcements.Random.string(32);
|
|
1241
1604
|
this.autoFlushHandlers = /* @__PURE__ */ new Map();
|
|
1605
|
+
this.pendingDeliveries = /* @__PURE__ */ new Map();
|
|
1606
|
+
this.channelErrorHandler = reportChannelError;
|
|
1242
1607
|
}
|
|
1243
1608
|
/**
|
|
1244
1609
|
* Add a new channel
|
|
1245
1610
|
*/
|
|
1246
1611
|
addChannel(channel) {
|
|
1247
|
-
this.channels.push(channel);
|
|
1612
|
+
this.channels.push(this.attachChannel(channel));
|
|
1248
1613
|
return this;
|
|
1249
1614
|
}
|
|
1250
1615
|
/**
|
|
1251
1616
|
* Set base configurations
|
|
1252
1617
|
*/
|
|
1253
1618
|
configure(config) {
|
|
1254
|
-
if (config.channels) this.
|
|
1619
|
+
if (config.channels) this.setChannels(config.channels);
|
|
1255
1620
|
if (config.autoFlushOn) this.enableAutoFlush(config.autoFlushOn);
|
|
1256
1621
|
if (config.minLevel !== void 0) this.setMinLevel(config.minLevel);
|
|
1257
1622
|
if (config.redact !== void 0) this.setRedact(config.redact);
|
|
1623
|
+
if (config.onChannelError) this.onChannelError(config.onChannelError);
|
|
1624
|
+
return this;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Handle a channel's delivery failure. Logging is fire-and-forget, so a
|
|
1628
|
+
* channel that throws or rejects never reaches the call site and never
|
|
1629
|
+
* becomes an unhandled rejection; it goes to this handler instead. The
|
|
1630
|
+
* default writes one line to stderr. The handler must not log through this
|
|
1631
|
+
* logger (a failing channel would loop).
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* log.onChannelError((error, channel) => metrics.increment(`log.${channel.name}.failed`));
|
|
1635
|
+
*/
|
|
1636
|
+
onChannelError(handler) {
|
|
1637
|
+
this.channelErrorHandler = handler;
|
|
1258
1638
|
return this;
|
|
1259
1639
|
}
|
|
1260
1640
|
/**
|
|
@@ -1303,9 +1683,18 @@ var Logger = class {
|
|
|
1303
1683
|
*/
|
|
1304
1684
|
setChannels(channels) {
|
|
1305
1685
|
this.channels = channels;
|
|
1686
|
+
for (const channel of channels) this.attachChannel(channel);
|
|
1306
1687
|
return this;
|
|
1307
1688
|
}
|
|
1308
1689
|
/**
|
|
1690
|
+
* Route a channel's own failures (outside `log()`, such as file retention)
|
|
1691
|
+
* to this logger's channel error handler.
|
|
1692
|
+
*/
|
|
1693
|
+
attachChannel(channel) {
|
|
1694
|
+
if (typeof channel.setErrorReporter === "function") channel.setErrorReporter((error, entry) => this.reportChannelError(error, channel, entry));
|
|
1695
|
+
return channel;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1309
1698
|
* Normalize log data to a single object
|
|
1310
1699
|
*/
|
|
1311
1700
|
normalizeLogData(dataOrModule, action, message = "", level, context) {
|
|
@@ -1332,21 +1721,62 @@ var Logger = class {
|
|
|
1332
1721
|
* — each channel sees its own shallow clone so one channel cannot observe
|
|
1333
1722
|
* another's mutations (e.g. a later terminal channel still sees the original
|
|
1334
1723
|
* colored message).
|
|
1724
|
+
*
|
|
1725
|
+
* Synchronous and fire-and-forget: it returns the logger, not a promise.
|
|
1726
|
+
* Async channel deliveries are tracked and awaited by {@link flush}; a
|
|
1727
|
+
* channel that throws or rejects is isolated from the others and reported
|
|
1728
|
+
* to {@link onChannelError}.
|
|
1335
1729
|
*/
|
|
1336
|
-
|
|
1730
|
+
log(data) {
|
|
1337
1731
|
if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) return this;
|
|
1338
|
-
|
|
1339
|
-
|
|
1732
|
+
let baseEntry;
|
|
1733
|
+
try {
|
|
1734
|
+
baseEntry = applyRedact(data, this.redactConfig);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
for (const channel of this.channels) this.reportChannelError(error, channel, redactionFailedEntry(data));
|
|
1737
|
+
return this;
|
|
1738
|
+
}
|
|
1739
|
+
for (const channel of this.channels) this.deliver(channel, data, baseEntry);
|
|
1740
|
+
return this;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Prepare and hand one entry to one channel, isolating its failure (a
|
|
1744
|
+
* channel redaction that throws included) and tracking an async delivery
|
|
1745
|
+
* until it settles.
|
|
1746
|
+
*/
|
|
1747
|
+
deliver(channel, data, baseEntry) {
|
|
1748
|
+
let payload = baseEntry;
|
|
1749
|
+
let result;
|
|
1750
|
+
try {
|
|
1340
1751
|
const channelRedact = channel.getRedactConfig?.();
|
|
1341
|
-
|
|
1342
|
-
let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;
|
|
1752
|
+
if (channelRedact) payload = applyRedact(data, mergeRedact(this.redactConfig, channelRedact));
|
|
1343
1753
|
if (channel.terminal === false) payload = {
|
|
1344
1754
|
...payload,
|
|
1345
1755
|
message: clearMessage(payload.message)
|
|
1346
1756
|
};
|
|
1347
|
-
channel.log(payload);
|
|
1757
|
+
result = channel.log(payload);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
this.reportChannelError(error, channel, payload);
|
|
1760
|
+
return;
|
|
1348
1761
|
}
|
|
1349
|
-
return
|
|
1762
|
+
if (!result || typeof result.then !== "function") return;
|
|
1763
|
+
let pending = this.pendingDeliveries.get(channel);
|
|
1764
|
+
if (!pending) {
|
|
1765
|
+
pending = /* @__PURE__ */ new Set();
|
|
1766
|
+
this.pendingDeliveries.set(channel, pending);
|
|
1767
|
+
}
|
|
1768
|
+
const tracked = pending;
|
|
1769
|
+
const delivery = Promise.resolve(result).catch((error) => this.reportChannelError(error, channel, payload)).finally(() => {
|
|
1770
|
+
tracked.delete(delivery);
|
|
1771
|
+
if (tracked.size === 0 && this.pendingDeliveries.get(channel) === tracked) this.pendingDeliveries.delete(channel);
|
|
1772
|
+
});
|
|
1773
|
+
tracked.add(delivery);
|
|
1774
|
+
}
|
|
1775
|
+
reportChannelError(error, channel, entry) {
|
|
1776
|
+
try {
|
|
1777
|
+
const outcome = this.channelErrorHandler(error, channel, entry);
|
|
1778
|
+
if (outcome && typeof outcome.then === "function") outcome.then(void 0, () => {});
|
|
1779
|
+
} catch {}
|
|
1350
1780
|
}
|
|
1351
1781
|
/**
|
|
1352
1782
|
* Make debug log
|
|
@@ -1442,7 +1872,9 @@ var Logger = class {
|
|
|
1442
1872
|
for (const channel of this.channels) if (channel.flushSync) channel.flushSync();
|
|
1443
1873
|
}
|
|
1444
1874
|
/**
|
|
1445
|
-
*
|
|
1875
|
+
* Wait for every in-flight async delivery, then drain every channel that
|
|
1876
|
+
* implements `flush()` — bounded by `timeout` (default
|
|
1877
|
+
* {@link DEFAULT_FLUSH_TIMEOUT_MS}).
|
|
1446
1878
|
*
|
|
1447
1879
|
* Unlike {@link flushSync}, this awaits each channel's async I/O — the
|
|
1448
1880
|
* correct call for a graceful shutdown that can afford to wait
|
|
@@ -1450,24 +1882,68 @@ var Logger = class {
|
|
|
1450
1882
|
* `process.exit`). A channel whose delivery is async (a network transport,
|
|
1451
1883
|
* an async disk write) implements `flush()`, not `flushSync()`.
|
|
1452
1884
|
*
|
|
1453
|
-
* Channels
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1885
|
+
* Channels drain concurrently and independently: each waits for its own
|
|
1886
|
+
* pending deliveries (including ones logged while the flush runs), then
|
|
1887
|
+
* runs its `flush()`. A channel whose delivery hangs therefore cannot hold
|
|
1888
|
+
* up another channel's drain, and a channel whose flush rejects can neither
|
|
1889
|
+
* stop the others nor escape as an unhandled rejection. When `timeout`
|
|
1890
|
+
* elapses, `flush()` resolves anyway and each channel still draining is
|
|
1891
|
+
* reported to {@link onChannelError}. Channels without `flush()` only wait
|
|
1892
|
+
* for their deliveries.
|
|
1456
1893
|
*
|
|
1457
1894
|
* @example
|
|
1458
1895
|
* async function shutdown() {
|
|
1459
1896
|
* await httpServer.close();
|
|
1460
|
-
* await log.flush();
|
|
1897
|
+
* await log.flush({ timeout: 3000 });
|
|
1461
1898
|
* process.exit(0);
|
|
1462
1899
|
* }
|
|
1463
1900
|
*/
|
|
1464
|
-
async flush() {
|
|
1465
|
-
|
|
1466
|
-
|
|
1901
|
+
async flush(options = {}) {
|
|
1902
|
+
const timeout = options.timeout ?? 5e3;
|
|
1903
|
+
const draining = new Set(this.channels);
|
|
1904
|
+
for (const channel of this.pendingDeliveries.keys()) draining.add(channel);
|
|
1905
|
+
const unfinished = new Set(draining);
|
|
1906
|
+
const drains = [...draining].map(async (channel) => {
|
|
1467
1907
|
try {
|
|
1468
|
-
await
|
|
1469
|
-
} catch {}
|
|
1470
|
-
|
|
1908
|
+
await this.drainChannel(channel);
|
|
1909
|
+
} catch {} finally {
|
|
1910
|
+
unfinished.delete(channel);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
const settled = Promise.allSettled(drains).then(() => false);
|
|
1914
|
+
if (!Number.isFinite(timeout)) {
|
|
1915
|
+
await settled;
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
let resolveTimeout;
|
|
1919
|
+
const timeoutReached = new Promise((resolve) => {
|
|
1920
|
+
resolveTimeout = resolve;
|
|
1921
|
+
});
|
|
1922
|
+
const timer = setTimeout(() => resolveTimeout(true), Math.max(0, timeout));
|
|
1923
|
+
timer.unref?.();
|
|
1924
|
+
const timedOut = await Promise.race([settled, timeoutReached]);
|
|
1925
|
+
clearTimeout(timer);
|
|
1926
|
+
if (!timedOut) return;
|
|
1927
|
+
for (const channel of unfinished) this.reportChannelError(/* @__PURE__ */ new Error(`flush timed out after ${timeout}ms; entries still in flight may be lost`), channel, {
|
|
1928
|
+
type: "warn",
|
|
1929
|
+
module: "logger",
|
|
1930
|
+
action: "flush",
|
|
1931
|
+
message: "flush timed out"
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Wait until the channel has no delivery in flight, run its `flush()`, and
|
|
1936
|
+
* repeat while that flush let new deliveries in.
|
|
1937
|
+
*/
|
|
1938
|
+
async drainChannel(channel) {
|
|
1939
|
+
do {
|
|
1940
|
+
let pending = this.pendingDeliveries.get(channel);
|
|
1941
|
+
while (pending && pending.size > 0) {
|
|
1942
|
+
await Promise.allSettled([...pending]);
|
|
1943
|
+
pending = this.pendingDeliveries.get(channel);
|
|
1944
|
+
}
|
|
1945
|
+
if (channel.flush) await channel.flush();
|
|
1946
|
+
} while ((this.pendingDeliveries.get(channel)?.size ?? 0) > 0);
|
|
1471
1947
|
}
|
|
1472
1948
|
/**
|
|
1473
1949
|
* Register one process-level handler per event that calls `flushSync()`
|
|
@@ -1513,6 +1989,34 @@ var Logger = class {
|
|
|
1513
1989
|
}
|
|
1514
1990
|
};
|
|
1515
1991
|
/**
|
|
1992
|
+
* Default `timeout` of {@link Logger.flush}: long enough for a healthy network
|
|
1993
|
+
* channel, short enough that a hung one cannot eat a shutdown budget.
|
|
1994
|
+
*/
|
|
1995
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1996
|
+
/**
|
|
1997
|
+
* What the error handler receives when the logger-wide redaction itself
|
|
1998
|
+
* failed: the entry's identity only, never the unredacted message or context.
|
|
1999
|
+
*/
|
|
2000
|
+
function redactionFailedEntry(data) {
|
|
2001
|
+
return {
|
|
2002
|
+
type: data.type,
|
|
2003
|
+
module: data.module,
|
|
2004
|
+
action: data.action,
|
|
2005
|
+
message: "[entry dropped: redaction failed]"
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Default {@link ChannelErrorHandler}: one stderr line per failure. Written
|
|
2010
|
+
* directly to the stream, never through a logger, so a broken channel cannot
|
|
2011
|
+
* recurse.
|
|
2012
|
+
*/
|
|
2013
|
+
function reportChannelError(error, channel) {
|
|
2014
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2015
|
+
try {
|
|
2016
|
+
process.stderr.write(`[@warlock.js/logger] channel "${channel.name}" failed to log: ${reason}\n`);
|
|
2017
|
+
} catch {}
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
1516
2020
|
* The package singleton. Use this for everyday logging — `log.info(...)`,
|
|
1517
2021
|
* `log.error(...)`, `log.configure(...)`. Custom logger instances can be
|
|
1518
2022
|
* created by instantiating `Logger` directly.
|
|
@@ -1529,7 +2033,7 @@ var Logger = class {
|
|
|
1529
2033
|
const log = new Logger();
|
|
1530
2034
|
|
|
1531
2035
|
//#endregion
|
|
1532
|
-
//#region
|
|
2036
|
+
//#region ../../packages/logger/src/utils/capture-unhandled-errors.ts
|
|
1533
2037
|
/**
|
|
1534
2038
|
* Best-effort budget (ms) for draining async channels (file, Sentry, …) before
|
|
1535
2039
|
* the forced exit. Bounded so a single stuck channel can't hang the process —
|
|
@@ -1579,17 +2083,25 @@ function captureAnyUnhandledRejection(options = {}) {
|
|
|
1579
2083
|
|
|
1580
2084
|
//#endregion
|
|
1581
2085
|
exports.ConsoleLog = ConsoleLog;
|
|
2086
|
+
exports.DAILY_FILE_NAME = DAILY_FILE_NAME;
|
|
2087
|
+
exports.DEFAULT_FLUSH_TIMEOUT_MS = DEFAULT_FLUSH_TIMEOUT_MS;
|
|
1582
2088
|
exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
|
|
1583
2089
|
exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
|
|
1584
2090
|
exports.FileLog = FileLog;
|
|
2091
|
+
exports.HOURLY_FILE_NAME = HOURLY_FILE_NAME;
|
|
1585
2092
|
exports.JSONFileLog = JSONFileLog;
|
|
1586
2093
|
exports.LogChannel = LogChannel;
|
|
1587
2094
|
exports.Logger = Logger;
|
|
2095
|
+
exports.MASK_EMAILS_MAX_LENGTH = MASK_EMAILS_MAX_LENGTH;
|
|
2096
|
+
exports.ROTATE_FILE_NAME = ROTATE_FILE_NAME;
|
|
1588
2097
|
exports.SentryLog = SentryLog;
|
|
1589
2098
|
exports.applyRedact = applyRedact;
|
|
1590
2099
|
exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
|
|
1591
2100
|
exports.clearMessage = clearMessage;
|
|
2101
|
+
exports.escapeLogValue = escapeLogValue;
|
|
1592
2102
|
exports.log = log;
|
|
2103
|
+
exports.maskEmail = maskEmail;
|
|
2104
|
+
exports.maskEmailsInText = maskEmailsInText;
|
|
1593
2105
|
exports.mergeRedact = mergeRedact;
|
|
1594
2106
|
exports.normalizeRedactKey = normalizeRedactKey;
|
|
1595
2107
|
exports.resolveRedactKeys = resolveRedactKeys;
|