@warlock.js/logger 5.30.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 +26 -5
- package/README.md +8 -11
- package/cjs/index.cjs +336 -28
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.d.mts.map +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +128 -12
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +203 -22
- 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.map +1 -1
- package/esm/channels/json-file-log.mjs +24 -5
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.d.mts.map +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/index.d.mts +5 -3
- package/esm/index.mjs +3 -2
- package/esm/log-channel.d.mts +23 -1
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +22 -0
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +5 -0
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +11 -2
- package/esm/logger.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 +1 -0
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +3 -2
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +241 -141
- package/llms.txt +2 -2
- package/package.json +2 -2
- 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 +32 -13
- package/skills/index.md +4 -1
- package/skills/logger-basics/SKILL.md +18 -21
- 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 +50 -17
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,35 @@ 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.31.0 - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Upgrading
|
|
10
|
+
|
|
11
|
+
- **BREAKING:** `FileLog`/`JSONFileLog` dated file names are ISO dates that sort by name: `chunk: "daily"` writes `YYYY-MM-DD.log` (was `DD-MM-YYYY.log`) and `chunk: "hourly"` writes `YYYY-MM-DD-HH.log` (was `DD-MM-YYYY-HH-00-00-am.log`). Size-rotated archives default to `<file name>-YYYY-MM-DD-<epoch ms>`. After upgrading, a running daily channel starts a new file under the new name. To keep the old names, set `fileName: "{DD}-{MM}-{YYYY}"` (daily) and `rotateFileName: "{DD}-{MM}-{YYYY}"`. Tools that find logs by name (shippers, cron cleanups) need the new pattern; `retention` does not remove files with the old names.
|
|
12
|
+
- `rotateFileName` is now a `{YYYY}`/`{MM}`/`{DD}`/`{HH}`/`{mm}`/`{name}` pattern or a function of the rotation date, not a Day.js format. A value without tokens is used as is, as before; a Day.js format such as `"DD-MM-YYYY"` was always written literally and still is — use `"{DD}-{MM}-{YYYY}"`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `FileLog` `fileName` option: a pattern (`"app-{YYYY}-{MM}-{DD}"`, tokens `{YYYY}` `{MM}` `{DD}` `{HH}` `{mm}` `{name}` in local time) or a `(date) => string` function, evaluated at every write; it replaces the chunk's default name. `rotateFileName` takes the same forms and is rendered at each rotation (a getter passed in the config was read once, at construction, so every archive kept the boot date). Exported `FileNameOption`, `FileLogRetention`, `DAILY_FILE_NAME`, `HOURLY_FILE_NAME` and `ROTATE_FILE_NAME`.
|
|
17
|
+
- `FileLog` `retention: { maxFiles?, maxAgeDays?, match? }`: removes old log files on boot and whenever the channel starts a new file (a size rotation or a dated name rolling over), plus `channel.prune()` on demand. It only considers files in the log directory (each group directory with `groupBy`) whose names match the channel's file name pattern, keeps the file being written, and reports failures to `onChannelError` instead of throwing. A function `fileName` needs `retention.match`. Invalid `maxFiles`/`maxAgeDays` throw a `TypeError` at construction.
|
|
18
|
+
- `LogChannel.reportError(error, entry)` and `setErrorReporter(reporter)` (type `ChannelErrorReporter`): a channel reports failures outside `log()` to the `onChannelError` handler of the logger it was added to (`addChannel`, `setChannels`, `configure({ channels })`), or one stderr line without one.
|
|
19
|
+
- `escapeLogValue(value, { maxLength? })` makes client-controlled text safe inside a log line: CR/LF/tab become visible `\r`/`\n`/`\t`, other control characters (NUL, DEL, C1, U+2028/U+2029) become `\uXXXX`, ANSI escape sequences are removed, and `maxLength` cuts with a `…[truncated]` marker. A user agent or form field can no longer forge entries in console or file logs.
|
|
20
|
+
- Skills demonstrate email masking inside text, channel-error observation and bounded asynchronous log drains.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- With `groupBy`, size rotation renames the group file that grew too large instead of the ungrouped top-level file.
|
|
25
|
+
- A file name that throws while a buffer is written (a `fileName` function, or an overridden `fileName` getter) is reported as a failed write instead of leaving the channel stuck in its writing state; later writes go through.
|
|
26
|
+
|
|
7
27
|
## 5.30.0 - 2026-10-16
|
|
8
28
|
|
|
9
29
|
### Upgrading
|
|
10
30
|
|
|
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()`.
|
|
31
|
+
- **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()`. `warlock upgrade --from 5.29 --to 5.31.0` removes the `await`s and lists chained `.then()` calls (codemod `logger-sync-calls`).
|
|
12
32
|
|
|
13
33
|
### Changed
|
|
14
34
|
|
|
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
|
|
35
|
+
- Logging is fire-and-forget: level methods no longer return a promise, so `@typescript-eslint/no-floating-promises` no longer flags every call site.
|
|
16
36
|
- `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
37
|
- `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
38
|
- `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.
|
|
@@ -20,7 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
20
40
|
### Added
|
|
21
41
|
|
|
22
42
|
- `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
|
|
43
|
+
- `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.
|
|
24
44
|
|
|
25
45
|
### Fixed
|
|
26
46
|
|
|
@@ -100,6 +120,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
100
120
|
### Changed
|
|
101
121
|
|
|
102
122
|
- Lockstep release maintenance and dependency refresh.
|
|
123
|
+
|
|
103
124
|
## 5.20.0 - 2026-09-24
|
|
104
125
|
|
|
105
126
|
### Changed
|
|
@@ -189,7 +210,7 @@ changelogs.
|
|
|
189
210
|
|
|
190
211
|
### Security
|
|
191
212
|
|
|
192
|
-
- **Secrets are now redacted by default. This is a behavior change — logs that previously showed these values in cleartext will now show `[REDACTED]`.** Redaction used to be a
|
|
213
|
+
- **Secrets are now redacted by default. This is a behavior change — logs that previously showed these values in cleartext will now show `[REDACTED]`.** Redaction used to be a _tool_ (`redact.paths`, entirely opt-in): unless an application configured it, a `password` in `context`, an `authorization` header, or an `apiKey` on a logged `Error` reached every sink — console, log file, JSON log file, Sentry — verbatim. Protection existed only where every call site had been configured correctly, with no signal when one hadn't. It is now a _default_.
|
|
193
214
|
|
|
194
215
|
A built-in denylist (`DEFAULT_REDACT_KEYS`) censors matching keys at any depth of `context`, `message`, and an `Error`'s own enumerable properties, applied at the same logger-wide choke point as `redact.paths` — so every channel inherits it, including custom ones. The set covers passwords, shared secrets, tokens (access/refresh/id/CSRF/session), API and private keys, HTTP credential headers (`authorization`, `x-api-key`, `cookie`, `set-cookie`), session IDs, connection strings, and high-sensitivity financial PII (card numbers, CVV, SSN, IBAN).
|
|
195
216
|
|
|
@@ -204,7 +225,7 @@ changelogs.
|
|
|
204
225
|
|
|
205
226
|
Paths are evaluated before keys, so a function `censor` on an explicit path still receives the original value rather than a mask, and that leaf is not censored twice.
|
|
206
227
|
|
|
207
|
-
Known residual gaps, unchanged by this release and documented on `applyRedact`: secrets interpolated into a `message`
|
|
228
|
+
Known residual gaps, unchanged by this release and documented on `applyRedact`: secrets interpolated into a `message` _string_ (`` `token=${t}` ``) cannot be reached by either matcher; `Map` / `Set` / `Buffer` contents are not traversed; and properties exposed only via getters or non-enumerable descriptors are not walked, so an HTTP client that hides its request config behind a getter can still slip through. Enumerable ones — axios's `.config` / `.response`, which carry the outgoing `Authorization` header — _are_ now covered.
|
|
208
229
|
|
|
209
230
|
### Fixed
|
|
210
231
|
|
package/README.md
CHANGED
|
@@ -23,10 +23,7 @@ yarn add @warlock.js/logger
|
|
|
23
23
|
```ts
|
|
24
24
|
import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
|
|
25
25
|
|
|
26
|
-
log.setChannels([
|
|
27
|
-
new ConsoleLog(),
|
|
28
|
-
new FileLog({ chunk: "daily" }),
|
|
29
|
-
]);
|
|
26
|
+
log.setChannels([new ConsoleLog(), new FileLog({ chunk: "daily" })]);
|
|
30
27
|
|
|
31
28
|
log.info("users", "register", "New user created");
|
|
32
29
|
log.error("payments", "charge", new Error("Card declined"));
|
|
@@ -61,12 +58,12 @@ log.error({
|
|
|
61
58
|
|
|
62
59
|
## Built-in channels
|
|
63
60
|
|
|
64
|
-
| Channel
|
|
65
|
-
|
|
66
|
-
| `ConsoleLog`
|
|
67
|
-
| `FileLog`
|
|
68
|
-
| `JSONFileLog` | `"fileJson"` | Structured JSON files — ideal for aggregators
|
|
69
|
-
| `SentryLog`
|
|
61
|
+
| Channel | Name | Purpose |
|
|
62
|
+
| ------------- | ------------ | --------------------------------------------------------------------------------- |
|
|
63
|
+
| `ConsoleLog` | `"console"` | Colorized terminal output |
|
|
64
|
+
| `FileLog` | `"file"` | Plain-text files, optional chunking + rotation + grouping |
|
|
65
|
+
| `JSONFileLog` | `"fileJson"` | Structured JSON files — ideal for aggregators |
|
|
66
|
+
| `SentryLog` | `"sentry"` | Forwards to Sentry (events + breadcrumbs); needs the optional `@sentry/node` peer |
|
|
70
67
|
|
|
71
68
|
All channels share the `BasicLogConfigurations` options (`levels`, `filter`, `dateFormat`); `FileLog` / `JSONFileLog` add storage, chunking, rotation, and grouping options, and `SentryLog` adds `client` / `options` / `eventLevels`.
|
|
72
69
|
|
|
@@ -74,7 +71,7 @@ All channels share the `BasicLogConfigurations` options (`levels`, `filter`, `da
|
|
|
74
71
|
|
|
75
72
|
The package exports one default singleton and one class:
|
|
76
73
|
|
|
77
|
-
- **`log`** — a pre-instantiated `Logger`. Day-to-day logging
|
|
74
|
+
- **`log`** — a pre-instantiated `Logger`. Day-to-day logging _and_ configuration both go through it: `log.info(...)`, `log.configure(...)`, `log.flush()` / `log.flushSync()`, `log.addChannel(...)`.
|
|
78
75
|
- **`Logger`** — the class. Use it when you need an isolated logger (libraries, sandboxes, parallel test suites).
|
|
79
76
|
|
|
80
77
|
`log` is a `Logger` instance, not a function — every level shortcut and configuration method is reachable on it. The bare-callable `log(data)` form was removed; use `log.log(data)` for the data-object form, or `log.info(...)` / `log.error(...)` / etc. for the positional form.
|
package/cjs/index.cjs
CHANGED
|
@@ -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) {
|
|
@@ -194,8 +216,54 @@ var ConsoleLog = class extends LogChannel {
|
|
|
194
216
|
}
|
|
195
217
|
};
|
|
196
218
|
|
|
219
|
+
//#endregion
|
|
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
|
+
|
|
197
258
|
//#endregion
|
|
198
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,6 +696,16 @@ 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
711
|
//#region ../../packages/logger/src/utils/safe-json-stringify.ts
|
|
@@ -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,6 +881,7 @@ var JSONFileLog = class extends FileLog {
|
|
|
622
881
|
}
|
|
623
882
|
}
|
|
624
883
|
this.onSave();
|
|
884
|
+
await this.afterWrite();
|
|
625
885
|
}
|
|
626
886
|
};
|
|
627
887
|
|
|
@@ -1200,6 +1460,41 @@ function mergeRedact(base, extra) {
|
|
|
1200
1460
|
};
|
|
1201
1461
|
}
|
|
1202
1462
|
|
|
1463
|
+
//#endregion
|
|
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
|
+
|
|
1203
1498
|
//#endregion
|
|
1204
1499
|
//#region ../../packages/logger/src/redact/mask-email.ts
|
|
1205
1500
|
/**
|
|
@@ -1314,14 +1609,14 @@ var Logger = class {
|
|
|
1314
1609
|
* Add a new channel
|
|
1315
1610
|
*/
|
|
1316
1611
|
addChannel(channel) {
|
|
1317
|
-
this.channels.push(channel);
|
|
1612
|
+
this.channels.push(this.attachChannel(channel));
|
|
1318
1613
|
return this;
|
|
1319
1614
|
}
|
|
1320
1615
|
/**
|
|
1321
1616
|
* Set base configurations
|
|
1322
1617
|
*/
|
|
1323
1618
|
configure(config) {
|
|
1324
|
-
if (config.channels) this.
|
|
1619
|
+
if (config.channels) this.setChannels(config.channels);
|
|
1325
1620
|
if (config.autoFlushOn) this.enableAutoFlush(config.autoFlushOn);
|
|
1326
1621
|
if (config.minLevel !== void 0) this.setMinLevel(config.minLevel);
|
|
1327
1622
|
if (config.redact !== void 0) this.setRedact(config.redact);
|
|
@@ -1388,9 +1683,18 @@ var Logger = class {
|
|
|
1388
1683
|
*/
|
|
1389
1684
|
setChannels(channels) {
|
|
1390
1685
|
this.channels = channels;
|
|
1686
|
+
for (const channel of channels) this.attachChannel(channel);
|
|
1391
1687
|
return this;
|
|
1392
1688
|
}
|
|
1393
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
|
+
/**
|
|
1394
1698
|
* Normalize log data to a single object
|
|
1395
1699
|
*/
|
|
1396
1700
|
normalizeLogData(dataOrModule, action, message = "", level, context) {
|
|
@@ -1779,18 +2083,22 @@ function captureAnyUnhandledRejection(options = {}) {
|
|
|
1779
2083
|
|
|
1780
2084
|
//#endregion
|
|
1781
2085
|
exports.ConsoleLog = ConsoleLog;
|
|
2086
|
+
exports.DAILY_FILE_NAME = DAILY_FILE_NAME;
|
|
1782
2087
|
exports.DEFAULT_FLUSH_TIMEOUT_MS = DEFAULT_FLUSH_TIMEOUT_MS;
|
|
1783
2088
|
exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
|
|
1784
2089
|
exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
|
|
1785
2090
|
exports.FileLog = FileLog;
|
|
2091
|
+
exports.HOURLY_FILE_NAME = HOURLY_FILE_NAME;
|
|
1786
2092
|
exports.JSONFileLog = JSONFileLog;
|
|
1787
2093
|
exports.LogChannel = LogChannel;
|
|
1788
2094
|
exports.Logger = Logger;
|
|
1789
2095
|
exports.MASK_EMAILS_MAX_LENGTH = MASK_EMAILS_MAX_LENGTH;
|
|
2096
|
+
exports.ROTATE_FILE_NAME = ROTATE_FILE_NAME;
|
|
1790
2097
|
exports.SentryLog = SentryLog;
|
|
1791
2098
|
exports.applyRedact = applyRedact;
|
|
1792
2099
|
exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
|
|
1793
2100
|
exports.clearMessage = clearMessage;
|
|
2101
|
+
exports.escapeLogValue = escapeLogValue;
|
|
1794
2102
|
exports.log = log;
|
|
1795
2103
|
exports.maskEmail = maskEmail;
|
|
1796
2104
|
exports.maskEmailsInText = maskEmailsInText;
|