@warlock.js/logger 5.1.0 → 5.2.2
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 +10 -0
- package/README.md +8 -0
- package/cjs/index.cjs +1 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +2 -2
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/json-file-log.d.mts.map +1 -1
- 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 +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts.map +1 -1
- package/esm/redact/default-keys.mjs.map +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/llms-full.txt +4 -0
- package/package.json +5 -2
- package/skills/logger-basics/SKILL.md +1 -1
- package/skills/overview/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.2.2
|
|
8
|
+
|
|
9
|
+
- Documented: the README, CHANGELOG, and `overview` skill now describe the
|
|
10
|
+
already-shipped `package.json` `"warlock": { "environment": "server" }`
|
|
11
|
+
marker — `@warlock.js/logger`'s entire runtime surface is server-only. The
|
|
12
|
+
marker is build-boundary metadata read by `@warlock.js/web`'s Gate A
|
|
13
|
+
(import resolution) and Gate C (emitted-bundle verification); app client
|
|
14
|
+
code must not value-import this package, type-only imports are allowed, and
|
|
15
|
+
server loaders/controllers/modules may import it freely. No source change.
|
|
16
|
+
|
|
7
17
|
## 5.1.0
|
|
8
18
|
|
|
9
19
|
No changes to `@warlock.js/logger`. Released in lockstep with the `@warlock.js/web`
|
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ npm install @warlock.js/logger
|
|
|
10
10
|
yarn add @warlock.js/logger
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Server-only package
|
|
14
|
+
|
|
15
|
+
`@warlock.js/logger`'s entire runtime surface is server-only. Its `package.json` declares `"warlock": { "environment": "server" }` — build-boundary metadata that `@warlock.js/web`'s Gate A (import resolution) and Gate C (emitted-bundle verification) read to keep it out of the browser bundle.
|
|
16
|
+
|
|
17
|
+
- App code that reaches the client bundle (including page and layout modules) must not value-import `@warlock.js/logger` — Gate A refuses the build.
|
|
18
|
+
- A type-only import (`import type { ... } from "@warlock.js/logger"`) is allowed — it carries no runtime edge.
|
|
19
|
+
- Server loaders, controllers, and modules may import it freely.
|
|
20
|
+
|
|
13
21
|
## 30-second tour
|
|
14
22
|
|
|
15
23
|
```ts
|
package/cjs/index.cjs
CHANGED
|
@@ -731,7 +731,7 @@ var SentryLog = class extends LogChannel {
|
|
|
731
731
|
*/
|
|
732
732
|
async log(data) {
|
|
733
733
|
if (!this.shouldBeLogged(data)) return;
|
|
734
|
-
if (isModuleExists === null) await loadSentry();
|
|
734
|
+
if (!this.sentry && isModuleExists === null) await loadSentry();
|
|
735
735
|
if (!this.sentry) {
|
|
736
736
|
this.reportMissingSdk();
|
|
737
737
|
return;
|
package/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["colors","EOL","Random"],"sources":["../../../../../../logger/src/log-channel.ts","../../../../../../logger/src/channels/console-log.ts","../../../../../../logger/src/channels/file-log.ts","../../../../../../logger/src/utils/safe-json-stringify.ts","../../../../../../logger/src/channels/json-file-log.ts","../../../../../../logger/src/channels/sentry-log.ts","../../../../../../logger/src/redact/default-keys.ts","../../../../../../logger/src/redact/redact.ts","../../../../../../logger/src/utils/clear-message.ts","../../../../../../logger/src/logger.ts","../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\n\r\nexport abstract class LogChannel<\r\n Options extends BasicLogConfigurations = BasicLogConfigurations,\r\n> implements LogContract {\r\n /**\r\n * Channel name\r\n */\r\n public name!: string;\r\n\r\n /**\r\n * Channel description\r\n */\r\n public description?: string;\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = false;\r\n\r\n /**\r\n * Default Configurations\r\n */\r\n protected defaultConfigurations: Options = {} as Options;\r\n\r\n /**\r\n * Channel configurations\r\n */\r\n protected channelConfigurations: Options = {} as Options; //\r\n\r\n /**\r\n * Determine whether the channel is fully initialized\r\n */\r\n protected isInitialized = false;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(configurations?: Options) {\r\n if (configurations) {\r\n this.setConfigurations(configurations);\r\n }\r\n\r\n setTimeout(async () => {\r\n if (this.init) {\r\n await this.init();\r\n }\r\n\r\n this.isInitialized = true;\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Initialize the channel\r\n */\r\n protected init?(): void | Promise<void>;\r\n\r\n /**\r\n * Get config value\r\n */\r\n protected config<K extends keyof Options>(key: K): Options[K] {\r\n return (\r\n this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key]\r\n );\r\n }\r\n\r\n /**\r\n * Set configurations\r\n */\r\n protected setConfigurations(configurations: Options) {\r\n this.channelConfigurations = {\r\n ...this.channelConfigurations,\r\n ...configurations,\r\n };\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Determine if the message should be logged\r\n */\r\n protected shouldBeLogged(data: LoggingData): boolean {\r\n // check for debug mode\r\n const allowedLevels = this.config(\"levels\");\r\n\r\n if (allowedLevels?.length && !allowedLevels.includes(data.type))\r\n return false;\r\n\r\n const filter = this.config(\"filter\");\r\n\r\n if (filter) {\r\n return filter(data);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Log the given message\r\n */\r\n public abstract log(data: LoggingData): void | Promise<void>;\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync?(): void;\r\n\r\n /**\r\n * Asynchronously flush buffered messages.\r\n *\r\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\r\n * don't implement it. Buffering channels override this to drain their buffer\r\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\r\n */\r\n public flush?(): void | Promise<void>;\r\n\r\n /**\r\n * Read the channel's redact config (if any). Used by `Logger` to apply\r\n * per-channel additive redaction on top of the logger-wide floor.\r\n * Subclasses normally don't override this — set `redact` in your channel\r\n * configuration instead.\r\n */\r\n public getRedactConfig(): RedactConfig | undefined {\r\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\r\n }\r\n\r\n /**\r\n * Get date and time formats\r\n */\r\n protected getDateAndTimeFormat() {\r\n const dateFormat = this.config(\"dateFormat\");\r\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\r\n const time = dateFormat?.time ?? \"HH:mm:ss\";\r\n\r\n return { date, time };\r\n }\r\n\r\n /**\r\n * get basic configurations with the given ones\r\n */\r\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\r\n return {\r\n filter: () => true,\r\n ...configurations,\r\n } as any as Options;\r\n }\r\n}\r\n","import { colors } from \"@mongez/copper\";\r\nimport { inspect } from \"util\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\n\r\nexport type ConsoleLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Render the log entry's `context` object on a second line after the main\r\n * message. When `false`, context is silently dropped (the historical\r\n * behavior). When `true`, contexts are pretty-printed with `util.inspect`\r\n * — colored, depth-limited, ideal for development. Persistent channels\r\n * (`FileLog`, `JSONFileLog`) always retain context regardless of this flag.\r\n *\r\n * @default false\r\n */\r\n showContext?: boolean;\r\n /**\r\n * Depth passed to `util.inspect` when rendering context. Only applies when\r\n * `showContext` is enabled.\r\n *\r\n * @default 4\r\n */\r\n contextDepth?: number;\r\n};\r\n\r\n/**\r\n * The longest level name (`success`) sets the tag column width. Padding every\r\n * level name to this width keeps the timestamp / module / action columns\r\n * aligned across lines. The level icons are bare (text-style, single-cell)\r\n * glyphs, so padding the name alone produces true columns.\r\n */\r\nconst LEVEL_NAME_WIDTH = 7;\r\n\r\n/**\r\n * Per-level console presentation: the colored, fixed-width `{icon} {name}` tag\r\n * and the function that colors the message body.\r\n */\r\ntype LevelStyle = {\r\n tag: string;\r\n message: (message: LoggingData[\"message\"]) => string;\r\n};\r\n\r\n/**\r\n * Build a single-color, fixed-width tag — `{icon} {name}` with the name padded\r\n * so every tag spans the same number of columns.\r\n */\r\nfunction buildTag(\r\n icon: string,\r\n name: string,\r\n color: (text: string) => string,\r\n): string {\r\n return color(`${icon} ${name.padEnd(LEVEL_NAME_WIDTH)}`);\r\n}\r\n\r\n/**\r\n * Console styling per log level. The `fatal` tag is a bright-red background\r\n * badge — the same column width as the others (`\" ☠ fatal \"` is 9 cells, like\r\n * `{icon} {name.padEnd(7)}`), but impossible to miss in a wall of red `error`\r\n * lines, where a plain red tag would blend in.\r\n */\r\nconst LEVEL_STYLES: Record<LogLevel, LevelStyle> = {\r\n debug: {\r\n tag: buildTag(\"⚙\", \"debug\", colors.magentaBright),\r\n message: colors.magentaBright,\r\n },\r\n info: {\r\n tag: buildTag(\"ℹ\", \"info\", colors.blueBright),\r\n message: colors.blueBright,\r\n },\r\n warn: {\r\n tag: buildTag(\"⚠\", \"warn\", colors.yellow),\r\n message: colors.yellowBright,\r\n },\r\n error: {\r\n tag: buildTag(\"✗\", \"error\", colors.red),\r\n message: colors.redBright,\r\n },\r\n success: {\r\n tag: buildTag(\"✓\", \"success\", colors.green),\r\n message: colors.greenBright,\r\n },\r\n fatal: {\r\n tag: colors.bgRedBright(colors.whiteBright(colors.bold(\" ☠ fatal \"))),\r\n message: colors.redBright.bold,\r\n },\r\n};\r\n\r\n/**\r\n * Fallback for an unrecognized level — a plain `[log]` tag padded to the same\r\n * width so the columns stay aligned.\r\n */\r\nconst DEFAULT_STYLE: LevelStyle = {\r\n tag: \"[log]\".padEnd(LEVEL_NAME_WIDTH + 2),\r\n message: message => message as string,\r\n};\r\n\r\nexport class ConsoleLog extends LogChannel<ConsoleLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"console\";\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = true;\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public log(data: LoggingData) {\r\n const { module, action, message, type: level } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n // Console shows time-only (HH:mm:ss.SSS) — within a dev session the date\r\n // rarely changes, so the full ISO date + `T`/`Z` are just noise. Persistent\r\n // channels (FileLog / JSONFileLog) keep the full ISO timestamp.\r\n const time = new Date().toISOString().slice(11, 23); // i.e. \"10:22:00.000\"\r\n\r\n const style = LEVEL_STYLES[level] ?? DEFAULT_STYLE;\r\n\r\n // The slate timestamp recedes so the colored level + message lead the eye;\r\n // module (cyan) and action (magenta) stay colored for subsystem scanning.\r\n // `slate` (256-color neutral gray) reads cleaner than bright-black `gray`,\r\n // which sits too close to the background on most terminal themes.\r\n console.log(\r\n style.tag,\r\n colors.slate(`(${time})`),\r\n colors.cyan(`[${module}]`),\r\n colors.magenta(`[${action}]`),\r\n style.message(message),\r\n );\r\n\r\n if (typeof message === \"object\") {\r\n console.log(message);\r\n }\r\n\r\n // Render context on a second line when explicitly enabled. We only\r\n // attempt rendering if there's anything meaningful to show — empty\r\n // objects clutter the terminal without adding signal.\r\n if (this.config(\"showContext\") && data.context && Object.keys(data.context).length > 0) {\r\n const depth = this.config(\"contextDepth\") ?? 4;\r\n console.log(\r\n colors.slate(\" ↳\"),\r\n inspect(data.context, { colors: true, depth, breakLength: 80 }),\r\n );\r\n }\r\n }\r\n}\r\n","import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport { EOL } from \"os\";\r\nimport path from \"path\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n LogLevel,\r\n LogMessage,\r\n} from \"../types\";\r\n\r\n// TODO: Add max messages per file before rotation\r\n\r\nexport type FileLogConfig = BasicLogConfigurations & {\r\n storagePath?: string;\r\n /**\r\n * File name, without extension\r\n */\r\n name?: string;\r\n /**\r\n * chunk mode\r\n * If set to `single`, the logs will be created in a single file, unless the rotate is set to true\r\n * If set to `daily`, the logs will be created in a daily file, unless the rotate is set to true\r\n * If set to `hourly`, the logs will be created in an hourly file, unless the rotate is set to true\r\n * @default single\r\n */\r\n chunk?: \"single\" | \"daily\" | \"hourly\";\r\n /**\r\n * Whether to rotate the file\r\n *\r\n * @default true\r\n */\r\n rotate?: boolean;\r\n /**\r\n * File Extension\r\n *\r\n * @default log\r\n */\r\n extension?: string;\r\n /**\r\n * If rotate is set, the rotate name will be added to the file name suffixed with `-`\r\n *\r\n * @default DD-MM-YYYY\r\n */\r\n rotateFileName?: string;\r\n /**\r\n * Max file size before rotating the file\r\n *\r\n * @default 10MB\r\n */\r\n maxFileSize?: number;\r\n /**\r\n * Set the max messages that needs to be added before writing to the file\r\n *\r\n * @default 100\r\n */\r\n maxMessagesToWrite?: number;\r\n /**\r\n * Group logs by\r\n * Please note that the order matters here\r\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\r\n *\r\n * @default none\r\n */\r\n groupBy?: (\"level\" | \"module\" | \"action\")[];\r\n /**\r\n * Define what levels should be logged\r\n *\r\n * @default all\r\n */\r\n levels?: LogLevel[];\r\n /**\r\n * Date and time format\r\n */\r\n dateFormat?: {\r\n date?: string;\r\n time?: string;\r\n };\r\n};\r\n\r\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"file\";\r\n\r\n /**\r\n * Messages buffer\r\n */\r\n protected messages: LogMessage[] = [];\r\n\r\n /**\r\n * Grouped messages\r\n */\r\n protected groupedMessages: Record<string, LogMessage[]> = {};\r\n\r\n /**\r\n * Default channel configurations\r\n */\r\n protected defaultConfigurations: FileLogConfig = {\r\n storagePath: process.cwd() + \"/storage/logs\",\r\n rotate: true,\r\n name: \"app\",\r\n extension: \"log\",\r\n chunk: \"single\",\r\n maxMessagesToWrite: 100,\r\n filter: () => true,\r\n maxFileSize: 10 * 1024 * 1024, // 10MB\r\n get rotateFileName() {\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n },\r\n dateFormat: {\r\n date: \"DD-MM-YYYY\",\r\n time: \"HH:mm:ss\",\r\n },\r\n };\r\n\r\n /**\r\n * Last write time\r\n */\r\n protected lastWriteTime = Date.now();\r\n\r\n /**\r\n * A flag to determine if the file is being written\r\n */\r\n protected isWriting = false;\r\n\r\n /**\r\n * Handle for the periodic flush interval. Stored so it can be cleared\r\n * in `dispose()` — long-lived processes that create channels dynamically\r\n * would otherwise leak one timer per channel.\r\n */\r\n protected flushIntervalHandle?: NodeJS.Timeout;\r\n\r\n /**\r\n * Check file size for file rotation\r\n */\r\n protected async checkAndRotateFile(filePath = this.filePath) {\r\n if (!this.config(\"rotate\")) return;\r\n\r\n try {\r\n const stats = await fs.promises.stat(filePath);\r\n if (stats.size >= this.config(\"maxFileSize\")!) {\r\n await this.rotateLogFile();\r\n }\r\n } catch (error: any) {\r\n // ENOENT is expected when the file hasn't been created yet — there is\r\n // nothing to rotate, so stay silent. Surface anything else.\r\n if (error.code !== \"ENOENT\") {\r\n console.error(\"Error checking log file:\", error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Rotate log file\r\n */\r\n protected async rotateLogFile() {\r\n const fileName = `${this.fileName}-${this.config(\"rotateFileName\")}-${Date.now()}`;\r\n\r\n const extension = this.extension;\r\n\r\n const rotatedFilePath = path.join(this.storagePath, `${fileName}.${extension}`);\r\n\r\n await fs.promises.rename(this.filePath, rotatedFilePath).catch((error) => {\r\n console.error(\"Error rotating file:\", error);\r\n });\r\n }\r\n\r\n /**\r\n * Flush messages\r\n *\r\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\r\n * entries indefinitely. The handle is stored on the instance so `dispose()`\r\n * can stop it — without this, every channel leaks a timer for the lifetime\r\n * of the process.\r\n */\r\n protected initMessageFlush() {\r\n this.flushIntervalHandle = setInterval(() => {\r\n if (\r\n this.messages.length > 0 &&\r\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\r\n ) {\r\n this.writeMessagesToFile();\r\n }\r\n }, 5000);\r\n }\r\n\r\n /**\r\n * Stop the background flush interval and drain any buffered entries.\r\n *\r\n * Call this when discarding a channel (e.g. reconfiguring the logger at\r\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\r\n * call more than once.\r\n */\r\n public dispose(): void {\r\n if (this.flushIntervalHandle) {\r\n clearInterval(this.flushIntervalHandle);\r\n this.flushIntervalHandle = undefined;\r\n }\r\n\r\n this.flushSync();\r\n }\r\n\r\n /**\r\n * Get file path\r\n */\r\n public get filePath() {\r\n const fileName = this.fileName;\r\n\r\n const extension = this.extension;\r\n\r\n return path.join(this.storagePath, `${fileName}.${extension}`);\r\n }\r\n\r\n /**\r\n * Get max messages\r\n */\r\n protected get maxMessagesToWrite(): number {\r\n return this.config(\"maxMessagesToWrite\")!;\r\n }\r\n\r\n /**\r\n * Get file name\r\n */\r\n public get fileName(): string {\r\n const debugLevel = this.config(\"chunk\")!;\r\n\r\n switch (debugLevel) {\r\n case \"single\":\r\n default:\r\n return this.config(\"name\")!;\r\n case \"daily\":\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n case \"hourly\":\r\n return dayjs().format(\"DD-MM-YYYY-HH-00-00-a\");\r\n }\r\n }\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return this.config(\"extension\")!;\r\n }\r\n\r\n /**\r\n * Get content\r\n */\r\n protected get content() {\r\n return this.messages.map((message) => message.content).join(EOL) + EOL;\r\n }\r\n\r\n /**\r\n * Get storage path\r\n */\r\n public get storagePath(): string {\r\n return this.config(\"storagePath\")!;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected async init() {\r\n const logsDirectory = this.storagePath;\r\n\r\n await ensureDirectoryAsync(logsDirectory);\r\n\r\n this.initMessageFlush();\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n fs.appendFileSync(filePath, content);\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n fs.appendFileSync(this.filePath, this.content);\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * Asynchronously drain buffered entries to disk.\r\n *\r\n * The async analogue of {@link flushSync}: it reuses the same async writer\r\n * as the background interval, so a caller on a graceful-shutdown path can\r\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\r\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\r\n * its overridden `writeMessagesToFile` performs the JSON merge.\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\r\n return;\r\n }\r\n\r\n // Clear the in-flight lock so a deliberate drain is never short-circuited\r\n // by a half-finished background write, then await the async writer to\r\n // completion — unlike the fire-and-forget interval, callers depend on the\r\n // buffer reaching disk before this resolves.\r\n this.isWriting = false;\r\n\r\n await this.writeMessagesToFile();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n let content = `[${date}] [${level}] [${module}][${action}]: `;\r\n\r\n let stack: string | undefined;\r\n\r\n // check if message is an instance of Error\r\n if (message instanceof Error) {\r\n // in that case we need to store the error message and stack trace\r\n content += message.message + EOL;\r\n content += `[trace]` + EOL;\r\n content += message.stack;\r\n stack = message.stack;\r\n } else {\r\n content += message;\r\n }\r\n\r\n this.messages.push({\r\n content,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n });\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Check if messages should be written\r\n */\r\n protected async checkIfMessagesShouldBeWritten() {\r\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\r\n await this.writeMessagesToFile();\r\n }\r\n }\r\n\r\n /**\r\n * Should be called after messages are saved\r\n */\r\n protected onSave() {\r\n this.messages = [];\r\n this.groupedMessages = {};\r\n this.isWriting = false;\r\n this.lastWriteTime = Date.now();\r\n }\r\n\r\n /**\r\n * Check if messages should be grouped\r\n */\r\n protected get messagedShouldBeGrouped(): boolean {\r\n return Number(this.config(\"groupBy\")?.length) > 0;\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile() {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure we check file size before writing\r\n\r\n try {\r\n await this.write(this.filePath, this.content);\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n\r\n try {\r\n await this.write(filePath, content);\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n }\r\n }\r\n\r\n this.onSave();\r\n this.isWriting = false;\r\n }\r\n\r\n /**\r\n * Prepare grouped messages\r\n */\r\n protected prepareGroupedMessages(): void {\r\n this.messages.forEach((message) => {\r\n const key = this.config(\"groupBy\")!\r\n .map((groupKey) => encodeURIComponent(message[groupKey]))\r\n .join(\"/\");\r\n\r\n this.groupedMessages[key] = this.groupedMessages[key] || [];\r\n this.groupedMessages[key].push(message);\r\n });\r\n }\r\n\r\n /**\r\n * Start writing to the file\r\n */\r\n protected async write(filePath: string, content: string) {\r\n return new Promise((resolve, reject) => {\r\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\r\n\r\n writer.write(content, (error) => {\r\n writer.end();\r\n if (error) {\r\n reject(error);\r\n } else {\r\n resolve(true);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n","import { stringify as safeStableStringify } from \"safe-stable-stringify\";\n\n/**\n * Replacer that surfaces Error data — `name`, `message`, and `stack` are\n * non-enumerable on `Error`, so neither default JSON serialization nor an\n * object spread captures them (both produce `{}`). They are copied explicitly;\n * the trailing spread then captures any additional enumerable props the caller\n * (or a subclass) attached, such as a `code` field.\n */\nfunction errorReplacer<Value = unknown>(\n _key: string,\n value: Value,\n): Record<string, unknown> | Value {\n if (value instanceof Error) {\n // Spread first, then the explicit Error fields. `name`/`message`/`stack`\n // are non-enumerable on `Error`, so the spread never carries them — the\n // explicit assignments are what surface them. Placing the spread first\n // means those explicit keys legitimately take precedence over any\n // identically-named *enumerable* prop a subclass attached (resolving the\n // duplicate-key warning). Because `safe-stable-stringify` sorts keys, the\n // insertion order here does not affect the serialized bytes.\n return {\n ...value,\n name: value.name,\n message: value.message,\n stack: value.stack,\n };\n }\n\n return value;\n}\n\n/**\n * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated\n * non-tree references are handled by `safe-stable-stringify`; functions and\n * symbols are dropped (standard JSON behavior); Errors are expanded via\n * `errorReplacer`. Class instances serialize as their enumerable props.\n *\n * @example\n * await fs.promises.writeFile(filePath, safeJsonStringify(payload, 2));\n */\nexport function safeJsonStringify(value: unknown, space?: number): string {\n return safeStableStringify(value, errorReplacer, space) ?? \"\";\n}\n","import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport path from \"path\";\r\nimport type { LogContract, LogMessage, LoggingData } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\nimport { FileLog } from \"./file-log\";\r\n\r\nexport class JSONFileLog extends FileLog implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"fileJson\";\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return \"json\";\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n fs.writeFileSync(filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(this.filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(this.filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.messages);\r\n fs.writeFileSync(this.filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n let stack: string[] | undefined;\r\n\r\n if (data.message instanceof Error) {\r\n stack = data.message.stack?.split(\"\\n\");\r\n data.message = data.message.message;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n this.messages.push({\r\n content: message,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n } as LogMessage);\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile(): Promise<void> {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure file rotation is handled\r\n\r\n let fileContents = { messages: [] as any[] };\r\n\r\n if (await fileExistsAsync(this.filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(this.filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n } else {\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n\r\n fileContents.messages.push(...this.messages);\r\n\r\n try {\r\n await fs.promises.writeFile(this.filePath, safeJsonStringify(fileContents, 2));\r\n\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n let fileContents: { messages: any[] } = { messages: [] };\r\n if (await fileExistsAsync(filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] };\r\n }\r\n } else {\r\n fileContents = { messages: [] };\r\n }\r\n\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n\r\n try {\r\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n this.onSave();\r\n }\r\n}\r\n","import { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\n\r\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\r\n// @sentry/node is referenced ONLY through the local minimal types below — never\r\n// via `typeof import(\"@sentry/node\")`. This package is served as source\r\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\r\n// program; a static type reference to the SDK would force module resolution and\r\n// break every consumer who (correctly) never installs the optional peer with a\r\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\r\n// the same reason, and its result is cast to these shapes. The shapes mirror the\r\n// stable `@sentry/node` public API (unchanged across v8–v10).\r\n\r\n/**\r\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\r\n */\r\ntype SentrySeverityLevel =\r\n | \"fatal\"\r\n | \"error\"\r\n | \"warning\"\r\n | \"log\"\r\n | \"info\"\r\n | \"debug\";\r\n\r\n/**\r\n * The Sentry `Scope` surface used while building a single event.\r\n */\r\nexport interface SentryScopeLike {\r\n setLevel(level: SentrySeverityLevel): void;\r\n setTags(tags: Record<string, string>): void;\r\n setContext(name: string, context: Record<string, unknown> | null): void;\r\n}\r\n\r\n/**\r\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\r\n */\r\nexport type SentryBreadcrumb = {\r\n category?: string;\r\n message?: string;\r\n level?: SentrySeverityLevel;\r\n data?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\r\n * namespace satisfies this shape, so an app can pass it straight through as\r\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\r\n */\r\nexport interface SentryForwarder {\r\n captureException(exception: unknown): string;\r\n captureMessage(message: string, level?: SentrySeverityLevel): string;\r\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\r\n withScope(callback: (scope: SentryScopeLike) => void): void;\r\n flush(timeout?: number): Promise<boolean>;\r\n}\r\n\r\n/**\r\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\r\n * fields; the index signature keeps any other SDK option valid without coupling\r\n * to the SDK's types.\r\n */\r\nexport type SentryInitOptions = {\r\n dsn?: string;\r\n environment?: string;\r\n release?: string;\r\n sampleRate?: number;\r\n [key: string]: unknown;\r\n};\r\n\r\n/**\r\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\r\n * lifecycle calls the channel uses when it owns initialization.\r\n */\r\ninterface SentryNamespace extends SentryForwarder {\r\n init(options?: SentryInitOptions): unknown;\r\n getClient(): unknown;\r\n}\r\n\r\nlet Sentry: SentryNamespace | undefined;\r\nlet isModuleExists: boolean | null = null;\r\nlet loadingPromise: Promise<void> | undefined;\r\n\r\nconst SENTRY_INSTALL_INSTRUCTIONS = `\r\nThe Sentry log channel requires the @sentry/node package.\r\nInstall it with:\r\n\r\n npm install @sentry/node\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add @sentry/node\r\n yarn add @sentry/node\r\n`.trim();\r\n\r\n/**\r\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\r\n * import failure to \"not installed\" — the curated install message surfaces at\r\n * `log()` time, never as a boot-time module-resolution crash.\r\n */\r\nfunction loadSentry(): Promise<void> {\r\n if (isModuleExists !== null) {\r\n return Promise.resolve();\r\n }\r\n\r\n if (loadingPromise) {\r\n return loadingPromise;\r\n }\r\n\r\n loadingPromise = (async () => {\r\n try {\r\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\r\n // statically resolves the optional peer — see the file header.\r\n const sentryModule: string = \"@sentry/node\";\r\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n })();\r\n\r\n return loadingPromise;\r\n}\r\n\r\nexport type SentryLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\r\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\r\n * channel forwards through it and never imports or re-initializes the SDK.\r\n */\r\n client?: SentryForwarder;\r\n /**\r\n * Initialize Sentry from these options instead of reusing a host client. The\r\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\r\n * guarded so it never clobbers an existing client.\r\n */\r\n options?: SentryInitOptions;\r\n /**\r\n * Levels delivered as Sentry *events* (these consume the error quota). Every\r\n * other level is recorded as a breadcrumb that rides along with the next\r\n * event, costing no quota.\r\n *\r\n * @default [\"fatal\", \"error\", \"warn\"]\r\n */\r\n eventLevels?: LogLevel[];\r\n /**\r\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\r\n *\r\n * @default 2000\r\n */\r\n flushTimeout?: number;\r\n};\r\n\r\n/**\r\n * Forwards log entries to Sentry.\r\n *\r\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\r\n * **events**: an `Error` message via `captureException` (preserving the real\r\n * stack), any other message via `captureMessage`. Every other level becomes a\r\n * **breadcrumb** — buffered and attached to the next event, consuming no error\r\n * quota. `module` / `action` are attached as searchable tags and the entry's\r\n * `context` as a structured Sentry context.\r\n *\r\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\r\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\r\n * graceful shutdown, `await log.flush()` drains pending events via\r\n * `Sentry.flush(timeout)`.\r\n *\r\n * @example\r\n * // Existing app — reuse the initialized Sentry client\r\n * import * as Sentry from \"@sentry/node\";\r\n * log.addChannel(new SentryLog({ client: Sentry }));\r\n *\r\n * @example\r\n * // New app — let the channel initialize Sentry\r\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\r\n */\r\nexport class SentryLog extends LogChannel<SentryLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"sentry\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public description =\r\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected defaultConfigurations: SentryLogConfig = {\r\n eventLevels: [\"fatal\", \"error\", \"warn\"],\r\n flushTimeout: 2_000,\r\n };\r\n\r\n /**\r\n * The resolved forwarder — the injected `client` or the lazily-imported\r\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\r\n * SDK is absent (then `log()` surfaces the install message once).\r\n */\r\n private sentry?: SentryForwarder;\r\n\r\n /**\r\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\r\n * doesn't spam stderr on every entry.\r\n */\r\n private warnedMissing = false;\r\n\r\n /**\r\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\r\n * entry logged on the same tick as construction — e.g. at app boot, before\r\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\r\n * silently dropped. The `options` (lazy-import) path is inherently async and\r\n * still resolves in `init()`.\r\n */\r\n public constructor(configurations?: SentryLogConfig) {\r\n super(configurations);\r\n\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\r\n * `@sentry/node` and (only when explicit `options` are supplied and no client\r\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\r\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\r\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\r\n */\r\n protected async init(): Promise<void> {\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n\r\n return;\r\n }\r\n\r\n await loadSentry();\r\n\r\n if (!Sentry) {\r\n return;\r\n }\r\n\r\n const options = this.config(\"options\");\r\n\r\n if (options && !Sentry.getClient()) {\r\n Sentry.init(options);\r\n }\r\n\r\n this.sentry = Sentry;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData): Promise<void> {\r\n if (!this.shouldBeLogged(data)) {\r\n return;\r\n }\r\n\r\n if (isModuleExists === null) {\r\n // wait until module is fully loaded\r\n await loadSentry();\r\n }\r\n\r\n if (!this.sentry) {\r\n this.reportMissingSdk();\r\n\r\n return;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (this.isEventLevel(level)) {\r\n this.captureEvent(this.sentry, {\r\n module,\r\n action,\r\n message,\r\n level,\r\n context,\r\n });\r\n\r\n return;\r\n }\r\n\r\n this.sentry.addBreadcrumb({\r\n category: module,\r\n message: this.toText(message),\r\n level: this.toSentryLevel(level),\r\n data: context,\r\n });\r\n }\r\n\r\n /**\r\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\r\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\r\n */\r\n public async flush(): Promise<void> {\r\n if (!this.sentry) {\r\n return;\r\n }\r\n\r\n await this.sentry.flush(this.config(\"flushTimeout\"));\r\n }\r\n\r\n /**\r\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\r\n */\r\n private isEventLevel(level: LogLevel): boolean {\r\n return Boolean(this.config(\"eventLevels\")?.includes(level));\r\n }\r\n\r\n /**\r\n * Send an entry as a Sentry event. An `Error` message goes through\r\n * `captureException` so Sentry parses the real stack and groups properly;\r\n * any other message goes through `captureMessage`. `module` / `action` are\r\n * attached as tags and `context` as a structured context, scoped to this\r\n * event only via `withScope`.\r\n */\r\n private captureEvent(\r\n sentry: SentryForwarder,\r\n entry: {\r\n module: string;\r\n action: string;\r\n message: unknown;\r\n level: LogLevel;\r\n context?: Record<string, any>;\r\n },\r\n ): void {\r\n const { module, action, message, level, context } = entry;\r\n const sentryLevel = this.toSentryLevel(level);\r\n\r\n sentry.withScope((scope) => {\r\n scope.setLevel(sentryLevel);\r\n scope.setTags({ module, action });\r\n\r\n if (context) {\r\n scope.setContext(\"context\", context);\r\n }\r\n\r\n if (message instanceof Error) {\r\n sentry.captureException(message);\r\n } else {\r\n sentry.captureMessage(this.toText(message), sentryLevel);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Map a logger level to a Sentry severity. `success` has no Sentry\r\n * equivalent, so it is reported as informational.\r\n */\r\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\r\n switch (level) {\r\n case \"warn\":\r\n return \"warning\";\r\n case \"success\":\r\n return \"info\";\r\n default:\r\n return level; // debug | info | error | fatal map 1:1\r\n }\r\n }\r\n\r\n /**\r\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\r\n * `.message`, a string as-is, anything else safely JSON-serialized.\r\n */\r\n private toText(message: unknown): string {\r\n if (typeof message === \"string\") {\r\n return message;\r\n }\r\n\r\n if (message instanceof Error) {\r\n return message.message;\r\n }\r\n\r\n return safeJsonStringify(message);\r\n }\r\n\r\n /**\r\n * Surface the install instructions exactly once when the SDK is absent. The\r\n * logger can't log through itself here, so this writes to stderr — matching\r\n * how the file channels report write failures.\r\n */\r\n private reportMissingSdk(): void {\r\n if (isModuleExists === false && !this.warnedMissing) {\r\n this.warnedMissing = true;\r\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\r\n }\r\n }\r\n}\r\n","/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n","import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n","/**\r\n * Clear message from any terminal codes\r\n */\r\nexport function clearMessage(message: any) {\r\n if (typeof message !== \"string\") return message;\r\n\r\n // eslint-disable-next-line no-control-regex\r\n return message.replace(/\\u001b[^m]*?m/g, \"\");\r\n}\r\n","import { Random } from \"@mongez/reinforcements\";\r\nimport type { LogChannel } from \"./log-channel\";\r\nimport { applyRedact, mergeRedact } from \"./redact\";\r\nimport type {\r\n AutoFlushEvent,\r\n LoggingData,\r\n LogLevel,\r\n OmittedLoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\nimport { clearMessage } from \"./utils/clear-message\";\r\n\r\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\r\n \"SIGINT\",\r\n \"SIGTERM\",\r\n \"SIGHUP\",\r\n \"SIGBREAK\",\r\n \"SIGUSR2\",\r\n]);\r\n\r\n/**\r\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\r\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\r\n * and easiest to drop; `error` is the loudest and never dropped by the\r\n * minimum-level filter. `success` sits beside `info` — it's an informational\r\n * outcome, not a warning.\r\n */\r\nconst LEVEL_RANK: Record<LogLevel, number> = {\r\n debug: 0,\r\n info: 1,\r\n success: 1,\r\n warn: 2,\r\n error: 3,\r\n fatal: 4,\r\n};\r\n\r\nexport class Logger {\r\n /**\r\n * Current channel\r\n */\r\n public channels: LogChannel[] = [];\r\n\r\n public id = \"logger-\" + Random.string(32);\r\n\r\n /**\r\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\r\n * calls to `enableAutoFlush` replace rather than stack, and so\r\n * `disableAutoFlush` can remove them cleanly.\r\n */\r\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\r\n\r\n /**\r\n * Logger-wide minimum severity. When set, entries below this level are\r\n * dropped before any channel is invoked — cheaper than per-channel `levels`\r\n * filters because the fan-out loop is skipped entirely. `undefined` means\r\n * no minimum (every entry reaches every channel that accepts it).\r\n */\r\n private minLevel?: LogLevel;\r\n\r\n /**\r\n * Logger-wide redaction floor. Applied once before fan-out — every\r\n * channel receives an entry with these paths already censored. Channel\r\n * configs can extend the path list (additive); they cannot remove paths\r\n * set here.\r\n */\r\n private redactConfig?: RedactConfig;\r\n\r\n /**\r\n * Add a new channel\r\n */\r\n public addChannel(channel: LogChannel) {\r\n this.channels.push(channel);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set base configurations\r\n */\r\n public configure(config: {\r\n channels?: LogChannel[];\r\n autoFlushOn?: AutoFlushEvent[];\r\n minLevel?: LogLevel;\r\n redact?: RedactConfig;\r\n }) {\r\n if (config.channels) {\r\n this.channels = config.channels;\r\n }\r\n\r\n if (config.autoFlushOn) {\r\n this.enableAutoFlush(config.autoFlushOn);\r\n }\r\n\r\n if (config.minLevel !== undefined) {\r\n this.setMinLevel(config.minLevel);\r\n }\r\n\r\n if (config.redact !== undefined) {\r\n this.setRedact(config.redact);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the logger-wide redaction floor. Applied to every entry before\r\n * fan-out; channel configs add more paths on top, never fewer. Pass\r\n * `undefined` to clear.\r\n *\r\n * @example\r\n * log.setRedact({\r\n * paths: [\"context.password\", \"context.*.token\"],\r\n * censor: \"[REDACTED]\",\r\n * });\r\n */\r\n public setRedact(config: RedactConfig | undefined): this {\r\n this.redactConfig = config;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active logger-wide redact config (or `undefined`).\r\n */\r\n public getRedact(): RedactConfig | undefined {\r\n return this.redactConfig;\r\n }\r\n\r\n /**\r\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\r\n * than per-channel `levels` filters because the loop never runs and no\r\n * channel receives the entry. Pass `undefined` to clear and accept all\r\n * levels again.\r\n *\r\n * @example\r\n * // production: silence debug noise everywhere at once\r\n * logger.setMinLevel(\"info\");\r\n */\r\n public setMinLevel(level: LogLevel | undefined): this {\r\n this.minLevel = level;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active minimum severity (or `undefined` when none is set).\r\n */\r\n public getMinLevel(): LogLevel | undefined {\r\n return this.minLevel;\r\n }\r\n\r\n /**\r\n * Set channels\r\n */\r\n public setChannels(channels: LogChannel[]) {\r\n this.channels = channels;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Normalize log data to a single object\r\n */\r\n private normalizeLogData(\r\n dataOrModule: LoggingData | OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n level?: LogLevel,\r\n context?: Record<string, any>,\r\n ): LoggingData {\r\n if (typeof dataOrModule === \"object\") {\r\n // If level is provided, override type\r\n return {\r\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\r\n module: dataOrModule.module,\r\n action: dataOrModule.action,\r\n message: dataOrModule.message,\r\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\r\n };\r\n }\r\n return {\r\n type: (level || \"info\") as LogLevel,\r\n module: dataOrModule,\r\n action: action as string,\r\n message,\r\n ...(context ? { context } : {}),\r\n };\r\n }\r\n\r\n /**\r\n * Make log\r\n *\r\n * Fans out a single log entry to every registered channel. Non-terminal\r\n * channels receive a copy whose `message` has had ANSI color codes stripped\r\n * — each channel sees its own shallow clone so one channel cannot observe\r\n * another's mutations (e.g. a later terminal channel still sees the original\r\n * colored message).\r\n */\r\n public async log(data: LoggingData) {\r\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\r\n return this;\r\n }\r\n\r\n // Apply the logger-wide redact floor once. Every channel sees the\r\n // result; no channel can undo a logger-wide redaction (additive-only\r\n // semantics).\r\n const baseEntry = applyRedact(data, this.redactConfig);\r\n\r\n for (const channel of this.channels) {\r\n const channelRedact = channel.getRedactConfig?.();\r\n const effectiveRedact = channelRedact\r\n ? mergeRedact(this.redactConfig, channelRedact)\r\n : undefined;\r\n\r\n // When the channel adds paths, redact again from `data` rather than\r\n // from `baseEntry` so the merged config (which already contains the\r\n // logger-wide paths) does the full pass — avoids double-cloning the\r\n // already-redacted base.\r\n let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;\r\n\r\n if (channel.terminal === false) {\r\n payload = { ...payload, message: clearMessage(payload.message) };\r\n }\r\n\r\n channel.log(payload);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Make debug log\r\n */\r\n public debug(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make info log\r\n */\r\n public info(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make warn log\r\n */\r\n public warn(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make error log\r\n */\r\n public error(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make success log\r\n */\r\n public success(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make fatal log — for unrecoverable failures where the application is going\r\n * down (failed bootstrap, lost connection to a required dependency that the\r\n * caller has decided not to retry, an `uncaughtException`).\r\n *\r\n * Identical shape to {@link error}; the level is purely informational —\r\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\r\n * `await log.flush()` and `process.exit(...)`.\r\n */\r\n public fatal(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\r\n * entry is never built and channels are not invoked, so this is genuinely\r\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\r\n * through the logger pipeline so persistent channels capture failures.\r\n *\r\n * @example\r\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\r\n */\r\n public assert(\r\n condition: unknown,\r\n module: string,\r\n action: string,\r\n message: any,\r\n context?: Record<string, any>,\r\n ): Promise<Logger> | Logger {\r\n if (condition) return this;\r\n return this.error(module, action, message, context);\r\n }\r\n\r\n /**\r\n * Start a duration timer. The returned function emits an `info` entry\r\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\r\n * called. Pass an object to `end()` to merge extra fields into context.\r\n *\r\n * @example\r\n * const end = log.timer(\"db\", \"users.findById\");\r\n * const user = await usersRepo.findById(id);\r\n * end({ id, found: !!user });\r\n */\r\n public timer(\r\n module: string,\r\n action: string,\r\n ): (extra?: Record<string, any>) => Promise<Logger> {\r\n const startedAt = Date.now();\r\n return (extra?: Record<string, any>) => {\r\n const durationMs = Date.now() - startedAt;\r\n return this.info(module, action, `completed in ${durationMs}ms`, {\r\n durationMs,\r\n ...(extra ?? {}),\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Get channel by name\r\n */\r\n public channel(name: string) {\r\n return this.channels.find((channel) => channel.name === name);\r\n }\r\n\r\n /**\r\n * Synchronously flush logs\r\n */\r\n public flushSync() {\r\n for (const channel of this.channels) {\r\n if (channel.flushSync) {\r\n channel.flushSync();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Asynchronously drain every channel that implements `flush()`.\r\n *\r\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\r\n * correct call for a graceful shutdown that can afford to wait\r\n * (`await log.flush()` after closing the HTTP server, before\r\n * `process.exit`). A channel whose delivery is async (a network transport,\r\n * an async disk write) implements `flush()`, not `flushSync()`.\r\n *\r\n * Channels are isolated: a channel whose flush rejects can neither prevent\r\n * the others from draining nor escape as an unhandled rejection. Channels\r\n * without `flush()` are skipped.\r\n *\r\n * @example\r\n * async function shutdown() {\r\n * await httpServer.close();\r\n * await log.flush();\r\n * process.exit(0);\r\n * }\r\n */\r\n public async flush(): Promise<void> {\r\n await Promise.allSettled(\r\n this.channels.map(async (channel) => {\r\n if (!channel.flush) {\r\n return;\r\n }\r\n\r\n try {\r\n await channel.flush();\r\n } catch {\r\n // A single channel must never break shutdown for the others —\r\n // a graceful drain is best-effort across every channel.\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Register one process-level handler per event that calls `flushSync()`\r\n * before the process terminates.\r\n *\r\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\r\n * the handler flushes and then re-raises the signal so Node's default exit\r\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\r\n * naturally afterwards.\r\n *\r\n * Idempotent: calling with the same events replaces the previous handlers.\r\n * Call `disableAutoFlush()` to unregister.\r\n *\r\n * @example\r\n * log.configure({\r\n * channels: [new ConsoleLog(), new FileLog()],\r\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\r\n * });\r\n */\r\n public enableAutoFlush(events: AutoFlushEvent[]): this {\r\n this.disableAutoFlush();\r\n\r\n for (const event of events) {\r\n const handler = SIGNAL_EVENTS.has(event)\r\n ? () => {\r\n this.flushSync();\r\n process.off(event, handler);\r\n process.kill(process.pid, event as NodeJS.Signals);\r\n }\r\n : () => {\r\n this.flushSync();\r\n };\r\n\r\n process.on(event, handler);\r\n this.autoFlushHandlers.set(event, handler);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove every handler previously registered by `enableAutoFlush`.\r\n * Safe to call when no handlers are registered.\r\n */\r\n public disableAutoFlush(): this {\r\n for (const [event, handler] of this.autoFlushHandlers) {\r\n process.off(event, handler);\r\n }\r\n\r\n this.autoFlushHandlers.clear();\r\n\r\n return this;\r\n }\r\n}\r\n\r\n/**\r\n * The package singleton. Use this for everyday logging — `log.info(...)`,\r\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\r\n * created by instantiating `Logger` directly.\r\n *\r\n * The name is intentionally short: `log` reads naturally at the call site\r\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\r\n * pino, bunyan, and most JS logging tutorials.\r\n *\r\n * Note that `log` is a `Logger` instance, **not** a function — the bare\r\n * callable form was removed when the dual `log` / `logger` exports were\r\n * collapsed into a single name. Use `log.info(...)` (or any other level\r\n * shortcut) to emit entries.\r\n */\r\nexport const log = new Logger();\r\n","import { log } from \"../logger\";\r\n\r\n/**\r\n * Options for {@link captureAnyUnhandledRejection}.\r\n */\r\nexport type CaptureUnhandledOptions = {\r\n /**\r\n * Take the process down with `process.exit(1)` after an `uncaughtException`\r\n * is logged. Defaults to `true` — an uncaught exception leaves the process\r\n * in an undefined state, so Node's own default is to exit non-zero.\r\n *\r\n * Registering an `uncaughtException` listener *suppresses* that default, so a\r\n * listener that only logs turns an unrecoverable crash into a silent\r\n * `exit 0` (which is how a config file throwing at boot looks like \"the\r\n * server just stopped\"). Keep this `true` in production so supervisors\r\n * restart and the failure is never silent. Set it to `false` where the\r\n * process is expected to recover on its own — e.g. a dev server that reloads\r\n * via HMR — to log the exception without exiting.\r\n */\r\n exitOnUncaughtException?: boolean;\r\n};\r\n\r\n/**\r\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\r\n * the forced exit. Bounded so a single stuck channel can't hang the process —\r\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\r\n * handler would not cover this path.\r\n */\r\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context, and make an\r\n * `uncaughtException` loud + visible instead of silently swallowed.\r\n *\r\n * Registers one listener for `unhandledRejection` and one for\r\n * `uncaughtException`; call once at startup after channels are configured.\r\n *\r\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\r\n * When no terminal channel has been configured yet — the early-boot window a\r\n * config file throwing at import time falls into — the stack is also written\r\n * to `console.error`, standing in for the Node default this listener\r\n * suppresses, so a fatal boot error is never invisible. A configured\r\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\r\n * channel exists (no double output).\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\r\n * not necessarily process-ending, so it stays at error and never exits.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection(); // exits on uncaught\r\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\r\n */\r\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\r\n const { exitOnUncaughtException = true } = options;\r\n\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n // Route through the logger first so file/Sentry channels capture it. A\r\n // crash handler must never itself throw (that would re-enter Node's\r\n // uncaught path), so the logging call is guarded.\r\n try {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n } catch {\r\n // fall through to the stderr fallback + exit below\r\n }\r\n\r\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\r\n // see when this fires before any terminal channel is configured — exactly\r\n // the early-boot window a config file throwing at import time falls into,\r\n // which is how a fatal boot error used to masquerade as a silent\r\n // `exit 0`. Stand in for the Node default this listener suppressed, but\r\n // only when no terminal channel already printed it, so a configured\r\n // `ConsoleLog` is never doubled.\r\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\r\n\r\n if (!hasTerminalChannel) {\r\n console.error(error);\r\n }\r\n\r\n if (!exitOnUncaughtException) {\r\n return;\r\n }\r\n\r\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\r\n // process supervisors restart and `warlock start` surfaces the failure.\r\n void Promise.race([\r\n log.flush(),\r\n new Promise<void>((resolve) => {\r\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\r\n timer.unref?.();\r\n }),\r\n ]).then(\r\n () => process.exit(1),\r\n () => process.exit(1),\r\n );\r\n });\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["colors","EOL","Random"],"sources":["../../../../../../logger/src/log-channel.ts","../../../../../../logger/src/channels/console-log.ts","../../../../../../logger/src/channels/file-log.ts","../../../../../../logger/src/utils/safe-json-stringify.ts","../../../../../../logger/src/channels/json-file-log.ts","../../../../../../logger/src/channels/sentry-log.ts","../../../../../../logger/src/redact/default-keys.ts","../../../../../../logger/src/redact/redact.ts","../../../../../../logger/src/utils/clear-message.ts","../../../../../../logger/src/logger.ts","../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\n\r\nexport abstract class LogChannel<\r\n Options extends BasicLogConfigurations = BasicLogConfigurations,\r\n> implements LogContract {\r\n /**\r\n * Channel name\r\n */\r\n public name!: string;\r\n\r\n /**\r\n * Channel description\r\n */\r\n public description?: string;\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = false;\r\n\r\n /**\r\n * Default Configurations\r\n */\r\n protected defaultConfigurations: Options = {} as Options;\r\n\r\n /**\r\n * Channel configurations\r\n */\r\n protected channelConfigurations: Options = {} as Options; //\r\n\r\n /**\r\n * Determine whether the channel is fully initialized\r\n */\r\n protected isInitialized = false;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(configurations?: Options) {\r\n if (configurations) {\r\n this.setConfigurations(configurations);\r\n }\r\n\r\n setTimeout(async () => {\r\n if (this.init) {\r\n await this.init();\r\n }\r\n\r\n this.isInitialized = true;\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Initialize the channel\r\n */\r\n protected init?(): void | Promise<void>;\r\n\r\n /**\r\n * Get config value\r\n */\r\n protected config<K extends keyof Options>(key: K): Options[K] {\r\n return (\r\n this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key]\r\n );\r\n }\r\n\r\n /**\r\n * Set configurations\r\n */\r\n protected setConfigurations(configurations: Options) {\r\n this.channelConfigurations = {\r\n ...this.channelConfigurations,\r\n ...configurations,\r\n };\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Determine if the message should be logged\r\n */\r\n protected shouldBeLogged(data: LoggingData): boolean {\r\n // check for debug mode\r\n const allowedLevels = this.config(\"levels\");\r\n\r\n if (allowedLevels?.length && !allowedLevels.includes(data.type))\r\n return false;\r\n\r\n const filter = this.config(\"filter\");\r\n\r\n if (filter) {\r\n return filter(data);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Log the given message\r\n */\r\n public abstract log(data: LoggingData): void | Promise<void>;\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync?(): void;\r\n\r\n /**\r\n * Asynchronously flush buffered messages.\r\n *\r\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\r\n * don't implement it. Buffering channels override this to drain their buffer\r\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\r\n */\r\n public flush?(): void | Promise<void>;\r\n\r\n /**\r\n * Read the channel's redact config (if any). Used by `Logger` to apply\r\n * per-channel additive redaction on top of the logger-wide floor.\r\n * Subclasses normally don't override this — set `redact` in your channel\r\n * configuration instead.\r\n */\r\n public getRedactConfig(): RedactConfig | undefined {\r\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\r\n }\r\n\r\n /**\r\n * Get date and time formats\r\n */\r\n protected getDateAndTimeFormat() {\r\n const dateFormat = this.config(\"dateFormat\");\r\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\r\n const time = dateFormat?.time ?? \"HH:mm:ss\";\r\n\r\n return { date, time };\r\n }\r\n\r\n /**\r\n * get basic configurations with the given ones\r\n */\r\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\r\n return {\r\n filter: () => true,\r\n ...configurations,\r\n } as any as Options;\r\n }\r\n}\r\n","import { colors } from \"@mongez/copper\";\r\nimport { inspect } from \"util\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\n\r\nexport type ConsoleLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Render the log entry's `context` object on a second line after the main\r\n * message. When `false`, context is silently dropped (the historical\r\n * behavior). When `true`, contexts are pretty-printed with `util.inspect`\r\n * — colored, depth-limited, ideal for development. Persistent channels\r\n * (`FileLog`, `JSONFileLog`) always retain context regardless of this flag.\r\n *\r\n * @default false\r\n */\r\n showContext?: boolean;\r\n /**\r\n * Depth passed to `util.inspect` when rendering context. Only applies when\r\n * `showContext` is enabled.\r\n *\r\n * @default 4\r\n */\r\n contextDepth?: number;\r\n};\r\n\r\n/**\r\n * The longest level name (`success`) sets the tag column width. Padding every\r\n * level name to this width keeps the timestamp / module / action columns\r\n * aligned across lines. The level icons are bare (text-style, single-cell)\r\n * glyphs, so padding the name alone produces true columns.\r\n */\r\nconst LEVEL_NAME_WIDTH = 7;\r\n\r\n/**\r\n * Per-level console presentation: the colored, fixed-width `{icon} {name}` tag\r\n * and the function that colors the message body.\r\n */\r\ntype LevelStyle = {\r\n tag: string;\r\n message: (message: LoggingData[\"message\"]) => string;\r\n};\r\n\r\n/**\r\n * Build a single-color, fixed-width tag — `{icon} {name}` with the name padded\r\n * so every tag spans the same number of columns.\r\n */\r\nfunction buildTag(\r\n icon: string,\r\n name: string,\r\n color: (text: string) => string,\r\n): string {\r\n return color(`${icon} ${name.padEnd(LEVEL_NAME_WIDTH)}`);\r\n}\r\n\r\n/**\r\n * Console styling per log level. The `fatal` tag is a bright-red background\r\n * badge — the same column width as the others (`\" ☠ fatal \"` is 9 cells, like\r\n * `{icon} {name.padEnd(7)}`), but impossible to miss in a wall of red `error`\r\n * lines, where a plain red tag would blend in.\r\n */\r\nconst LEVEL_STYLES: Record<LogLevel, LevelStyle> = {\r\n debug: {\r\n tag: buildTag(\"⚙\", \"debug\", colors.magentaBright),\r\n message: colors.magentaBright,\r\n },\r\n info: {\r\n tag: buildTag(\"ℹ\", \"info\", colors.blueBright),\r\n message: colors.blueBright,\r\n },\r\n warn: {\r\n tag: buildTag(\"⚠\", \"warn\", colors.yellow),\r\n message: colors.yellowBright,\r\n },\r\n error: {\r\n tag: buildTag(\"✗\", \"error\", colors.red),\r\n message: colors.redBright,\r\n },\r\n success: {\r\n tag: buildTag(\"✓\", \"success\", colors.green),\r\n message: colors.greenBright,\r\n },\r\n fatal: {\r\n tag: colors.bgRedBright(colors.whiteBright(colors.bold(\" ☠ fatal \"))),\r\n message: colors.redBright.bold,\r\n },\r\n};\r\n\r\n/**\r\n * Fallback for an unrecognized level — a plain `[log]` tag padded to the same\r\n * width so the columns stay aligned.\r\n */\r\nconst DEFAULT_STYLE: LevelStyle = {\r\n tag: \"[log]\".padEnd(LEVEL_NAME_WIDTH + 2),\r\n message: message => message as string,\r\n};\r\n\r\nexport class ConsoleLog extends LogChannel<ConsoleLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"console\";\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = true;\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public log(data: LoggingData) {\r\n const { module, action, message, type: level } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n // Console shows time-only (HH:mm:ss.SSS) — within a dev session the date\r\n // rarely changes, so the full ISO date + `T`/`Z` are just noise. Persistent\r\n // channels (FileLog / JSONFileLog) keep the full ISO timestamp.\r\n const time = new Date().toISOString().slice(11, 23); // i.e. \"10:22:00.000\"\r\n\r\n const style = LEVEL_STYLES[level] ?? DEFAULT_STYLE;\r\n\r\n // The slate timestamp recedes so the colored level + message lead the eye;\r\n // module (cyan) and action (magenta) stay colored for subsystem scanning.\r\n // `slate` (256-color neutral gray) reads cleaner than bright-black `gray`,\r\n // which sits too close to the background on most terminal themes.\r\n console.log(\r\n style.tag,\r\n colors.slate(`(${time})`),\r\n colors.cyan(`[${module}]`),\r\n colors.magenta(`[${action}]`),\r\n style.message(message),\r\n );\r\n\r\n if (typeof message === \"object\") {\r\n console.log(message);\r\n }\r\n\r\n // Render context on a second line when explicitly enabled. We only\r\n // attempt rendering if there's anything meaningful to show — empty\r\n // objects clutter the terminal without adding signal.\r\n if (this.config(\"showContext\") && data.context && Object.keys(data.context).length > 0) {\r\n const depth = this.config(\"contextDepth\") ?? 4;\r\n console.log(\r\n colors.slate(\" ↳\"),\r\n inspect(data.context, { colors: true, depth, breakLength: 80 }),\r\n );\r\n }\r\n }\r\n}\r\n","import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport { EOL } from \"os\";\r\nimport path from \"path\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n LogLevel,\r\n LogMessage,\r\n} from \"../types\";\r\n\r\n// TODO: Add max messages per file before rotation\r\n\r\nexport type FileLogConfig = BasicLogConfigurations & {\r\n storagePath?: string;\r\n /**\r\n * File name, without extension\r\n */\r\n name?: string;\r\n /**\r\n * chunk mode\r\n * If set to `single`, the logs will be created in a single file, unless the rotate is set to true\r\n * If set to `daily`, the logs will be created in a daily file, unless the rotate is set to true\r\n * If set to `hourly`, the logs will be created in an hourly file, unless the rotate is set to true\r\n * @default single\r\n */\r\n chunk?: \"single\" | \"daily\" | \"hourly\";\r\n /**\r\n * Whether to rotate the file\r\n *\r\n * @default true\r\n */\r\n rotate?: boolean;\r\n /**\r\n * File Extension\r\n *\r\n * @default log\r\n */\r\n extension?: string;\r\n /**\r\n * If rotate is set, the rotate name will be added to the file name suffixed with `-`\r\n *\r\n * @default DD-MM-YYYY\r\n */\r\n rotateFileName?: string;\r\n /**\r\n * Max file size before rotating the file\r\n *\r\n * @default 10MB\r\n */\r\n maxFileSize?: number;\r\n /**\r\n * Set the max messages that needs to be added before writing to the file\r\n *\r\n * @default 100\r\n */\r\n maxMessagesToWrite?: number;\r\n /**\r\n * Group logs by\r\n * Please note that the order matters here\r\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\r\n *\r\n * @default none\r\n */\r\n groupBy?: (\"level\" | \"module\" | \"action\")[];\r\n /**\r\n * Define what levels should be logged\r\n *\r\n * @default all\r\n */\r\n levels?: LogLevel[];\r\n /**\r\n * Date and time format\r\n */\r\n dateFormat?: {\r\n date?: string;\r\n time?: string;\r\n };\r\n};\r\n\r\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"file\";\r\n\r\n /**\r\n * Messages buffer\r\n */\r\n protected messages: LogMessage[] = [];\r\n\r\n /**\r\n * Grouped messages\r\n */\r\n protected groupedMessages: Record<string, LogMessage[]> = {};\r\n\r\n /**\r\n * Default channel configurations\r\n */\r\n protected defaultConfigurations: FileLogConfig = {\r\n storagePath: process.cwd() + \"/storage/logs\",\r\n rotate: true,\r\n name: \"app\",\r\n extension: \"log\",\r\n chunk: \"single\",\r\n maxMessagesToWrite: 100,\r\n filter: () => true,\r\n maxFileSize: 10 * 1024 * 1024, // 10MB\r\n get rotateFileName() {\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n },\r\n dateFormat: {\r\n date: \"DD-MM-YYYY\",\r\n time: \"HH:mm:ss\",\r\n },\r\n };\r\n\r\n /**\r\n * Last write time\r\n */\r\n protected lastWriteTime = Date.now();\r\n\r\n /**\r\n * A flag to determine if the file is being written\r\n */\r\n protected isWriting = false;\r\n\r\n /**\r\n * Handle for the periodic flush interval. Stored so it can be cleared\r\n * in `dispose()` — long-lived processes that create channels dynamically\r\n * would otherwise leak one timer per channel.\r\n */\r\n protected flushIntervalHandle?: NodeJS.Timeout;\r\n\r\n /**\r\n * Check file size for file rotation\r\n */\r\n protected async checkAndRotateFile(filePath = this.filePath) {\r\n if (!this.config(\"rotate\")) return;\r\n\r\n try {\r\n const stats = await fs.promises.stat(filePath);\r\n if (stats.size >= this.config(\"maxFileSize\")!) {\r\n await this.rotateLogFile();\r\n }\r\n } catch (error: any) {\r\n // ENOENT is expected when the file hasn't been created yet — there is\r\n // nothing to rotate, so stay silent. Surface anything else.\r\n if (error.code !== \"ENOENT\") {\r\n console.error(\"Error checking log file:\", error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Rotate log file\r\n */\r\n protected async rotateLogFile() {\r\n const fileName = `${this.fileName}-${this.config(\"rotateFileName\")}-${Date.now()}`;\r\n\r\n const extension = this.extension;\r\n\r\n const rotatedFilePath = path.join(this.storagePath, `${fileName}.${extension}`);\r\n\r\n await fs.promises.rename(this.filePath, rotatedFilePath).catch((error) => {\r\n console.error(\"Error rotating file:\", error);\r\n });\r\n }\r\n\r\n /**\r\n * Flush messages\r\n *\r\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\r\n * entries indefinitely. The handle is stored on the instance so `dispose()`\r\n * can stop it — without this, every channel leaks a timer for the lifetime\r\n * of the process.\r\n */\r\n protected initMessageFlush() {\r\n this.flushIntervalHandle = setInterval(() => {\r\n if (\r\n this.messages.length > 0 &&\r\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\r\n ) {\r\n this.writeMessagesToFile();\r\n }\r\n }, 5000);\r\n }\r\n\r\n /**\r\n * Stop the background flush interval and drain any buffered entries.\r\n *\r\n * Call this when discarding a channel (e.g. reconfiguring the logger at\r\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\r\n * call more than once.\r\n */\r\n public dispose(): void {\r\n if (this.flushIntervalHandle) {\r\n clearInterval(this.flushIntervalHandle);\r\n this.flushIntervalHandle = undefined;\r\n }\r\n\r\n this.flushSync();\r\n }\r\n\r\n /**\r\n * Get file path\r\n */\r\n public get filePath() {\r\n const fileName = this.fileName;\r\n\r\n const extension = this.extension;\r\n\r\n return path.join(this.storagePath, `${fileName}.${extension}`);\r\n }\r\n\r\n /**\r\n * Get max messages\r\n */\r\n protected get maxMessagesToWrite(): number {\r\n return this.config(\"maxMessagesToWrite\")!;\r\n }\r\n\r\n /**\r\n * Get file name\r\n */\r\n public get fileName(): string {\r\n const debugLevel = this.config(\"chunk\")!;\r\n\r\n switch (debugLevel) {\r\n case \"single\":\r\n default:\r\n return this.config(\"name\")!;\r\n case \"daily\":\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n case \"hourly\":\r\n return dayjs().format(\"DD-MM-YYYY-HH-00-00-a\");\r\n }\r\n }\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return this.config(\"extension\")!;\r\n }\r\n\r\n /**\r\n * Get content\r\n */\r\n protected get content() {\r\n return this.messages.map((message) => message.content).join(EOL) + EOL;\r\n }\r\n\r\n /**\r\n * Get storage path\r\n */\r\n public get storagePath(): string {\r\n return this.config(\"storagePath\")!;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected async init() {\r\n const logsDirectory = this.storagePath;\r\n\r\n await ensureDirectoryAsync(logsDirectory);\r\n\r\n this.initMessageFlush();\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n fs.appendFileSync(filePath, content);\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n fs.appendFileSync(this.filePath, this.content);\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * Asynchronously drain buffered entries to disk.\r\n *\r\n * The async analogue of {@link flushSync}: it reuses the same async writer\r\n * as the background interval, so a caller on a graceful-shutdown path can\r\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\r\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\r\n * its overridden `writeMessagesToFile` performs the JSON merge.\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\r\n return;\r\n }\r\n\r\n // Clear the in-flight lock so a deliberate drain is never short-circuited\r\n // by a half-finished background write, then await the async writer to\r\n // completion — unlike the fire-and-forget interval, callers depend on the\r\n // buffer reaching disk before this resolves.\r\n this.isWriting = false;\r\n\r\n await this.writeMessagesToFile();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n let content = `[${date}] [${level}] [${module}][${action}]: `;\r\n\r\n let stack: string | undefined;\r\n\r\n // check if message is an instance of Error\r\n if (message instanceof Error) {\r\n // in that case we need to store the error message and stack trace\r\n content += message.message + EOL;\r\n content += `[trace]` + EOL;\r\n content += message.stack;\r\n stack = message.stack;\r\n } else {\r\n content += message;\r\n }\r\n\r\n this.messages.push({\r\n content,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n });\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Check if messages should be written\r\n */\r\n protected async checkIfMessagesShouldBeWritten() {\r\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\r\n await this.writeMessagesToFile();\r\n }\r\n }\r\n\r\n /**\r\n * Should be called after messages are saved\r\n */\r\n protected onSave() {\r\n this.messages = [];\r\n this.groupedMessages = {};\r\n this.isWriting = false;\r\n this.lastWriteTime = Date.now();\r\n }\r\n\r\n /**\r\n * Check if messages should be grouped\r\n */\r\n protected get messagedShouldBeGrouped(): boolean {\r\n return Number(this.config(\"groupBy\")?.length) > 0;\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile() {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure we check file size before writing\r\n\r\n try {\r\n await this.write(this.filePath, this.content);\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n\r\n try {\r\n await this.write(filePath, content);\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n }\r\n }\r\n\r\n this.onSave();\r\n this.isWriting = false;\r\n }\r\n\r\n /**\r\n * Prepare grouped messages\r\n */\r\n protected prepareGroupedMessages(): void {\r\n this.messages.forEach((message) => {\r\n const key = this.config(\"groupBy\")!\r\n .map((groupKey) => encodeURIComponent(message[groupKey]))\r\n .join(\"/\");\r\n\r\n this.groupedMessages[key] = this.groupedMessages[key] || [];\r\n this.groupedMessages[key].push(message);\r\n });\r\n }\r\n\r\n /**\r\n * Start writing to the file\r\n */\r\n protected async write(filePath: string, content: string) {\r\n return new Promise((resolve, reject) => {\r\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\r\n\r\n writer.write(content, (error) => {\r\n writer.end();\r\n if (error) {\r\n reject(error);\r\n } else {\r\n resolve(true);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n","import { stringify as safeStableStringify } from \"safe-stable-stringify\";\n\n/**\n * Replacer that surfaces Error data — `name`, `message`, and `stack` are\n * non-enumerable on `Error`, so neither default JSON serialization nor an\n * object spread captures them (both produce `{}`). They are copied explicitly;\n * the trailing spread then captures any additional enumerable props the caller\n * (or a subclass) attached, such as a `code` field.\n */\nfunction errorReplacer<Value = unknown>(\n _key: string,\n value: Value,\n): Record<string, unknown> | Value {\n if (value instanceof Error) {\n // Spread first, then the explicit Error fields. `name`/`message`/`stack`\n // are non-enumerable on `Error`, so the spread never carries them — the\n // explicit assignments are what surface them. Placing the spread first\n // means those explicit keys legitimately take precedence over any\n // identically-named *enumerable* prop a subclass attached (resolving the\n // duplicate-key warning). Because `safe-stable-stringify` sorts keys, the\n // insertion order here does not affect the serialized bytes.\n return {\n ...value,\n name: value.name,\n message: value.message,\n stack: value.stack,\n };\n }\n\n return value;\n}\n\n/**\n * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated\n * non-tree references are handled by `safe-stable-stringify`; functions and\n * symbols are dropped (standard JSON behavior); Errors are expanded via\n * `errorReplacer`. Class instances serialize as their enumerable props.\n *\n * @example\n * await fs.promises.writeFile(filePath, safeJsonStringify(payload, 2));\n */\nexport function safeJsonStringify(value: unknown, space?: number): string {\n return safeStableStringify(value, errorReplacer, space) ?? \"\";\n}\n","import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport path from \"path\";\r\nimport type { LogContract, LogMessage, LoggingData } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\nimport { FileLog } from \"./file-log\";\r\n\r\nexport class JSONFileLog extends FileLog implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"fileJson\";\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return \"json\";\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n fs.writeFileSync(filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(this.filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(this.filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.messages);\r\n fs.writeFileSync(this.filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n let stack: string[] | undefined;\r\n\r\n if (data.message instanceof Error) {\r\n stack = data.message.stack?.split(\"\\n\");\r\n data.message = data.message.message;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n this.messages.push({\r\n content: message,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n } as LogMessage);\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile(): Promise<void> {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure file rotation is handled\r\n\r\n let fileContents = { messages: [] as any[] };\r\n\r\n if (await fileExistsAsync(this.filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(this.filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n } else {\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n\r\n fileContents.messages.push(...this.messages);\r\n\r\n try {\r\n await fs.promises.writeFile(this.filePath, safeJsonStringify(fileContents, 2));\r\n\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n let fileContents: { messages: any[] } = { messages: [] };\r\n if (await fileExistsAsync(filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] };\r\n }\r\n } else {\r\n fileContents = { messages: [] };\r\n }\r\n\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n\r\n try {\r\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n this.onSave();\r\n }\r\n}\r\n","import { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\n\r\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\r\n// @sentry/node is referenced ONLY through the local minimal types below — never\r\n// via `typeof import(\"@sentry/node\")`. This package is served as source\r\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\r\n// program; a static type reference to the SDK would force module resolution and\r\n// break every consumer who (correctly) never installs the optional peer with a\r\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\r\n// the same reason, and its result is cast to these shapes. The shapes mirror the\r\n// stable `@sentry/node` public API (unchanged across v8–v10).\r\n\r\n/**\r\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\r\n */\r\ntype SentrySeverityLevel =\r\n | \"fatal\"\r\n | \"error\"\r\n | \"warning\"\r\n | \"log\"\r\n | \"info\"\r\n | \"debug\";\r\n\r\n/**\r\n * The Sentry `Scope` surface used while building a single event.\r\n */\r\nexport interface SentryScopeLike {\r\n setLevel(level: SentrySeverityLevel): void;\r\n setTags(tags: Record<string, string>): void;\r\n setContext(name: string, context: Record<string, unknown> | null): void;\r\n}\r\n\r\n/**\r\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\r\n */\r\nexport type SentryBreadcrumb = {\r\n category?: string;\r\n message?: string;\r\n level?: SentrySeverityLevel;\r\n data?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\r\n * namespace satisfies this shape, so an app can pass it straight through as\r\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\r\n */\r\nexport interface SentryForwarder {\r\n captureException(exception: unknown): string;\r\n captureMessage(message: string, level?: SentrySeverityLevel): string;\r\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\r\n withScope(callback: (scope: SentryScopeLike) => void): void;\r\n flush(timeout?: number): Promise<boolean>;\r\n}\r\n\r\n/**\r\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\r\n * fields; the index signature keeps any other SDK option valid without coupling\r\n * to the SDK's types.\r\n */\r\nexport type SentryInitOptions = {\r\n dsn?: string;\r\n environment?: string;\r\n release?: string;\r\n sampleRate?: number;\r\n [key: string]: unknown;\r\n};\r\n\r\n/**\r\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\r\n * lifecycle calls the channel uses when it owns initialization.\r\n */\r\ninterface SentryNamespace extends SentryForwarder {\r\n init(options?: SentryInitOptions): unknown;\r\n getClient(): unknown;\r\n}\r\n\r\nlet Sentry: SentryNamespace | undefined;\r\nlet isModuleExists: boolean | null = null;\r\nlet loadingPromise: Promise<void> | undefined;\r\n\r\nconst SENTRY_INSTALL_INSTRUCTIONS = `\r\nThe Sentry log channel requires the @sentry/node package.\r\nInstall it with:\r\n\r\n npm install @sentry/node\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add @sentry/node\r\n yarn add @sentry/node\r\n`.trim();\r\n\r\n/**\r\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\r\n * import failure to \"not installed\" — the curated install message surfaces at\r\n * `log()` time, never as a boot-time module-resolution crash.\r\n */\r\nfunction loadSentry(): Promise<void> {\r\n if (isModuleExists !== null) {\r\n return Promise.resolve();\r\n }\r\n\r\n if (loadingPromise) {\r\n return loadingPromise;\r\n }\r\n\r\n loadingPromise = (async () => {\r\n try {\r\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\r\n // statically resolves the optional peer — see the file header.\r\n const sentryModule: string = \"@sentry/node\";\r\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n })();\r\n\r\n return loadingPromise;\r\n}\r\n\r\nexport type SentryLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\r\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\r\n * channel forwards through it and never imports or re-initializes the SDK.\r\n */\r\n client?: SentryForwarder;\r\n /**\r\n * Initialize Sentry from these options instead of reusing a host client. The\r\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\r\n * guarded so it never clobbers an existing client.\r\n */\r\n options?: SentryInitOptions;\r\n /**\r\n * Levels delivered as Sentry *events* (these consume the error quota). Every\r\n * other level is recorded as a breadcrumb that rides along with the next\r\n * event, costing no quota.\r\n *\r\n * @default [\"fatal\", \"error\", \"warn\"]\r\n */\r\n eventLevels?: LogLevel[];\r\n /**\r\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\r\n *\r\n * @default 2000\r\n */\r\n flushTimeout?: number;\r\n};\r\n\r\n/**\r\n * Forwards log entries to Sentry.\r\n *\r\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\r\n * **events**: an `Error` message via `captureException` (preserving the real\r\n * stack), any other message via `captureMessage`. Every other level becomes a\r\n * **breadcrumb** — buffered and attached to the next event, consuming no error\r\n * quota. `module` / `action` are attached as searchable tags and the entry's\r\n * `context` as a structured Sentry context.\r\n *\r\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\r\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\r\n * graceful shutdown, `await log.flush()` drains pending events via\r\n * `Sentry.flush(timeout)`.\r\n *\r\n * @example\r\n * // Existing app — reuse the initialized Sentry client\r\n * import * as Sentry from \"@sentry/node\";\r\n * log.addChannel(new SentryLog({ client: Sentry }));\r\n *\r\n * @example\r\n * // New app — let the channel initialize Sentry\r\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\r\n */\r\nexport class SentryLog extends LogChannel<SentryLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"sentry\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public description =\r\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected defaultConfigurations: SentryLogConfig = {\r\n eventLevels: [\"fatal\", \"error\", \"warn\"],\r\n flushTimeout: 2_000,\r\n };\r\n\r\n /**\r\n * The resolved forwarder — the injected `client` or the lazily-imported\r\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\r\n * SDK is absent (then `log()` surfaces the install message once).\r\n */\r\n private sentry?: SentryForwarder;\r\n\r\n /**\r\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\r\n * doesn't spam stderr on every entry.\r\n */\r\n private warnedMissing = false;\r\n\r\n /**\r\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\r\n * entry logged on the same tick as construction — e.g. at app boot, before\r\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\r\n * silently dropped. The `options` (lazy-import) path is inherently async and\r\n * still resolves in `init()`.\r\n */\r\n public constructor(configurations?: SentryLogConfig) {\r\n super(configurations);\r\n\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\r\n * `@sentry/node` and (only when explicit `options` are supplied and no client\r\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\r\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\r\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\r\n */\r\n protected async init(): Promise<void> {\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n\r\n return;\r\n }\r\n\r\n await loadSentry();\r\n\r\n if (!Sentry) {\r\n return;\r\n }\r\n\r\n const options = this.config(\"options\");\r\n\r\n if (options && !Sentry.getClient()) {\r\n Sentry.init(options);\r\n }\r\n\r\n this.sentry = Sentry;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData): Promise<void> {\r\n if (!this.shouldBeLogged(data)) {\r\n return;\r\n }\r\n\r\n // Only wait for the optional SDK when we do not already have a forwarder.\r\n // An INJECTED client is resolved synchronously in the constructor, and that is\r\n // the whole point of doing it there: an entry logged on the same tick as\r\n // construction must still send. Awaiting loadSentry() unconditionally undid\r\n // that — with @sentry/node absent (it is an optional peer) the import can take\r\n // seconds, so a log with a perfectly good injected client stalled behind an SDK\r\n // it was never going to use.\r\n if (!this.sentry && isModuleExists === null) {\r\n // wait until module is fully loaded\r\n await loadSentry();\r\n }\r\n\r\n if (!this.sentry) {\r\n this.reportMissingSdk();\r\n\r\n return;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (this.isEventLevel(level)) {\r\n this.captureEvent(this.sentry, {\r\n module,\r\n action,\r\n message,\r\n level,\r\n context,\r\n });\r\n\r\n return;\r\n }\r\n\r\n this.sentry.addBreadcrumb({\r\n category: module,\r\n message: this.toText(message),\r\n level: this.toSentryLevel(level),\r\n data: context,\r\n });\r\n }\r\n\r\n /**\r\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\r\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\r\n */\r\n public async flush(): Promise<void> {\r\n if (!this.sentry) {\r\n return;\r\n }\r\n\r\n await this.sentry.flush(this.config(\"flushTimeout\"));\r\n }\r\n\r\n /**\r\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\r\n */\r\n private isEventLevel(level: LogLevel): boolean {\r\n return Boolean(this.config(\"eventLevels\")?.includes(level));\r\n }\r\n\r\n /**\r\n * Send an entry as a Sentry event. An `Error` message goes through\r\n * `captureException` so Sentry parses the real stack and groups properly;\r\n * any other message goes through `captureMessage`. `module` / `action` are\r\n * attached as tags and `context` as a structured context, scoped to this\r\n * event only via `withScope`.\r\n */\r\n private captureEvent(\r\n sentry: SentryForwarder,\r\n entry: {\r\n module: string;\r\n action: string;\r\n message: unknown;\r\n level: LogLevel;\r\n context?: Record<string, any>;\r\n },\r\n ): void {\r\n const { module, action, message, level, context } = entry;\r\n const sentryLevel = this.toSentryLevel(level);\r\n\r\n sentry.withScope((scope) => {\r\n scope.setLevel(sentryLevel);\r\n scope.setTags({ module, action });\r\n\r\n if (context) {\r\n scope.setContext(\"context\", context);\r\n }\r\n\r\n if (message instanceof Error) {\r\n sentry.captureException(message);\r\n } else {\r\n sentry.captureMessage(this.toText(message), sentryLevel);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Map a logger level to a Sentry severity. `success` has no Sentry\r\n * equivalent, so it is reported as informational.\r\n */\r\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\r\n switch (level) {\r\n case \"warn\":\r\n return \"warning\";\r\n case \"success\":\r\n return \"info\";\r\n default:\r\n return level; // debug | info | error | fatal map 1:1\r\n }\r\n }\r\n\r\n /**\r\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\r\n * `.message`, a string as-is, anything else safely JSON-serialized.\r\n */\r\n private toText(message: unknown): string {\r\n if (typeof message === \"string\") {\r\n return message;\r\n }\r\n\r\n if (message instanceof Error) {\r\n return message.message;\r\n }\r\n\r\n return safeJsonStringify(message);\r\n }\r\n\r\n /**\r\n * Surface the install instructions exactly once when the SDK is absent. The\r\n * logger can't log through itself here, so this writes to stderr — matching\r\n * how the file channels report write failures.\r\n */\r\n private reportMissingSdk(): void {\r\n if (isModuleExists === false && !this.warnedMissing) {\r\n this.warnedMissing = true;\r\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\r\n }\r\n }\r\n}\r\n","/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n","import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n","/**\r\n * Clear message from any terminal codes\r\n */\r\nexport function clearMessage(message: any) {\r\n if (typeof message !== \"string\") return message;\r\n\r\n // eslint-disable-next-line no-control-regex\r\n return message.replace(/\\u001b[^m]*?m/g, \"\");\r\n}\r\n","import { Random } from \"@mongez/reinforcements\";\r\nimport type { LogChannel } from \"./log-channel\";\r\nimport { applyRedact, mergeRedact } from \"./redact\";\r\nimport type {\r\n AutoFlushEvent,\r\n LoggingData,\r\n LogLevel,\r\n OmittedLoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\nimport { clearMessage } from \"./utils/clear-message\";\r\n\r\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\r\n \"SIGINT\",\r\n \"SIGTERM\",\r\n \"SIGHUP\",\r\n \"SIGBREAK\",\r\n \"SIGUSR2\",\r\n]);\r\n\r\n/**\r\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\r\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\r\n * and easiest to drop; `error` is the loudest and never dropped by the\r\n * minimum-level filter. `success` sits beside `info` — it's an informational\r\n * outcome, not a warning.\r\n */\r\nconst LEVEL_RANK: Record<LogLevel, number> = {\r\n debug: 0,\r\n info: 1,\r\n success: 1,\r\n warn: 2,\r\n error: 3,\r\n fatal: 4,\r\n};\r\n\r\nexport class Logger {\r\n /**\r\n * Current channel\r\n */\r\n public channels: LogChannel[] = [];\r\n\r\n public id = \"logger-\" + Random.string(32);\r\n\r\n /**\r\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\r\n * calls to `enableAutoFlush` replace rather than stack, and so\r\n * `disableAutoFlush` can remove them cleanly.\r\n */\r\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\r\n\r\n /**\r\n * Logger-wide minimum severity. When set, entries below this level are\r\n * dropped before any channel is invoked — cheaper than per-channel `levels`\r\n * filters because the fan-out loop is skipped entirely. `undefined` means\r\n * no minimum (every entry reaches every channel that accepts it).\r\n */\r\n private minLevel?: LogLevel;\r\n\r\n /**\r\n * Logger-wide redaction floor. Applied once before fan-out — every\r\n * channel receives an entry with these paths already censored. Channel\r\n * configs can extend the path list (additive); they cannot remove paths\r\n * set here.\r\n */\r\n private redactConfig?: RedactConfig;\r\n\r\n /**\r\n * Add a new channel\r\n */\r\n public addChannel(channel: LogChannel) {\r\n this.channels.push(channel);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set base configurations\r\n */\r\n public configure(config: {\r\n channels?: LogChannel[];\r\n autoFlushOn?: AutoFlushEvent[];\r\n minLevel?: LogLevel;\r\n redact?: RedactConfig;\r\n }) {\r\n if (config.channels) {\r\n this.channels = config.channels;\r\n }\r\n\r\n if (config.autoFlushOn) {\r\n this.enableAutoFlush(config.autoFlushOn);\r\n }\r\n\r\n if (config.minLevel !== undefined) {\r\n this.setMinLevel(config.minLevel);\r\n }\r\n\r\n if (config.redact !== undefined) {\r\n this.setRedact(config.redact);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the logger-wide redaction floor. Applied to every entry before\r\n * fan-out; channel configs add more paths on top, never fewer. Pass\r\n * `undefined` to clear.\r\n *\r\n * @example\r\n * log.setRedact({\r\n * paths: [\"context.password\", \"context.*.token\"],\r\n * censor: \"[REDACTED]\",\r\n * });\r\n */\r\n public setRedact(config: RedactConfig | undefined): this {\r\n this.redactConfig = config;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active logger-wide redact config (or `undefined`).\r\n */\r\n public getRedact(): RedactConfig | undefined {\r\n return this.redactConfig;\r\n }\r\n\r\n /**\r\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\r\n * than per-channel `levels` filters because the loop never runs and no\r\n * channel receives the entry. Pass `undefined` to clear and accept all\r\n * levels again.\r\n *\r\n * @example\r\n * // production: silence debug noise everywhere at once\r\n * logger.setMinLevel(\"info\");\r\n */\r\n public setMinLevel(level: LogLevel | undefined): this {\r\n this.minLevel = level;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active minimum severity (or `undefined` when none is set).\r\n */\r\n public getMinLevel(): LogLevel | undefined {\r\n return this.minLevel;\r\n }\r\n\r\n /**\r\n * Set channels\r\n */\r\n public setChannels(channels: LogChannel[]) {\r\n this.channels = channels;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Normalize log data to a single object\r\n */\r\n private normalizeLogData(\r\n dataOrModule: LoggingData | OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n level?: LogLevel,\r\n context?: Record<string, any>,\r\n ): LoggingData {\r\n if (typeof dataOrModule === \"object\") {\r\n // If level is provided, override type\r\n return {\r\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\r\n module: dataOrModule.module,\r\n action: dataOrModule.action,\r\n message: dataOrModule.message,\r\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\r\n };\r\n }\r\n return {\r\n type: (level || \"info\") as LogLevel,\r\n module: dataOrModule,\r\n action: action as string,\r\n message,\r\n ...(context ? { context } : {}),\r\n };\r\n }\r\n\r\n /**\r\n * Make log\r\n *\r\n * Fans out a single log entry to every registered channel. Non-terminal\r\n * channels receive a copy whose `message` has had ANSI color codes stripped\r\n * — each channel sees its own shallow clone so one channel cannot observe\r\n * another's mutations (e.g. a later terminal channel still sees the original\r\n * colored message).\r\n */\r\n public async log(data: LoggingData) {\r\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\r\n return this;\r\n }\r\n\r\n // Apply the logger-wide redact floor once. Every channel sees the\r\n // result; no channel can undo a logger-wide redaction (additive-only\r\n // semantics).\r\n const baseEntry = applyRedact(data, this.redactConfig);\r\n\r\n for (const channel of this.channels) {\r\n const channelRedact = channel.getRedactConfig?.();\r\n const effectiveRedact = channelRedact\r\n ? mergeRedact(this.redactConfig, channelRedact)\r\n : undefined;\r\n\r\n // When the channel adds paths, redact again from `data` rather than\r\n // from `baseEntry` so the merged config (which already contains the\r\n // logger-wide paths) does the full pass — avoids double-cloning the\r\n // already-redacted base.\r\n let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;\r\n\r\n if (channel.terminal === false) {\r\n payload = { ...payload, message: clearMessage(payload.message) };\r\n }\r\n\r\n channel.log(payload);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Make debug log\r\n */\r\n public debug(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make info log\r\n */\r\n public info(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make warn log\r\n */\r\n public warn(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make error log\r\n */\r\n public error(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make success log\r\n */\r\n public success(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make fatal log — for unrecoverable failures where the application is going\r\n * down (failed bootstrap, lost connection to a required dependency that the\r\n * caller has decided not to retry, an `uncaughtException`).\r\n *\r\n * Identical shape to {@link error}; the level is purely informational —\r\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\r\n * `await log.flush()` and `process.exit(...)`.\r\n */\r\n public fatal(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\r\n * entry is never built and channels are not invoked, so this is genuinely\r\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\r\n * through the logger pipeline so persistent channels capture failures.\r\n *\r\n * @example\r\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\r\n */\r\n public assert(\r\n condition: unknown,\r\n module: string,\r\n action: string,\r\n message: any,\r\n context?: Record<string, any>,\r\n ): Promise<Logger> | Logger {\r\n if (condition) return this;\r\n return this.error(module, action, message, context);\r\n }\r\n\r\n /**\r\n * Start a duration timer. The returned function emits an `info` entry\r\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\r\n * called. Pass an object to `end()` to merge extra fields into context.\r\n *\r\n * @example\r\n * const end = log.timer(\"db\", \"users.findById\");\r\n * const user = await usersRepo.findById(id);\r\n * end({ id, found: !!user });\r\n */\r\n public timer(\r\n module: string,\r\n action: string,\r\n ): (extra?: Record<string, any>) => Promise<Logger> {\r\n const startedAt = Date.now();\r\n return (extra?: Record<string, any>) => {\r\n const durationMs = Date.now() - startedAt;\r\n return this.info(module, action, `completed in ${durationMs}ms`, {\r\n durationMs,\r\n ...(extra ?? {}),\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Get channel by name\r\n */\r\n public channel(name: string) {\r\n return this.channels.find((channel) => channel.name === name);\r\n }\r\n\r\n /**\r\n * Synchronously flush logs\r\n */\r\n public flushSync() {\r\n for (const channel of this.channels) {\r\n if (channel.flushSync) {\r\n channel.flushSync();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Asynchronously drain every channel that implements `flush()`.\r\n *\r\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\r\n * correct call for a graceful shutdown that can afford to wait\r\n * (`await log.flush()` after closing the HTTP server, before\r\n * `process.exit`). A channel whose delivery is async (a network transport,\r\n * an async disk write) implements `flush()`, not `flushSync()`.\r\n *\r\n * Channels are isolated: a channel whose flush rejects can neither prevent\r\n * the others from draining nor escape as an unhandled rejection. Channels\r\n * without `flush()` are skipped.\r\n *\r\n * @example\r\n * async function shutdown() {\r\n * await httpServer.close();\r\n * await log.flush();\r\n * process.exit(0);\r\n * }\r\n */\r\n public async flush(): Promise<void> {\r\n await Promise.allSettled(\r\n this.channels.map(async (channel) => {\r\n if (!channel.flush) {\r\n return;\r\n }\r\n\r\n try {\r\n await channel.flush();\r\n } catch {\r\n // A single channel must never break shutdown for the others —\r\n // a graceful drain is best-effort across every channel.\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Register one process-level handler per event that calls `flushSync()`\r\n * before the process terminates.\r\n *\r\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\r\n * the handler flushes and then re-raises the signal so Node's default exit\r\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\r\n * naturally afterwards.\r\n *\r\n * Idempotent: calling with the same events replaces the previous handlers.\r\n * Call `disableAutoFlush()` to unregister.\r\n *\r\n * @example\r\n * log.configure({\r\n * channels: [new ConsoleLog(), new FileLog()],\r\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\r\n * });\r\n */\r\n public enableAutoFlush(events: AutoFlushEvent[]): this {\r\n this.disableAutoFlush();\r\n\r\n for (const event of events) {\r\n const handler = SIGNAL_EVENTS.has(event)\r\n ? () => {\r\n this.flushSync();\r\n process.off(event, handler);\r\n process.kill(process.pid, event as NodeJS.Signals);\r\n }\r\n : () => {\r\n this.flushSync();\r\n };\r\n\r\n process.on(event, handler);\r\n this.autoFlushHandlers.set(event, handler);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove every handler previously registered by `enableAutoFlush`.\r\n * Safe to call when no handlers are registered.\r\n */\r\n public disableAutoFlush(): this {\r\n for (const [event, handler] of this.autoFlushHandlers) {\r\n process.off(event, handler);\r\n }\r\n\r\n this.autoFlushHandlers.clear();\r\n\r\n return this;\r\n }\r\n}\r\n\r\n/**\r\n * The package singleton. Use this for everyday logging — `log.info(...)`,\r\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\r\n * created by instantiating `Logger` directly.\r\n *\r\n * The name is intentionally short: `log` reads naturally at the call site\r\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\r\n * pino, bunyan, and most JS logging tutorials.\r\n *\r\n * Note that `log` is a `Logger` instance, **not** a function — the bare\r\n * callable form was removed when the dual `log` / `logger` exports were\r\n * collapsed into a single name. Use `log.info(...)` (or any other level\r\n * shortcut) to emit entries.\r\n */\r\nexport const log = new Logger();\r\n","import { log } from \"../logger\";\r\n\r\n/**\r\n * Options for {@link captureAnyUnhandledRejection}.\r\n */\r\nexport type CaptureUnhandledOptions = {\r\n /**\r\n * Take the process down with `process.exit(1)` after an `uncaughtException`\r\n * is logged. Defaults to `true` — an uncaught exception leaves the process\r\n * in an undefined state, so Node's own default is to exit non-zero.\r\n *\r\n * Registering an `uncaughtException` listener *suppresses* that default, so a\r\n * listener that only logs turns an unrecoverable crash into a silent\r\n * `exit 0` (which is how a config file throwing at boot looks like \"the\r\n * server just stopped\"). Keep this `true` in production so supervisors\r\n * restart and the failure is never silent. Set it to `false` where the\r\n * process is expected to recover on its own — e.g. a dev server that reloads\r\n * via HMR — to log the exception without exiting.\r\n */\r\n exitOnUncaughtException?: boolean;\r\n};\r\n\r\n/**\r\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\r\n * the forced exit. Bounded so a single stuck channel can't hang the process —\r\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\r\n * handler would not cover this path.\r\n */\r\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context, and make an\r\n * `uncaughtException` loud + visible instead of silently swallowed.\r\n *\r\n * Registers one listener for `unhandledRejection` and one for\r\n * `uncaughtException`; call once at startup after channels are configured.\r\n *\r\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\r\n * When no terminal channel has been configured yet — the early-boot window a\r\n * config file throwing at import time falls into — the stack is also written\r\n * to `console.error`, standing in for the Node default this listener\r\n * suppresses, so a fatal boot error is never invisible. A configured\r\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\r\n * channel exists (no double output).\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\r\n * not necessarily process-ending, so it stays at error and never exits.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection(); // exits on uncaught\r\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\r\n */\r\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\r\n const { exitOnUncaughtException = true } = options;\r\n\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n // Route through the logger first so file/Sentry channels capture it. A\r\n // crash handler must never itself throw (that would re-enter Node's\r\n // uncaught path), so the logging call is guarded.\r\n try {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n } catch {\r\n // fall through to the stderr fallback + exit below\r\n }\r\n\r\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\r\n // see when this fires before any terminal channel is configured — exactly\r\n // the early-boot window a config file throwing at import time falls into,\r\n // which is how a fatal boot error used to masquerade as a silent\r\n // `exit 0`. Stand in for the Node default this listener suppressed, but\r\n // only when no terminal channel already printed it, so a configured\r\n // `ConsoleLog` is never doubled.\r\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\r\n\r\n if (!hasTerminalChannel) {\r\n console.error(error);\r\n }\r\n\r\n if (!exitOnUncaughtException) {\r\n return;\r\n }\r\n\r\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\r\n // process supervisors restart and `warlock start` surfaces the failure.\r\n void Promise.race([\r\n log.flush(),\r\n new Promise<void>((resolve) => {\r\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\r\n timer.unref?.();\r\n }),\r\n ]).then(\r\n () => process.exit(1),\r\n () => process.exit(1),\r\n );\r\n });\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/console-log.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { inspect } from \"util\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\n\r\nexport type ConsoleLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Render the log entry's `context` object on a second line after the main\r\n * message. When `false`, context is silently dropped (the historical\r\n * behavior). When `true`, contexts are pretty-printed with `util.inspect`\r\n * — colored, depth-limited, ideal for development. Persistent channels\r\n * (`FileLog`, `JSONFileLog`) always retain context regardless of this flag.\r\n *\r\n * @default false\r\n */\r\n showContext?: boolean;\r\n /**\r\n * Depth passed to `util.inspect` when rendering context. Only applies when\r\n * `showContext` is enabled.\r\n *\r\n * @default 4\r\n */\r\n contextDepth?: number;\r\n};\r\n\r\n/**\r\n * The longest level name (`success`) sets the tag column width. Padding every\r\n * level name to this width keeps the timestamp / module / action columns\r\n * aligned across lines. The level icons are bare (text-style, single-cell)\r\n * glyphs, so padding the name alone produces true columns.\r\n */\r\nconst LEVEL_NAME_WIDTH = 7;\r\n\r\n/**\r\n * Per-level console presentation: the colored, fixed-width `{icon} {name}` tag\r\n * and the function that colors the message body.\r\n */\r\ntype LevelStyle = {\r\n tag: string;\r\n message: (message: LoggingData[\"message\"]) => string;\r\n};\r\n\r\n/**\r\n * Build a single-color, fixed-width tag — `{icon} {name}` with the name padded\r\n * so every tag spans the same number of columns.\r\n */\r\nfunction buildTag(\r\n icon: string,\r\n name: string,\r\n color: (text: string) => string,\r\n): string {\r\n return color(`${icon} ${name.padEnd(LEVEL_NAME_WIDTH)}`);\r\n}\r\n\r\n/**\r\n * Console styling per log level. The `fatal` tag is a bright-red background\r\n * badge — the same column width as the others (`\" ☠ fatal \"` is 9 cells, like\r\n * `{icon} {name.padEnd(7)}`), but impossible to miss in a wall of red `error`\r\n * lines, where a plain red tag would blend in.\r\n */\r\nconst LEVEL_STYLES: Record<LogLevel, LevelStyle> = {\r\n debug: {\r\n tag: buildTag(\"⚙\", \"debug\", colors.magentaBright),\r\n message: colors.magentaBright,\r\n },\r\n info: {\r\n tag: buildTag(\"ℹ\", \"info\", colors.blueBright),\r\n message: colors.blueBright,\r\n },\r\n warn: {\r\n tag: buildTag(\"⚠\", \"warn\", colors.yellow),\r\n message: colors.yellowBright,\r\n },\r\n error: {\r\n tag: buildTag(\"✗\", \"error\", colors.red),\r\n message: colors.redBright,\r\n },\r\n success: {\r\n tag: buildTag(\"✓\", \"success\", colors.green),\r\n message: colors.greenBright,\r\n },\r\n fatal: {\r\n tag: colors.bgRedBright(colors.whiteBright(colors.bold(\" ☠ fatal \"))),\r\n message: colors.redBright.bold,\r\n },\r\n};\r\n\r\n/**\r\n * Fallback for an unrecognized level — a plain `[log]` tag padded to the same\r\n * width so the columns stay aligned.\r\n */\r\nconst DEFAULT_STYLE: LevelStyle = {\r\n tag: \"[log]\".padEnd(LEVEL_NAME_WIDTH + 2),\r\n message: message => message as string,\r\n};\r\n\r\nexport class ConsoleLog extends LogChannel<ConsoleLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"console\";\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = true;\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public log(data: LoggingData) {\r\n const { module, action, message, type: level } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n // Console shows time-only (HH:mm:ss.SSS) — within a dev session the date\r\n // rarely changes, so the full ISO date + `T`/`Z` are just noise. Persistent\r\n // channels (FileLog / JSONFileLog) keep the full ISO timestamp.\r\n const time = new Date().toISOString().slice(11, 23); // i.e. \"10:22:00.000\"\r\n\r\n const style = LEVEL_STYLES[level] ?? DEFAULT_STYLE;\r\n\r\n // The slate timestamp recedes so the colored level + message lead the eye;\r\n // module (cyan) and action (magenta) stay colored for subsystem scanning.\r\n // `slate` (256-color neutral gray) reads cleaner than bright-black `gray`,\r\n // which sits too close to the background on most terminal themes.\r\n console.log(\r\n style.tag,\r\n colors.slate(`(${time})`),\r\n colors.cyan(`[${module}]`),\r\n colors.magenta(`[${action}]`),\r\n style.message(message),\r\n );\r\n\r\n if (typeof message === \"object\") {\r\n console.log(message);\r\n }\r\n\r\n // Render context on a second line when explicitly enabled. We only\r\n // attempt rendering if there's anything meaningful to show — empty\r\n // objects clutter the terminal without adding signal.\r\n if (this.config(\"showContext\") && data.context && Object.keys(data.context).length > 0) {\r\n const depth = this.config(\"contextDepth\") ?? 4;\r\n console.log(\r\n colors.slate(\" ↳\"),\r\n inspect(data.context, { colors: true, depth, breakLength: 80 }),\r\n );\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;AA+BA,MAAM,mBAAmB;;;;;AAezB,SAAS,SACP,MACA,MACA,OACQ;CACR,OAAO,MAAM,GAAG,KAAK,GAAG,KAAK,OAAO,gBAAgB,GAAG;AACzD;;;;;;;AAQA,MAAM,eAA6C;CACjD,OAAO;EACL,KAAK,SAAS,KAAK,SAAS,OAAO,aAAa;EAChD,SAAS,OAAO;CAClB;CACA,MAAM;EACJ,KAAK,SAAS,KAAK,QAAQ,OAAO,UAAU;EAC5C,SAAS,OAAO;CAClB;CACA,MAAM;EACJ,KAAK,SAAS,KAAK,QAAQ,OAAO,MAAM;EACxC,SAAS,OAAO;CAClB;CACA,OAAO;EACL,KAAK,SAAS,KAAK,SAAS,OAAO,GAAG;EACtC,SAAS,OAAO;CAClB;CACA,SAAS;EACP,KAAK,SAAS,KAAK,WAAW,OAAO,KAAK;EAC1C,SAAS,OAAO;CAClB;CACA,OAAO;EACL,KAAK,OAAO,YAAY,OAAO,YAAY,OAAO,KAAK,WAAW,CAAC,CAAC;EACpE,SAAS,OAAO,UAAU;CAC5B;AACF;;;;;AAMA,MAAM,gBAA4B;CAChC,KAAK,QAAQ,OAAO,CAAoB;CACxC,UAAS,YAAW;AACtB;AAEA,IAAa,aAAb,cAAgC,WAA6B;;;cAI7C;kBAKI;;;;;CAKlB,AAAO,IAAI,MAAmB;EAC5B,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,UAAU;EAEjD,IAAI,CAAC,KAAK,eAAe,IAAI,GAAG;EAKhC,MAAM,wBAAO,IAAI,KAAK,
|
|
1
|
+
{"version":3,"file":"console-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/console-log.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { inspect } from \"util\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\n\r\nexport type ConsoleLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Render the log entry's `context` object on a second line after the main\r\n * message. When `false`, context is silently dropped (the historical\r\n * behavior). When `true`, contexts are pretty-printed with `util.inspect`\r\n * — colored, depth-limited, ideal for development. Persistent channels\r\n * (`FileLog`, `JSONFileLog`) always retain context regardless of this flag.\r\n *\r\n * @default false\r\n */\r\n showContext?: boolean;\r\n /**\r\n * Depth passed to `util.inspect` when rendering context. Only applies when\r\n * `showContext` is enabled.\r\n *\r\n * @default 4\r\n */\r\n contextDepth?: number;\r\n};\r\n\r\n/**\r\n * The longest level name (`success`) sets the tag column width. Padding every\r\n * level name to this width keeps the timestamp / module / action columns\r\n * aligned across lines. The level icons are bare (text-style, single-cell)\r\n * glyphs, so padding the name alone produces true columns.\r\n */\r\nconst LEVEL_NAME_WIDTH = 7;\r\n\r\n/**\r\n * Per-level console presentation: the colored, fixed-width `{icon} {name}` tag\r\n * and the function that colors the message body.\r\n */\r\ntype LevelStyle = {\r\n tag: string;\r\n message: (message: LoggingData[\"message\"]) => string;\r\n};\r\n\r\n/**\r\n * Build a single-color, fixed-width tag — `{icon} {name}` with the name padded\r\n * so every tag spans the same number of columns.\r\n */\r\nfunction buildTag(\r\n icon: string,\r\n name: string,\r\n color: (text: string) => string,\r\n): string {\r\n return color(`${icon} ${name.padEnd(LEVEL_NAME_WIDTH)}`);\r\n}\r\n\r\n/**\r\n * Console styling per log level. The `fatal` tag is a bright-red background\r\n * badge — the same column width as the others (`\" ☠ fatal \"` is 9 cells, like\r\n * `{icon} {name.padEnd(7)}`), but impossible to miss in a wall of red `error`\r\n * lines, where a plain red tag would blend in.\r\n */\r\nconst LEVEL_STYLES: Record<LogLevel, LevelStyle> = {\r\n debug: {\r\n tag: buildTag(\"⚙\", \"debug\", colors.magentaBright),\r\n message: colors.magentaBright,\r\n },\r\n info: {\r\n tag: buildTag(\"ℹ\", \"info\", colors.blueBright),\r\n message: colors.blueBright,\r\n },\r\n warn: {\r\n tag: buildTag(\"⚠\", \"warn\", colors.yellow),\r\n message: colors.yellowBright,\r\n },\r\n error: {\r\n tag: buildTag(\"✗\", \"error\", colors.red),\r\n message: colors.redBright,\r\n },\r\n success: {\r\n tag: buildTag(\"✓\", \"success\", colors.green),\r\n message: colors.greenBright,\r\n },\r\n fatal: {\r\n tag: colors.bgRedBright(colors.whiteBright(colors.bold(\" ☠ fatal \"))),\r\n message: colors.redBright.bold,\r\n },\r\n};\r\n\r\n/**\r\n * Fallback for an unrecognized level — a plain `[log]` tag padded to the same\r\n * width so the columns stay aligned.\r\n */\r\nconst DEFAULT_STYLE: LevelStyle = {\r\n tag: \"[log]\".padEnd(LEVEL_NAME_WIDTH + 2),\r\n message: message => message as string,\r\n};\r\n\r\nexport class ConsoleLog extends LogChannel<ConsoleLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"console\";\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = true;\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public log(data: LoggingData) {\r\n const { module, action, message, type: level } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n // Console shows time-only (HH:mm:ss.SSS) — within a dev session the date\r\n // rarely changes, so the full ISO date + `T`/`Z` are just noise. Persistent\r\n // channels (FileLog / JSONFileLog) keep the full ISO timestamp.\r\n const time = new Date().toISOString().slice(11, 23); // i.e. \"10:22:00.000\"\r\n\r\n const style = LEVEL_STYLES[level] ?? DEFAULT_STYLE;\r\n\r\n // The slate timestamp recedes so the colored level + message lead the eye;\r\n // module (cyan) and action (magenta) stay colored for subsystem scanning.\r\n // `slate` (256-color neutral gray) reads cleaner than bright-black `gray`,\r\n // which sits too close to the background on most terminal themes.\r\n console.log(\r\n style.tag,\r\n colors.slate(`(${time})`),\r\n colors.cyan(`[${module}]`),\r\n colors.magenta(`[${action}]`),\r\n style.message(message),\r\n );\r\n\r\n if (typeof message === \"object\") {\r\n console.log(message);\r\n }\r\n\r\n // Render context on a second line when explicitly enabled. We only\r\n // attempt rendering if there's anything meaningful to show — empty\r\n // objects clutter the terminal without adding signal.\r\n if (this.config(\"showContext\") && data.context && Object.keys(data.context).length > 0) {\r\n const depth = this.config(\"contextDepth\") ?? 4;\r\n console.log(\r\n colors.slate(\" ↳\"),\r\n inspect(data.context, { colors: true, depth, breakLength: 80 }),\r\n );\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;AA+BA,MAAM,mBAAmB;;;;;AAezB,SAAS,SACP,MACA,MACA,OACQ;CACR,OAAO,MAAM,GAAG,KAAK,GAAG,KAAK,OAAO,gBAAgB,GAAG;AACzD;;;;;;;AAQA,MAAM,eAA6C;CACjD,OAAO;EACL,KAAK,SAAS,KAAK,SAAS,OAAO,aAAa;EAChD,SAAS,OAAO;CAClB;CACA,MAAM;EACJ,KAAK,SAAS,KAAK,QAAQ,OAAO,UAAU;EAC5C,SAAS,OAAO;CAClB;CACA,MAAM;EACJ,KAAK,SAAS,KAAK,QAAQ,OAAO,MAAM;EACxC,SAAS,OAAO;CAClB;CACA,OAAO;EACL,KAAK,SAAS,KAAK,SAAS,OAAO,GAAG;EACtC,SAAS,OAAO;CAClB;CACA,SAAS;EACP,KAAK,SAAS,KAAK,WAAW,OAAO,KAAK;EAC1C,SAAS,OAAO;CAClB;CACA,OAAO;EACL,KAAK,OAAO,YAAY,OAAO,YAAY,OAAO,KAAK,WAAW,CAAC,CAAC;EACpE,SAAS,OAAO,UAAU;CAC5B;AACF;;;;;AAMA,MAAM,gBAA4B;CAChC,KAAK,QAAQ,OAAO,CAAoB;CACxC,UAAS,YAAW;AACtB;AAEA,IAAa,aAAb,cAAgC,WAA6B;;;cAI7C;kBAKI;;;;;CAKlB,AAAO,IAAI,MAAmB;EAC5B,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,UAAU;EAEjD,IAAI,CAAC,KAAK,eAAe,IAAI,GAAG;EAKhC,MAAM,wBAAO,IAAI,KAAK,GAAE,YAAY,EAAE,MAAM,IAAI,EAAE;EAElD,MAAM,QAAQ,aAAa,UAAU;EAMrC,QAAQ,IACN,MAAM,KACN,OAAO,MAAM,IAAI,KAAK,EAAE,GACxB,OAAO,KAAK,IAAI,OAAO,EAAE,GACzB,OAAO,QAAQ,IAAI,OAAO,EAAE,GAC5B,MAAM,QAAQ,OAAO,CACvB;EAEA,IAAI,OAAO,YAAY,UACrB,QAAQ,IAAI,OAAO;EAMrB,IAAI,KAAK,OAAO,aAAa,KAAK,KAAK,WAAW,OAAO,KAAK,KAAK,OAAO,EAAE,SAAS,GAAG;GACtF,MAAM,QAAQ,KAAK,OAAO,cAAc,KAAK;GAC7C,QAAQ,IACN,OAAO,MAAM,KAAK,GAClB,QAAQ,KAAK,SAAS;IAAE,QAAQ;IAAM;IAAO,aAAa;GAAG,CAAC,CAChE;EACF;CACF;AACF"}
|
|
@@ -102,7 +102,7 @@ declare class FileLog extends LogChannel<FileLogConfig> implements LogContract {
|
|
|
102
102
|
/**
|
|
103
103
|
* Check file size for file rotation
|
|
104
104
|
*/
|
|
105
|
-
protected checkAndRotateFile(filePath?:
|
|
105
|
+
protected checkAndRotateFile(filePath?: string): Promise<void>;
|
|
106
106
|
/**
|
|
107
107
|
* Rotate log file
|
|
108
108
|
*/
|
|
@@ -127,7 +127,7 @@ declare class FileLog extends LogChannel<FileLogConfig> implements LogContract {
|
|
|
127
127
|
/**
|
|
128
128
|
* Get file path
|
|
129
129
|
*/
|
|
130
|
-
get filePath():
|
|
130
|
+
get filePath(): string;
|
|
131
131
|
/**
|
|
132
132
|
* Get max messages
|
|
133
133
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/file-log.ts"],"mappings":";;;;KAgBY,aAAA,GAAgB,sBAAA;EAC1B,WAAA;EADU;;;EAKV,IAAA;EAL0B;;;;;;;EAa1B,KAAA;EA8BA;;;;;EAxBA,MAAA;EA4CE;;AAAI;AAIR;;EA1CE,SAAA;EA0CsC;;;;;EApCtC,cAAA;EA6F2D;;;;;EAvF3D,WAAA;EAuT8C;;;;;EAjT9C,kBAAA;EAwB2E;;;;;;;EAhB3E,OAAA;EA8BU;;;;;EAxBV,MAAA,GAAS,QAAQ;EAuDP;;;EAnDV,UAAA;IACE,IAAA;IACA,IAAA;EAAA;AAAA;AAAA,cAIS,OAAA,SAAgB,UAAA,CAAW,aAAA,aAA0B,WAAA;EAiGtD;;;EA7FH,IAAA;EA6II;;;EAAA,UAxID,QAAA,EAAU,UAAA;EA8KJ;;;EAAA,UAzKN,eAAA,EAAiB,MAAA,SAAe,UAAA;EAiNpB;;;EAAA,UA5MZ,qBAAA,EAAuB,aAAA;EA6NC;;;EAAA,UAxMxB,aAAA;EAoQI;;;EAAA,UA/PJ,SAAA;EA8RoC;;;;;EAAA,UAvRpC,mBAAA,GAAsB,MAAA,CAAO,OAAA;EAmUgB;AAAA;;EAAA,UA9TvC,kBAAA,CAAmB,QAAA,
|
|
1
|
+
{"version":3,"file":"file-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/file-log.ts"],"mappings":";;;;KAgBY,aAAA,GAAgB,sBAAA;EAC1B,WAAA;EADU;;;EAKV,IAAA;EAL0B;;;;;;;EAa1B,KAAA;EA8BA;;;;;EAxBA,MAAA;EA4CE;;AAAI;AAIR;;EA1CE,SAAA;EA0CsC;;;;;EApCtC,cAAA;EA6F2D;;;;;EAvF3D,WAAA;EAuT8C;;;;;EAjT9C,kBAAA;EAwB2E;;;;;;;EAhB3E,OAAA;EA8BU;;;;;EAxBV,MAAA,GAAS,QAAQ;EAuDP;;;EAnDV,UAAA;IACE,IAAA;IACA,IAAA;EAAA;AAAA;AAAA,cAIS,OAAA,SAAgB,UAAA,CAAW,aAAA,aAA0B,WAAA;EAiGtD;;;EA7FH,IAAA;EA6II;;;EAAA,UAxID,QAAA,EAAU,UAAA;EA8KJ;;;EAAA,UAzKN,eAAA,EAAiB,MAAA,SAAe,UAAA;EAiNpB;;;EAAA,UA5MZ,qBAAA,EAAuB,aAAA;EA6NC;;;EAAA,UAxMxB,aAAA;EAoQI;;;EAAA,UA/PJ,SAAA;EA8RoC;;;;;EAAA,UAvRpC,mBAAA,GAAsB,MAAA,CAAO,OAAA;EAmUgB;AAAA;;EAAA,UA9TvC,kBAAA,CAAmB,QAAA,YAAwB,OAAA;;;;YAoB3C,aAAA,CAAA,GAAa,OAAA;;;;;;;;;YAoBnB,gBAAA,CAAA;;;;;;;;EAkBH,OAAA,CAAA;;;;MAYI,QAAA,CAAA;;;;gBAWG,kBAAA,CAAA;;;;MAOH,QAAA,CAAA;;;;MAiBA,SAAA,CAAA;;;;gBAOG,OAAA,CAAA;;;;MAOH,WAAA,CAAA;;;;YAOK,IAAA,CAAA,GAAI,OAAA;;;;EAWb,SAAA,CAAA;;;;;;;;;;EA6BM,KAAA,CAAA,GAAS,OAAA;;;;EAiBT,GAAA,CAAI,IAAA,EAAM,WAAA,GAAW,OAAA;;;;YAyClB,8BAAA,CAAA,GAA8B,OAAA;;;;YASpC,MAAA,CAAA;;;;gBAUI,uBAAA,CAAA;;;;YAOE,mBAAA,CAAA,GAAmB,OAAA;;;;YAwBnB,0BAAA,CAAA,GAA8B,OAAA;;;;YA8BpC,sBAAA,CAAA;;;;YAcM,KAAA,CAAM,QAAA,UAAkB,OAAA,WAAe,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport { EOL } from \"os\";\r\nimport path from \"path\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n LogLevel,\r\n LogMessage,\r\n} from \"../types\";\r\n\r\n// TODO: Add max messages per file before rotation\r\n\r\nexport type FileLogConfig = BasicLogConfigurations & {\r\n storagePath?: string;\r\n /**\r\n * File name, without extension\r\n */\r\n name?: string;\r\n /**\r\n * chunk mode\r\n * If set to `single`, the logs will be created in a single file, unless the rotate is set to true\r\n * If set to `daily`, the logs will be created in a daily file, unless the rotate is set to true\r\n * If set to `hourly`, the logs will be created in an hourly file, unless the rotate is set to true\r\n * @default single\r\n */\r\n chunk?: \"single\" | \"daily\" | \"hourly\";\r\n /**\r\n * Whether to rotate the file\r\n *\r\n * @default true\r\n */\r\n rotate?: boolean;\r\n /**\r\n * File Extension\r\n *\r\n * @default log\r\n */\r\n extension?: string;\r\n /**\r\n * If rotate is set, the rotate name will be added to the file name suffixed with `-`\r\n *\r\n * @default DD-MM-YYYY\r\n */\r\n rotateFileName?: string;\r\n /**\r\n * Max file size before rotating the file\r\n *\r\n * @default 10MB\r\n */\r\n maxFileSize?: number;\r\n /**\r\n * Set the max messages that needs to be added before writing to the file\r\n *\r\n * @default 100\r\n */\r\n maxMessagesToWrite?: number;\r\n /**\r\n * Group logs by\r\n * Please note that the order matters here\r\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\r\n *\r\n * @default none\r\n */\r\n groupBy?: (\"level\" | \"module\" | \"action\")[];\r\n /**\r\n * Define what levels should be logged\r\n *\r\n * @default all\r\n */\r\n levels?: LogLevel[];\r\n /**\r\n * Date and time format\r\n */\r\n dateFormat?: {\r\n date?: string;\r\n time?: string;\r\n };\r\n};\r\n\r\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"file\";\r\n\r\n /**\r\n * Messages buffer\r\n */\r\n protected messages: LogMessage[] = [];\r\n\r\n /**\r\n * Grouped messages\r\n */\r\n protected groupedMessages: Record<string, LogMessage[]> = {};\r\n\r\n /**\r\n * Default channel configurations\r\n */\r\n protected defaultConfigurations: FileLogConfig = {\r\n storagePath: process.cwd() + \"/storage/logs\",\r\n rotate: true,\r\n name: \"app\",\r\n extension: \"log\",\r\n chunk: \"single\",\r\n maxMessagesToWrite: 100,\r\n filter: () => true,\r\n maxFileSize: 10 * 1024 * 1024, // 10MB\r\n get rotateFileName() {\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n },\r\n dateFormat: {\r\n date: \"DD-MM-YYYY\",\r\n time: \"HH:mm:ss\",\r\n },\r\n };\r\n\r\n /**\r\n * Last write time\r\n */\r\n protected lastWriteTime = Date.now();\r\n\r\n /**\r\n * A flag to determine if the file is being written\r\n */\r\n protected isWriting = false;\r\n\r\n /**\r\n * Handle for the periodic flush interval. Stored so it can be cleared\r\n * in `dispose()` — long-lived processes that create channels dynamically\r\n * would otherwise leak one timer per channel.\r\n */\r\n protected flushIntervalHandle?: NodeJS.Timeout;\r\n\r\n /**\r\n * Check file size for file rotation\r\n */\r\n protected async checkAndRotateFile(filePath = this.filePath) {\r\n if (!this.config(\"rotate\")) return;\r\n\r\n try {\r\n const stats = await fs.promises.stat(filePath);\r\n if (stats.size >= this.config(\"maxFileSize\")!) {\r\n await this.rotateLogFile();\r\n }\r\n } catch (error: any) {\r\n // ENOENT is expected when the file hasn't been created yet — there is\r\n // nothing to rotate, so stay silent. Surface anything else.\r\n if (error.code !== \"ENOENT\") {\r\n console.error(\"Error checking log file:\", error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Rotate log file\r\n */\r\n protected async rotateLogFile() {\r\n const fileName = `${this.fileName}-${this.config(\"rotateFileName\")}-${Date.now()}`;\r\n\r\n const extension = this.extension;\r\n\r\n const rotatedFilePath = path.join(this.storagePath, `${fileName}.${extension}`);\r\n\r\n await fs.promises.rename(this.filePath, rotatedFilePath).catch((error) => {\r\n console.error(\"Error rotating file:\", error);\r\n });\r\n }\r\n\r\n /**\r\n * Flush messages\r\n *\r\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\r\n * entries indefinitely. The handle is stored on the instance so `dispose()`\r\n * can stop it — without this, every channel leaks a timer for the lifetime\r\n * of the process.\r\n */\r\n protected initMessageFlush() {\r\n this.flushIntervalHandle = setInterval(() => {\r\n if (\r\n this.messages.length > 0 &&\r\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\r\n ) {\r\n this.writeMessagesToFile();\r\n }\r\n }, 5000);\r\n }\r\n\r\n /**\r\n * Stop the background flush interval and drain any buffered entries.\r\n *\r\n * Call this when discarding a channel (e.g. reconfiguring the logger at\r\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\r\n * call more than once.\r\n */\r\n public dispose(): void {\r\n if (this.flushIntervalHandle) {\r\n clearInterval(this.flushIntervalHandle);\r\n this.flushIntervalHandle = undefined;\r\n }\r\n\r\n this.flushSync();\r\n }\r\n\r\n /**\r\n * Get file path\r\n */\r\n public get filePath() {\r\n const fileName = this.fileName;\r\n\r\n const extension = this.extension;\r\n\r\n return path.join(this.storagePath, `${fileName}.${extension}`);\r\n }\r\n\r\n /**\r\n * Get max messages\r\n */\r\n protected get maxMessagesToWrite(): number {\r\n return this.config(\"maxMessagesToWrite\")!;\r\n }\r\n\r\n /**\r\n * Get file name\r\n */\r\n public get fileName(): string {\r\n const debugLevel = this.config(\"chunk\")!;\r\n\r\n switch (debugLevel) {\r\n case \"single\":\r\n default:\r\n return this.config(\"name\")!;\r\n case \"daily\":\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n case \"hourly\":\r\n return dayjs().format(\"DD-MM-YYYY-HH-00-00-a\");\r\n }\r\n }\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return this.config(\"extension\")!;\r\n }\r\n\r\n /**\r\n * Get content\r\n */\r\n protected get content() {\r\n return this.messages.map((message) => message.content).join(EOL) + EOL;\r\n }\r\n\r\n /**\r\n * Get storage path\r\n */\r\n public get storagePath(): string {\r\n return this.config(\"storagePath\")!;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected async init() {\r\n const logsDirectory = this.storagePath;\r\n\r\n await ensureDirectoryAsync(logsDirectory);\r\n\r\n this.initMessageFlush();\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n fs.appendFileSync(filePath, content);\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n fs.appendFileSync(this.filePath, this.content);\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * Asynchronously drain buffered entries to disk.\r\n *\r\n * The async analogue of {@link flushSync}: it reuses the same async writer\r\n * as the background interval, so a caller on a graceful-shutdown path can\r\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\r\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\r\n * its overridden `writeMessagesToFile` performs the JSON merge.\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\r\n return;\r\n }\r\n\r\n // Clear the in-flight lock so a deliberate drain is never short-circuited\r\n // by a half-finished background write, then await the async writer to\r\n // completion — unlike the fire-and-forget interval, callers depend on the\r\n // buffer reaching disk before this resolves.\r\n this.isWriting = false;\r\n\r\n await this.writeMessagesToFile();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n let content = `[${date}] [${level}] [${module}][${action}]: `;\r\n\r\n let stack: string | undefined;\r\n\r\n // check if message is an instance of Error\r\n if (message instanceof Error) {\r\n // in that case we need to store the error message and stack trace\r\n content += message.message + EOL;\r\n content += `[trace]` + EOL;\r\n content += message.stack;\r\n stack = message.stack;\r\n } else {\r\n content += message;\r\n }\r\n\r\n this.messages.push({\r\n content,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n });\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Check if messages should be written\r\n */\r\n protected async checkIfMessagesShouldBeWritten() {\r\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\r\n await this.writeMessagesToFile();\r\n }\r\n }\r\n\r\n /**\r\n * Should be called after messages are saved\r\n */\r\n protected onSave() {\r\n this.messages = [];\r\n this.groupedMessages = {};\r\n this.isWriting = false;\r\n this.lastWriteTime = Date.now();\r\n }\r\n\r\n /**\r\n * Check if messages should be grouped\r\n */\r\n protected get messagedShouldBeGrouped(): boolean {\r\n return Number(this.config(\"groupBy\")?.length) > 0;\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile() {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure we check file size before writing\r\n\r\n try {\r\n await this.write(this.filePath, this.content);\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n\r\n try {\r\n await this.write(filePath, content);\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n }\r\n }\r\n\r\n this.onSave();\r\n this.isWriting = false;\r\n }\r\n\r\n /**\r\n * Prepare grouped messages\r\n */\r\n protected prepareGroupedMessages(): void {\r\n this.messages.forEach((message) => {\r\n const key = this.config(\"groupBy\")!\r\n .map((groupKey) => encodeURIComponent(message[groupKey]))\r\n .join(\"/\");\r\n\r\n this.groupedMessages[key] = this.groupedMessages[key] || [];\r\n this.groupedMessages[key].push(message);\r\n });\r\n }\r\n\r\n /**\r\n * Start writing to the file\r\n */\r\n protected async write(filePath: string, content: string) {\r\n return new Promise((resolve, reject) => {\r\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\r\n\r\n writer.write(content, (error) => {\r\n writer.end();\r\n if (error) {\r\n reject(error);\r\n } else {\r\n resolve(true);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport { EOL } from \"os\";\r\nimport path from \"path\";\r\nimport { LogChannel } from \"../log-channel\";\r\nimport type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n LogLevel,\r\n LogMessage,\r\n} from \"../types\";\r\n\r\n// TODO: Add max messages per file before rotation\r\n\r\nexport type FileLogConfig = BasicLogConfigurations & {\r\n storagePath?: string;\r\n /**\r\n * File name, without extension\r\n */\r\n name?: string;\r\n /**\r\n * chunk mode\r\n * If set to `single`, the logs will be created in a single file, unless the rotate is set to true\r\n * If set to `daily`, the logs will be created in a daily file, unless the rotate is set to true\r\n * If set to `hourly`, the logs will be created in an hourly file, unless the rotate is set to true\r\n * @default single\r\n */\r\n chunk?: \"single\" | \"daily\" | \"hourly\";\r\n /**\r\n * Whether to rotate the file\r\n *\r\n * @default true\r\n */\r\n rotate?: boolean;\r\n /**\r\n * File Extension\r\n *\r\n * @default log\r\n */\r\n extension?: string;\r\n /**\r\n * If rotate is set, the rotate name will be added to the file name suffixed with `-`\r\n *\r\n * @default DD-MM-YYYY\r\n */\r\n rotateFileName?: string;\r\n /**\r\n * Max file size before rotating the file\r\n *\r\n * @default 10MB\r\n */\r\n maxFileSize?: number;\r\n /**\r\n * Set the max messages that needs to be added before writing to the file\r\n *\r\n * @default 100\r\n */\r\n maxMessagesToWrite?: number;\r\n /**\r\n * Group logs by\r\n * Please note that the order matters here\r\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\r\n *\r\n * @default none\r\n */\r\n groupBy?: (\"level\" | \"module\" | \"action\")[];\r\n /**\r\n * Define what levels should be logged\r\n *\r\n * @default all\r\n */\r\n levels?: LogLevel[];\r\n /**\r\n * Date and time format\r\n */\r\n dateFormat?: {\r\n date?: string;\r\n time?: string;\r\n };\r\n};\r\n\r\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"file\";\r\n\r\n /**\r\n * Messages buffer\r\n */\r\n protected messages: LogMessage[] = [];\r\n\r\n /**\r\n * Grouped messages\r\n */\r\n protected groupedMessages: Record<string, LogMessage[]> = {};\r\n\r\n /**\r\n * Default channel configurations\r\n */\r\n protected defaultConfigurations: FileLogConfig = {\r\n storagePath: process.cwd() + \"/storage/logs\",\r\n rotate: true,\r\n name: \"app\",\r\n extension: \"log\",\r\n chunk: \"single\",\r\n maxMessagesToWrite: 100,\r\n filter: () => true,\r\n maxFileSize: 10 * 1024 * 1024, // 10MB\r\n get rotateFileName() {\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n },\r\n dateFormat: {\r\n date: \"DD-MM-YYYY\",\r\n time: \"HH:mm:ss\",\r\n },\r\n };\r\n\r\n /**\r\n * Last write time\r\n */\r\n protected lastWriteTime = Date.now();\r\n\r\n /**\r\n * A flag to determine if the file is being written\r\n */\r\n protected isWriting = false;\r\n\r\n /**\r\n * Handle for the periodic flush interval. Stored so it can be cleared\r\n * in `dispose()` — long-lived processes that create channels dynamically\r\n * would otherwise leak one timer per channel.\r\n */\r\n protected flushIntervalHandle?: NodeJS.Timeout;\r\n\r\n /**\r\n * Check file size for file rotation\r\n */\r\n protected async checkAndRotateFile(filePath = this.filePath) {\r\n if (!this.config(\"rotate\")) return;\r\n\r\n try {\r\n const stats = await fs.promises.stat(filePath);\r\n if (stats.size >= this.config(\"maxFileSize\")!) {\r\n await this.rotateLogFile();\r\n }\r\n } catch (error: any) {\r\n // ENOENT is expected when the file hasn't been created yet — there is\r\n // nothing to rotate, so stay silent. Surface anything else.\r\n if (error.code !== \"ENOENT\") {\r\n console.error(\"Error checking log file:\", error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Rotate log file\r\n */\r\n protected async rotateLogFile() {\r\n const fileName = `${this.fileName}-${this.config(\"rotateFileName\")}-${Date.now()}`;\r\n\r\n const extension = this.extension;\r\n\r\n const rotatedFilePath = path.join(this.storagePath, `${fileName}.${extension}`);\r\n\r\n await fs.promises.rename(this.filePath, rotatedFilePath).catch((error) => {\r\n console.error(\"Error rotating file:\", error);\r\n });\r\n }\r\n\r\n /**\r\n * Flush messages\r\n *\r\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\r\n * entries indefinitely. The handle is stored on the instance so `dispose()`\r\n * can stop it — without this, every channel leaks a timer for the lifetime\r\n * of the process.\r\n */\r\n protected initMessageFlush() {\r\n this.flushIntervalHandle = setInterval(() => {\r\n if (\r\n this.messages.length > 0 &&\r\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\r\n ) {\r\n this.writeMessagesToFile();\r\n }\r\n }, 5000);\r\n }\r\n\r\n /**\r\n * Stop the background flush interval and drain any buffered entries.\r\n *\r\n * Call this when discarding a channel (e.g. reconfiguring the logger at\r\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\r\n * call more than once.\r\n */\r\n public dispose(): void {\r\n if (this.flushIntervalHandle) {\r\n clearInterval(this.flushIntervalHandle);\r\n this.flushIntervalHandle = undefined;\r\n }\r\n\r\n this.flushSync();\r\n }\r\n\r\n /**\r\n * Get file path\r\n */\r\n public get filePath() {\r\n const fileName = this.fileName;\r\n\r\n const extension = this.extension;\r\n\r\n return path.join(this.storagePath, `${fileName}.${extension}`);\r\n }\r\n\r\n /**\r\n * Get max messages\r\n */\r\n protected get maxMessagesToWrite(): number {\r\n return this.config(\"maxMessagesToWrite\")!;\r\n }\r\n\r\n /**\r\n * Get file name\r\n */\r\n public get fileName(): string {\r\n const debugLevel = this.config(\"chunk\")!;\r\n\r\n switch (debugLevel) {\r\n case \"single\":\r\n default:\r\n return this.config(\"name\")!;\r\n case \"daily\":\r\n return dayjs().format(\"DD-MM-YYYY\");\r\n case \"hourly\":\r\n return dayjs().format(\"DD-MM-YYYY-HH-00-00-a\");\r\n }\r\n }\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return this.config(\"extension\")!;\r\n }\r\n\r\n /**\r\n * Get content\r\n */\r\n protected get content() {\r\n return this.messages.map((message) => message.content).join(EOL) + EOL;\r\n }\r\n\r\n /**\r\n * Get storage path\r\n */\r\n public get storagePath(): string {\r\n return this.config(\"storagePath\")!;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected async init() {\r\n const logsDirectory = this.storagePath;\r\n\r\n await ensureDirectoryAsync(logsDirectory);\r\n\r\n this.initMessageFlush();\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n fs.appendFileSync(filePath, content);\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n fs.appendFileSync(this.filePath, this.content);\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * Asynchronously drain buffered entries to disk.\r\n *\r\n * The async analogue of {@link flushSync}: it reuses the same async writer\r\n * as the background interval, so a caller on a graceful-shutdown path can\r\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\r\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\r\n * its overridden `writeMessagesToFile` performs the JSON merge.\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\r\n return;\r\n }\r\n\r\n // Clear the in-flight lock so a deliberate drain is never short-circuited\r\n // by a half-finished background write, then await the async writer to\r\n // completion — unlike the fire-and-forget interval, callers depend on the\r\n // buffer reaching disk before this resolves.\r\n this.isWriting = false;\r\n\r\n await this.writeMessagesToFile();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n let content = `[${date}] [${level}] [${module}][${action}]: `;\r\n\r\n let stack: string | undefined;\r\n\r\n // check if message is an instance of Error\r\n if (message instanceof Error) {\r\n // in that case we need to store the error message and stack trace\r\n content += message.message + EOL;\r\n content += `[trace]` + EOL;\r\n content += message.stack;\r\n stack = message.stack;\r\n } else {\r\n content += message;\r\n }\r\n\r\n this.messages.push({\r\n content,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n });\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Check if messages should be written\r\n */\r\n protected async checkIfMessagesShouldBeWritten() {\r\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\r\n await this.writeMessagesToFile();\r\n }\r\n }\r\n\r\n /**\r\n * Should be called after messages are saved\r\n */\r\n protected onSave() {\r\n this.messages = [];\r\n this.groupedMessages = {};\r\n this.isWriting = false;\r\n this.lastWriteTime = Date.now();\r\n }\r\n\r\n /**\r\n * Check if messages should be grouped\r\n */\r\n protected get messagedShouldBeGrouped(): boolean {\r\n return Number(this.config(\"groupBy\")?.length) > 0;\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile() {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure we check file size before writing\r\n\r\n try {\r\n await this.write(this.filePath, this.content);\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n const content = this.groupedMessages[key].map((message) => message.content).join(EOL) + EOL;\r\n\r\n try {\r\n await this.write(filePath, content);\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n }\r\n }\r\n\r\n this.onSave();\r\n this.isWriting = false;\r\n }\r\n\r\n /**\r\n * Prepare grouped messages\r\n */\r\n protected prepareGroupedMessages(): void {\r\n this.messages.forEach((message) => {\r\n const key = this.config(\"groupBy\")!\r\n .map((groupKey) => encodeURIComponent(message[groupKey]))\r\n .join(\"/\");\r\n\r\n this.groupedMessages[key] = this.groupedMessages[key] || [];\r\n this.groupedMessages[key].push(message);\r\n });\r\n }\r\n\r\n /**\r\n * Start writing to the file\r\n */\r\n protected async write(filePath: string, content: string) {\r\n return new Promise((resolve, reject) => {\r\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\r\n\r\n writer.write(content, (error) => {\r\n writer.end();\r\n if (error) {\r\n reject(error);\r\n } else {\r\n resolve(true);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/json-file-log.ts"],"mappings":";;;;cAQa,WAAA,SAAoB,OAAA,YAAmB,WAAA;;AAApD;;EAIS,IAAA;EAuDgB;;;EAAA,IAlDZ,SAAA;EAToB;;;EAgBxB,SAAA;EAhBwB;;;EA2DlB,GAAA,CAAI,IAAA,EAAM,WAAA,GAAW,OAAA;EA3C3B;;;EAAA,UA4ES,mBAAA,
|
|
1
|
+
{"version":3,"file":"json-file-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/json-file-log.ts"],"mappings":";;;;cAQa,WAAA,SAAoB,OAAA,YAAmB,WAAA;;AAApD;;EAIS,IAAA;EAuDgB;;;EAAA,IAlDZ,SAAA,CAAA;EAToB;;;EAgBxB,SAAA,CAAA;EAhBwB;;;EA2DlB,GAAA,CAAI,IAAA,EAAM,WAAA,GAAW,OAAA;EA3C3B;;;EAAA,UA4ES,mBAAA,CAAA,GAAuB,OAAA;EAjCL;;;EAAA,UAyElB,0BAAA,CAAA,GAA8B,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/json-file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport path from \"path\";\r\nimport type { LogContract, LogMessage, LoggingData } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\nimport { FileLog } from \"./file-log\";\r\n\r\nexport class JSONFileLog extends FileLog implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"fileJson\";\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return \"json\";\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n fs.writeFileSync(filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(this.filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(this.filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.messages);\r\n fs.writeFileSync(this.filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n let stack: string[] | undefined;\r\n\r\n if (data.message instanceof Error) {\r\n stack = data.message.stack?.split(\"\\n\");\r\n data.message = data.message.message;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n this.messages.push({\r\n content: message,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n } as LogMessage);\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile(): Promise<void> {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure file rotation is handled\r\n\r\n let fileContents = { messages: [] as any[] };\r\n\r\n if (await fileExistsAsync(this.filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(this.filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n } else {\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n\r\n fileContents.messages.push(...this.messages);\r\n\r\n try {\r\n await fs.promises.writeFile(this.filePath, safeJsonStringify(fileContents, 2));\r\n\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n let fileContents: { messages: any[] } = { messages: [] };\r\n if (await fileExistsAsync(filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] };\r\n }\r\n } else {\r\n fileContents = { messages: [] };\r\n }\r\n\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n\r\n try {\r\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n this.onSave();\r\n }\r\n}\r\n"],"mappings":";;;;;;;;AAQA,IAAa,cAAb,cAAiC,QAA+B;;;cAIhD;;;;;CAKd,IAAW,YAAoB;EAC7B,OAAO;CACT;;;;CAKA,AAAO,YAAkB;EACvB,IAAI,KAAK,SAAS,WAAW,KAAK,OAAO,KAAK,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"json-file-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/json-file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\r\nimport dayjs from \"dayjs\";\r\nimport fs from \"fs\";\r\nimport path from \"path\";\r\nimport type { LogContract, LogMessage, LoggingData } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\nimport { FileLog } from \"./file-log\";\r\n\r\nexport class JSONFileLog extends FileLog implements LogContract {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"fileJson\";\r\n\r\n /**\r\n * Get file extension\r\n */\r\n public get extension(): string {\r\n return \"json\";\r\n }\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync(): void {\r\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n this.prepareGroupedMessages();\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n fs.mkdirSync(directoryPath, { recursive: true });\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n fs.writeFileSync(filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n } else {\r\n fs.mkdirSync(this.storagePath, { recursive: true });\r\n let fileContents = { messages: [] as any[] };\r\n if (fs.existsSync(this.filePath)) {\r\n try {\r\n fileContents = JSON.parse(fs.readFileSync(this.filePath, \"utf-8\"));\r\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\r\n } catch (e) {\r\n fileContents = { messages: [] };\r\n }\r\n }\r\n fileContents.messages.push(...this.messages);\r\n fs.writeFileSync(this.filePath, safeJsonStringify(fileContents, 2));\r\n }\r\n\r\n this.onSave();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData) {\r\n let stack: string[] | undefined;\r\n\r\n if (data.message instanceof Error) {\r\n stack = data.message.stack?.split(\"\\n\");\r\n data.message = data.message.message;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (!this.shouldBeLogged(data)) return;\r\n\r\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\r\n\r\n const date = dayjs().format(dateFormat + \" \" + time);\r\n\r\n this.messages.push({\r\n content: message,\r\n level,\r\n date,\r\n module,\r\n action,\r\n stack,\r\n context,\r\n timestamp: new Date().toISOString(),\r\n } as LogMessage);\r\n\r\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\r\n }\r\n\r\n /**\r\n * Write messages to the file\r\n */\r\n protected async writeMessagesToFile(): Promise<void> {\r\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\r\n\r\n this.isWriting = true;\r\n\r\n if (this.messagedShouldBeGrouped) {\r\n return await this.writeGroupedMessagesToFile();\r\n }\r\n\r\n await this.checkAndRotateFile(); // Ensure file rotation is handled\r\n\r\n let fileContents = { messages: [] as any[] };\r\n\r\n if (await fileExistsAsync(this.filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(this.filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n } else {\r\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\r\n }\r\n\r\n fileContents.messages.push(...this.messages);\r\n\r\n try {\r\n await fs.promises.writeFile(this.filePath, safeJsonStringify(fileContents, 2));\r\n\r\n this.onSave();\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n // Implement fallback logic here\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n /**\r\n * Write grouped messages to the file\r\n */\r\n protected async writeGroupedMessagesToFile(): Promise<void> {\r\n // first step, is to group the messages\r\n this.prepareGroupedMessages();\r\n\r\n // now each key in the grouped messages, represents the directory path that should extend the storage path\r\n for (const key in this.groupedMessages) {\r\n const directoryPath = path.join(this.storagePath, key);\r\n\r\n await ensureDirectoryAsync(directoryPath);\r\n\r\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\r\n\r\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\r\n\r\n let fileContents: { messages: any[] } = { messages: [] };\r\n if (await fileExistsAsync(filePath)) {\r\n try {\r\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\r\n } catch (error) {\r\n console.error(\"Error reading log file, reinitializing:\", error);\r\n fileContents = { messages: [] };\r\n }\r\n } else {\r\n fileContents = { messages: [] };\r\n }\r\n\r\n fileContents.messages.push(...this.groupedMessages[key]);\r\n\r\n try {\r\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\r\n } catch (error) {\r\n console.error(\"Failed to write log:\", error);\r\n this.isWriting = false;\r\n }\r\n }\r\n\r\n this.onSave();\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/sentry-log.ts"],"mappings":";;;;;;AAC8E;KAgBzE,mBAAA;;;AAAmB;UAWP,eAAA;EACf,QAAA,CAAS,KAAA,EAAO,mBAAA;EAChB,OAAA,CAAQ,IAAA,EAAM,MAAA;EACd,UAAA,CAAW,IAAA,UAAc,OAAA,EAAS,MAAA;AAAA;;;;KAMxB,gBAAA;EACV,QAAA;EACA,OAAA;EACA,KAAA,GAAQ,mBAAA;EACR,IAAA,GAAO,MAAM;AAAA;;;;;;UAQE,eAAA;EACf,gBAAA,CAAiB,SAAA;EACjB,cAAA,CAAe,OAAA,UAAiB,KAAA,GAAQ,mBAAA;EACxC,aAAA,CAAc,UAAA,EAAY,gBAAA;EAC1B,SAAA,CAAU,QAAA,GAAW,KAAA,EAAO,eAAA;EAC5B,KAAA,CAAM,OAAA,YAAmB,OAAA;AAAA;;;;;;KAQf,iBAAA;EACV,GAAA;EACA,WAAA;EACA,OAAA;EACA,UAAA;EAAA,CACC,GAAA;AAAA;AAAA,KAyDS,eAAA,GAAkB,sBAAA;EAtEI;;;;;EA4EhC,MAAA,GAAS,eAAA;EA/E+B;;;;;EAqFxC,OAAA,GAAU,iBAAA;EAnFkB;;;;;;;EA2F5B,WAAA,GAAc,QAAA;EAlFJ;;;;;EAwFV,YAAA;AAAA;;;;AAnFY;AAyDd;;;;;;;;;;;;;;;;;;AA0Bc;AA2Bd;cAAa,SAAA,SAAkB,UAAA,CAAW,eAAA;;;;EAIjC,IAAA;EAqDiB;;;EAhDjB,WAAA;EATsB;;;EAAA,UAenB,qBAAA,EAAuB,eAAA;EAfO;;;;;EAAA,QAyBhC,MAAA;EAMA;;;;EAAA,QAAA,aAAA;EA0BgB;;;;;;;cAjBL,cAAA,GAAiB,eAAA;
|
|
1
|
+
{"version":3,"file":"sentry-log.d.mts","names":[],"sources":["../../../../../../../logger/src/channels/sentry-log.ts"],"mappings":";;;;;;AAC8E;KAgBzE,mBAAA;;;AAAmB;UAWP,eAAA;EACf,QAAA,CAAS,KAAA,EAAO,mBAAA;EAChB,OAAA,CAAQ,IAAA,EAAM,MAAA;EACd,UAAA,CAAW,IAAA,UAAc,OAAA,EAAS,MAAA;AAAA;;;;KAMxB,gBAAA;EACV,QAAA;EACA,OAAA;EACA,KAAA,GAAQ,mBAAA;EACR,IAAA,GAAO,MAAM;AAAA;;;;;;UAQE,eAAA;EACf,gBAAA,CAAiB,SAAA;EACjB,cAAA,CAAe,OAAA,UAAiB,KAAA,GAAQ,mBAAA;EACxC,aAAA,CAAc,UAAA,EAAY,gBAAA;EAC1B,SAAA,CAAU,QAAA,GAAW,KAAA,EAAO,eAAA;EAC5B,KAAA,CAAM,OAAA,YAAmB,OAAA;AAAA;;;;;;KAQf,iBAAA;EACV,GAAA;EACA,WAAA;EACA,OAAA;EACA,UAAA;EAAA,CACC,GAAA;AAAA;AAAA,KAyDS,eAAA,GAAkB,sBAAA;EAtEI;;;;;EA4EhC,MAAA,GAAS,eAAA;EA/E+B;;;;;EAqFxC,OAAA,GAAU,iBAAA;EAnFkB;;;;;;;EA2F5B,WAAA,GAAc,QAAA;EAlFJ;;;;;EAwFV,YAAA;AAAA;;;;AAnFY;AAyDd;;;;;;;;;;;;;;;;;;AA0Bc;AA2Bd;cAAa,SAAA,SAAkB,UAAA,CAAW,eAAA;;;;EAIjC,IAAA;EAqDiB;;;EAhDjB,WAAA;EATsB;;;EAAA,UAenB,qBAAA,EAAuB,eAAA;EAfO;;;;;EAAA,QAyBhC,MAAA;EAMA;;;;EAAA,QAAA,aAAA;EA0BgB;;;;;;;cAjBL,cAAA,GAAiB,eAAA;EAmH5B;;;;;AAiEgB;;EAjEhB,UAlGQ,IAAA,CAAA,GAAQ,OAAA;;;;EA2BX,GAAA,CAAI,IAAA,EAAM,WAAA,GAAc,OAAA;;;;;EAiDxB,KAAA,CAAA,GAAS,OAAA;;;;UAWd,YAAA;;;;;;;;UAWA,YAAA;;;;;UAiCA,aAAA;;;;;UAeA,MAAA;;;;;;UAiBA,gBAAA;AAAA"}
|
|
@@ -106,7 +106,7 @@ var SentryLog = class extends LogChannel {
|
|
|
106
106
|
*/
|
|
107
107
|
async log(data) {
|
|
108
108
|
if (!this.shouldBeLogged(data)) return;
|
|
109
|
-
if (isModuleExists === null) await loadSentry();
|
|
109
|
+
if (!this.sentry && isModuleExists === null) await loadSentry();
|
|
110
110
|
if (!this.sentry) {
|
|
111
111
|
this.reportMissingSdk();
|
|
112
112
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/sentry-log.ts"],"sourcesContent":["import { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\n\r\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\r\n// @sentry/node is referenced ONLY through the local minimal types below — never\r\n// via `typeof import(\"@sentry/node\")`. This package is served as source\r\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\r\n// program; a static type reference to the SDK would force module resolution and\r\n// break every consumer who (correctly) never installs the optional peer with a\r\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\r\n// the same reason, and its result is cast to these shapes. The shapes mirror the\r\n// stable `@sentry/node` public API (unchanged across v8–v10).\r\n\r\n/**\r\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\r\n */\r\ntype SentrySeverityLevel =\r\n | \"fatal\"\r\n | \"error\"\r\n | \"warning\"\r\n | \"log\"\r\n | \"info\"\r\n | \"debug\";\r\n\r\n/**\r\n * The Sentry `Scope` surface used while building a single event.\r\n */\r\nexport interface SentryScopeLike {\r\n setLevel(level: SentrySeverityLevel): void;\r\n setTags(tags: Record<string, string>): void;\r\n setContext(name: string, context: Record<string, unknown> | null): void;\r\n}\r\n\r\n/**\r\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\r\n */\r\nexport type SentryBreadcrumb = {\r\n category?: string;\r\n message?: string;\r\n level?: SentrySeverityLevel;\r\n data?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\r\n * namespace satisfies this shape, so an app can pass it straight through as\r\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\r\n */\r\nexport interface SentryForwarder {\r\n captureException(exception: unknown): string;\r\n captureMessage(message: string, level?: SentrySeverityLevel): string;\r\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\r\n withScope(callback: (scope: SentryScopeLike) => void): void;\r\n flush(timeout?: number): Promise<boolean>;\r\n}\r\n\r\n/**\r\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\r\n * fields; the index signature keeps any other SDK option valid without coupling\r\n * to the SDK's types.\r\n */\r\nexport type SentryInitOptions = {\r\n dsn?: string;\r\n environment?: string;\r\n release?: string;\r\n sampleRate?: number;\r\n [key: string]: unknown;\r\n};\r\n\r\n/**\r\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\r\n * lifecycle calls the channel uses when it owns initialization.\r\n */\r\ninterface SentryNamespace extends SentryForwarder {\r\n init(options?: SentryInitOptions): unknown;\r\n getClient(): unknown;\r\n}\r\n\r\nlet Sentry: SentryNamespace | undefined;\r\nlet isModuleExists: boolean | null = null;\r\nlet loadingPromise: Promise<void> | undefined;\r\n\r\nconst SENTRY_INSTALL_INSTRUCTIONS = `\r\nThe Sentry log channel requires the @sentry/node package.\r\nInstall it with:\r\n\r\n npm install @sentry/node\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add @sentry/node\r\n yarn add @sentry/node\r\n`.trim();\r\n\r\n/**\r\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\r\n * import failure to \"not installed\" — the curated install message surfaces at\r\n * `log()` time, never as a boot-time module-resolution crash.\r\n */\r\nfunction loadSentry(): Promise<void> {\r\n if (isModuleExists !== null) {\r\n return Promise.resolve();\r\n }\r\n\r\n if (loadingPromise) {\r\n return loadingPromise;\r\n }\r\n\r\n loadingPromise = (async () => {\r\n try {\r\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\r\n // statically resolves the optional peer — see the file header.\r\n const sentryModule: string = \"@sentry/node\";\r\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n })();\r\n\r\n return loadingPromise;\r\n}\r\n\r\nexport type SentryLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\r\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\r\n * channel forwards through it and never imports or re-initializes the SDK.\r\n */\r\n client?: SentryForwarder;\r\n /**\r\n * Initialize Sentry from these options instead of reusing a host client. The\r\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\r\n * guarded so it never clobbers an existing client.\r\n */\r\n options?: SentryInitOptions;\r\n /**\r\n * Levels delivered as Sentry *events* (these consume the error quota). Every\r\n * other level is recorded as a breadcrumb that rides along with the next\r\n * event, costing no quota.\r\n *\r\n * @default [\"fatal\", \"error\", \"warn\"]\r\n */\r\n eventLevels?: LogLevel[];\r\n /**\r\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\r\n *\r\n * @default 2000\r\n */\r\n flushTimeout?: number;\r\n};\r\n\r\n/**\r\n * Forwards log entries to Sentry.\r\n *\r\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\r\n * **events**: an `Error` message via `captureException` (preserving the real\r\n * stack), any other message via `captureMessage`. Every other level becomes a\r\n * **breadcrumb** — buffered and attached to the next event, consuming no error\r\n * quota. `module` / `action` are attached as searchable tags and the entry's\r\n * `context` as a structured Sentry context.\r\n *\r\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\r\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\r\n * graceful shutdown, `await log.flush()` drains pending events via\r\n * `Sentry.flush(timeout)`.\r\n *\r\n * @example\r\n * // Existing app — reuse the initialized Sentry client\r\n * import * as Sentry from \"@sentry/node\";\r\n * log.addChannel(new SentryLog({ client: Sentry }));\r\n *\r\n * @example\r\n * // New app — let the channel initialize Sentry\r\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\r\n */\r\nexport class SentryLog extends LogChannel<SentryLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"sentry\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public description =\r\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected defaultConfigurations: SentryLogConfig = {\r\n eventLevels: [\"fatal\", \"error\", \"warn\"],\r\n flushTimeout: 2_000,\r\n };\r\n\r\n /**\r\n * The resolved forwarder — the injected `client` or the lazily-imported\r\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\r\n * SDK is absent (then `log()` surfaces the install message once).\r\n */\r\n private sentry?: SentryForwarder;\r\n\r\n /**\r\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\r\n * doesn't spam stderr on every entry.\r\n */\r\n private warnedMissing = false;\r\n\r\n /**\r\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\r\n * entry logged on the same tick as construction — e.g. at app boot, before\r\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\r\n * silently dropped. The `options` (lazy-import) path is inherently async and\r\n * still resolves in `init()`.\r\n */\r\n public constructor(configurations?: SentryLogConfig) {\r\n super(configurations);\r\n\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\r\n * `@sentry/node` and (only when explicit `options` are supplied and no client\r\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\r\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\r\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\r\n */\r\n protected async init(): Promise<void> {\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n\r\n return;\r\n }\r\n\r\n await loadSentry();\r\n\r\n if (!Sentry) {\r\n return;\r\n }\r\n\r\n const options = this.config(\"options\");\r\n\r\n if (options && !Sentry.getClient()) {\r\n Sentry.init(options);\r\n }\r\n\r\n this.sentry = Sentry;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData): Promise<void> {\r\n if (!this.shouldBeLogged(data)) {\r\n return;\r\n }\r\n\r\n if (isModuleExists === null) {\r\n // wait until module is fully loaded\r\n await loadSentry();\r\n }\r\n\r\n if (!this.sentry) {\r\n this.reportMissingSdk();\r\n\r\n return;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (this.isEventLevel(level)) {\r\n this.captureEvent(this.sentry, {\r\n module,\r\n action,\r\n message,\r\n level,\r\n context,\r\n });\r\n\r\n return;\r\n }\r\n\r\n this.sentry.addBreadcrumb({\r\n category: module,\r\n message: this.toText(message),\r\n level: this.toSentryLevel(level),\r\n data: context,\r\n });\r\n }\r\n\r\n /**\r\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\r\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\r\n */\r\n public async flush(): Promise<void> {\r\n if (!this.sentry) {\r\n return;\r\n }\r\n\r\n await this.sentry.flush(this.config(\"flushTimeout\"));\r\n }\r\n\r\n /**\r\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\r\n */\r\n private isEventLevel(level: LogLevel): boolean {\r\n return Boolean(this.config(\"eventLevels\")?.includes(level));\r\n }\r\n\r\n /**\r\n * Send an entry as a Sentry event. An `Error` message goes through\r\n * `captureException` so Sentry parses the real stack and groups properly;\r\n * any other message goes through `captureMessage`. `module` / `action` are\r\n * attached as tags and `context` as a structured context, scoped to this\r\n * event only via `withScope`.\r\n */\r\n private captureEvent(\r\n sentry: SentryForwarder,\r\n entry: {\r\n module: string;\r\n action: string;\r\n message: unknown;\r\n level: LogLevel;\r\n context?: Record<string, any>;\r\n },\r\n ): void {\r\n const { module, action, message, level, context } = entry;\r\n const sentryLevel = this.toSentryLevel(level);\r\n\r\n sentry.withScope((scope) => {\r\n scope.setLevel(sentryLevel);\r\n scope.setTags({ module, action });\r\n\r\n if (context) {\r\n scope.setContext(\"context\", context);\r\n }\r\n\r\n if (message instanceof Error) {\r\n sentry.captureException(message);\r\n } else {\r\n sentry.captureMessage(this.toText(message), sentryLevel);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Map a logger level to a Sentry severity. `success` has no Sentry\r\n * equivalent, so it is reported as informational.\r\n */\r\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\r\n switch (level) {\r\n case \"warn\":\r\n return \"warning\";\r\n case \"success\":\r\n return \"info\";\r\n default:\r\n return level; // debug | info | error | fatal map 1:1\r\n }\r\n }\r\n\r\n /**\r\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\r\n * `.message`, a string as-is, anything else safely JSON-serialized.\r\n */\r\n private toText(message: unknown): string {\r\n if (typeof message === \"string\") {\r\n return message;\r\n }\r\n\r\n if (message instanceof Error) {\r\n return message.message;\r\n }\r\n\r\n return safeJsonStringify(message);\r\n }\r\n\r\n /**\r\n * Surface the install instructions exactly once when the SDK is absent. The\r\n * logger can't log through itself here, so this writes to stderr — matching\r\n * how the file channels report write failures.\r\n */\r\n private reportMissingSdk(): void {\r\n if (isModuleExists === false && !this.warnedMissing) {\r\n this.warnedMissing = true;\r\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;AA+EA,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,8BAA8B;;;;;;;;;;EAUlC,KAAK;;;;;;AAOP,SAAS,aAA4B;CACnC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GAIF,SAAU,MAAM,OAAO;GACvB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,YAAb,cAA+B,WAA4B;;;;;;;;CAwCzD,AAAO,YAAY,gBAAkC;EACnD,MAAM,cAAc;cArCR;qBAMZ;+BAKiD;GACjD,aAAa;IAAC;IAAS;IAAS;GAAM;GACtC,cAAc;EAChB;uBAawB;EAYtB,MAAM,WAAW,KAAK,OAAO,QAAQ;EAErC,IAAI,UACF,KAAK,SAAS;CAElB;;;;;;;;CASA,MAAgB,OAAsB;EACpC,MAAM,WAAW,KAAK,OAAO,QAAQ;EAErC,IAAI,UAAU;GACZ,KAAK,SAAS;GAEd;EACF;EAEA,MAAM,WAAW;EAEjB,IAAI,CAAC,QACH;EAGF,MAAM,UAAU,KAAK,OAAO,SAAS;EAErC,IAAI,WAAW,CAAC,OAAO,UAAU,GAC/B,OAAO,KAAK,OAAO;EAGrB,KAAK,SAAS;CAChB;;;;CAKA,MAAa,IAAI,MAAkC;EACjD,IAAI,CAAC,KAAK,eAAe,IAAI,GAC3B;EAGF,IAAI,mBAAmB,MAErB,MAAM,WAAW;EAGnB,IAAI,CAAC,KAAK,QAAQ;GAChB,KAAK,iBAAiB;GAEtB;EACF;EAEA,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,OAAO,YAAY;EAE1D,IAAI,KAAK,aAAa,KAAK,GAAG;GAC5B,KAAK,aAAa,KAAK,QAAQ;IAC7B;IACA;IACA;IACA;IACA;GACF,CAAC;GAED;EACF;EAEA,KAAK,OAAO,cAAc;GACxB,UAAU;GACV,SAAS,KAAK,OAAO,OAAO;GAC5B,OAAO,KAAK,cAAc,KAAK;GAC/B,MAAM;EACR,CAAC;CACH;;;;;CAMA,MAAa,QAAuB;EAClC,IAAI,CAAC,KAAK,QACR;EAGF,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,cAAc,CAAC;CACrD;;;;CAKA,AAAQ,aAAa,OAA0B;EAC7C,OAAO,QAAQ,KAAK,OAAO,aAAa,CAAC,EAAE,SAAS,KAAK,CAAC;CAC5D;;;;;;;;CASA,AAAQ,aACN,QACA,OAOM;EACN,MAAM,EAAE,QAAQ,QAAQ,SAAS,OAAO,YAAY;EACpD,MAAM,cAAc,KAAK,cAAc,KAAK;EAE5C,OAAO,WAAW,UAAU;GAC1B,MAAM,SAAS,WAAW;GAC1B,MAAM,QAAQ;IAAE;IAAQ;GAAO,CAAC;GAEhC,IAAI,SACF,MAAM,WAAW,WAAW,OAAO;GAGrC,IAAI,mBAAmB,OACrB,OAAO,iBAAiB,OAAO;QAE/B,OAAO,eAAe,KAAK,OAAO,OAAO,GAAG,WAAW;EAE3D,CAAC;CACH;;;;;CAMA,AAAQ,cAAc,OAAsC;EAC1D,QAAQ,OAAR;GACE,KAAK,QACH,OAAO;GACT,KAAK,WACH,OAAO;GACT,SACE,OAAO;EACX;CACF;;;;;CAMA,AAAQ,OAAO,SAA0B;EACvC,IAAI,OAAO,YAAY,UACrB,OAAO;EAGT,IAAI,mBAAmB,OACrB,OAAO,QAAQ;EAGjB,OAAO,kBAAkB,OAAO;CAClC;;;;;;CAOA,AAAQ,mBAAyB;EAC/B,IAAI,mBAAmB,SAAS,CAAC,KAAK,eAAe;GACnD,KAAK,gBAAgB;GACrB,QAAQ,MAAM,2BAA2B;EAC3C;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"sentry-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/sentry-log.ts"],"sourcesContent":["import { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\n\r\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\r\n// @sentry/node is referenced ONLY through the local minimal types below — never\r\n// via `typeof import(\"@sentry/node\")`. This package is served as source\r\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\r\n// program; a static type reference to the SDK would force module resolution and\r\n// break every consumer who (correctly) never installs the optional peer with a\r\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\r\n// the same reason, and its result is cast to these shapes. The shapes mirror the\r\n// stable `@sentry/node` public API (unchanged across v8–v10).\r\n\r\n/**\r\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\r\n */\r\ntype SentrySeverityLevel =\r\n | \"fatal\"\r\n | \"error\"\r\n | \"warning\"\r\n | \"log\"\r\n | \"info\"\r\n | \"debug\";\r\n\r\n/**\r\n * The Sentry `Scope` surface used while building a single event.\r\n */\r\nexport interface SentryScopeLike {\r\n setLevel(level: SentrySeverityLevel): void;\r\n setTags(tags: Record<string, string>): void;\r\n setContext(name: string, context: Record<string, unknown> | null): void;\r\n}\r\n\r\n/**\r\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\r\n */\r\nexport type SentryBreadcrumb = {\r\n category?: string;\r\n message?: string;\r\n level?: SentrySeverityLevel;\r\n data?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\r\n * namespace satisfies this shape, so an app can pass it straight through as\r\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\r\n */\r\nexport interface SentryForwarder {\r\n captureException(exception: unknown): string;\r\n captureMessage(message: string, level?: SentrySeverityLevel): string;\r\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\r\n withScope(callback: (scope: SentryScopeLike) => void): void;\r\n flush(timeout?: number): Promise<boolean>;\r\n}\r\n\r\n/**\r\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\r\n * fields; the index signature keeps any other SDK option valid without coupling\r\n * to the SDK's types.\r\n */\r\nexport type SentryInitOptions = {\r\n dsn?: string;\r\n environment?: string;\r\n release?: string;\r\n sampleRate?: number;\r\n [key: string]: unknown;\r\n};\r\n\r\n/**\r\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\r\n * lifecycle calls the channel uses when it owns initialization.\r\n */\r\ninterface SentryNamespace extends SentryForwarder {\r\n init(options?: SentryInitOptions): unknown;\r\n getClient(): unknown;\r\n}\r\n\r\nlet Sentry: SentryNamespace | undefined;\r\nlet isModuleExists: boolean | null = null;\r\nlet loadingPromise: Promise<void> | undefined;\r\n\r\nconst SENTRY_INSTALL_INSTRUCTIONS = `\r\nThe Sentry log channel requires the @sentry/node package.\r\nInstall it with:\r\n\r\n npm install @sentry/node\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add @sentry/node\r\n yarn add @sentry/node\r\n`.trim();\r\n\r\n/**\r\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\r\n * import failure to \"not installed\" — the curated install message surfaces at\r\n * `log()` time, never as a boot-time module-resolution crash.\r\n */\r\nfunction loadSentry(): Promise<void> {\r\n if (isModuleExists !== null) {\r\n return Promise.resolve();\r\n }\r\n\r\n if (loadingPromise) {\r\n return loadingPromise;\r\n }\r\n\r\n loadingPromise = (async () => {\r\n try {\r\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\r\n // statically resolves the optional peer — see the file header.\r\n const sentryModule: string = \"@sentry/node\";\r\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n })();\r\n\r\n return loadingPromise;\r\n}\r\n\r\nexport type SentryLogConfig = BasicLogConfigurations & {\r\n /**\r\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\r\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\r\n * channel forwards through it and never imports or re-initializes the SDK.\r\n */\r\n client?: SentryForwarder;\r\n /**\r\n * Initialize Sentry from these options instead of reusing a host client. The\r\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\r\n * guarded so it never clobbers an existing client.\r\n */\r\n options?: SentryInitOptions;\r\n /**\r\n * Levels delivered as Sentry *events* (these consume the error quota). Every\r\n * other level is recorded as a breadcrumb that rides along with the next\r\n * event, costing no quota.\r\n *\r\n * @default [\"fatal\", \"error\", \"warn\"]\r\n */\r\n eventLevels?: LogLevel[];\r\n /**\r\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\r\n *\r\n * @default 2000\r\n */\r\n flushTimeout?: number;\r\n};\r\n\r\n/**\r\n * Forwards log entries to Sentry.\r\n *\r\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\r\n * **events**: an `Error` message via `captureException` (preserving the real\r\n * stack), any other message via `captureMessage`. Every other level becomes a\r\n * **breadcrumb** — buffered and attached to the next event, consuming no error\r\n * quota. `module` / `action` are attached as searchable tags and the entry's\r\n * `context` as a structured Sentry context.\r\n *\r\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\r\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\r\n * graceful shutdown, `await log.flush()` drains pending events via\r\n * `Sentry.flush(timeout)`.\r\n *\r\n * @example\r\n * // Existing app — reuse the initialized Sentry client\r\n * import * as Sentry from \"@sentry/node\";\r\n * log.addChannel(new SentryLog({ client: Sentry }));\r\n *\r\n * @example\r\n * // New app — let the channel initialize Sentry\r\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\r\n */\r\nexport class SentryLog extends LogChannel<SentryLogConfig> {\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public name = \"sentry\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public description =\r\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n protected defaultConfigurations: SentryLogConfig = {\r\n eventLevels: [\"fatal\", \"error\", \"warn\"],\r\n flushTimeout: 2_000,\r\n };\r\n\r\n /**\r\n * The resolved forwarder — the injected `client` or the lazily-imported\r\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\r\n * SDK is absent (then `log()` surfaces the install message once).\r\n */\r\n private sentry?: SentryForwarder;\r\n\r\n /**\r\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\r\n * doesn't spam stderr on every entry.\r\n */\r\n private warnedMissing = false;\r\n\r\n /**\r\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\r\n * entry logged on the same tick as construction — e.g. at app boot, before\r\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\r\n * silently dropped. The `options` (lazy-import) path is inherently async and\r\n * still resolves in `init()`.\r\n */\r\n public constructor(configurations?: SentryLogConfig) {\r\n super(configurations);\r\n\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\r\n * `@sentry/node` and (only when explicit `options` are supplied and no client\r\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\r\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\r\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\r\n */\r\n protected async init(): Promise<void> {\r\n const injected = this.config(\"client\");\r\n\r\n if (injected) {\r\n this.sentry = injected;\r\n\r\n return;\r\n }\r\n\r\n await loadSentry();\r\n\r\n if (!Sentry) {\r\n return;\r\n }\r\n\r\n const options = this.config(\"options\");\r\n\r\n if (options && !Sentry.getClient()) {\r\n Sentry.init(options);\r\n }\r\n\r\n this.sentry = Sentry;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async log(data: LoggingData): Promise<void> {\r\n if (!this.shouldBeLogged(data)) {\r\n return;\r\n }\r\n\r\n // Only wait for the optional SDK when we do not already have a forwarder.\r\n // An INJECTED client is resolved synchronously in the constructor, and that is\r\n // the whole point of doing it there: an entry logged on the same tick as\r\n // construction must still send. Awaiting loadSentry() unconditionally undid\r\n // that — with @sentry/node absent (it is an optional peer) the import can take\r\n // seconds, so a log with a perfectly good injected client stalled behind an SDK\r\n // it was never going to use.\r\n if (!this.sentry && isModuleExists === null) {\r\n // wait until module is fully loaded\r\n await loadSentry();\r\n }\r\n\r\n if (!this.sentry) {\r\n this.reportMissingSdk();\r\n\r\n return;\r\n }\r\n\r\n const { module, action, message, type: level, context } = data;\r\n\r\n if (this.isEventLevel(level)) {\r\n this.captureEvent(this.sentry, {\r\n module,\r\n action,\r\n message,\r\n level,\r\n context,\r\n });\r\n\r\n return;\r\n }\r\n\r\n this.sentry.addBreadcrumb({\r\n category: module,\r\n message: this.toText(message),\r\n level: this.toSentryLevel(level),\r\n data: context,\r\n });\r\n }\r\n\r\n /**\r\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\r\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\r\n */\r\n public async flush(): Promise<void> {\r\n if (!this.sentry) {\r\n return;\r\n }\r\n\r\n await this.sentry.flush(this.config(\"flushTimeout\"));\r\n }\r\n\r\n /**\r\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\r\n */\r\n private isEventLevel(level: LogLevel): boolean {\r\n return Boolean(this.config(\"eventLevels\")?.includes(level));\r\n }\r\n\r\n /**\r\n * Send an entry as a Sentry event. An `Error` message goes through\r\n * `captureException` so Sentry parses the real stack and groups properly;\r\n * any other message goes through `captureMessage`. `module` / `action` are\r\n * attached as tags and `context` as a structured context, scoped to this\r\n * event only via `withScope`.\r\n */\r\n private captureEvent(\r\n sentry: SentryForwarder,\r\n entry: {\r\n module: string;\r\n action: string;\r\n message: unknown;\r\n level: LogLevel;\r\n context?: Record<string, any>;\r\n },\r\n ): void {\r\n const { module, action, message, level, context } = entry;\r\n const sentryLevel = this.toSentryLevel(level);\r\n\r\n sentry.withScope((scope) => {\r\n scope.setLevel(sentryLevel);\r\n scope.setTags({ module, action });\r\n\r\n if (context) {\r\n scope.setContext(\"context\", context);\r\n }\r\n\r\n if (message instanceof Error) {\r\n sentry.captureException(message);\r\n } else {\r\n sentry.captureMessage(this.toText(message), sentryLevel);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Map a logger level to a Sentry severity. `success` has no Sentry\r\n * equivalent, so it is reported as informational.\r\n */\r\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\r\n switch (level) {\r\n case \"warn\":\r\n return \"warning\";\r\n case \"success\":\r\n return \"info\";\r\n default:\r\n return level; // debug | info | error | fatal map 1:1\r\n }\r\n }\r\n\r\n /**\r\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\r\n * `.message`, a string as-is, anything else safely JSON-serialized.\r\n */\r\n private toText(message: unknown): string {\r\n if (typeof message === \"string\") {\r\n return message;\r\n }\r\n\r\n if (message instanceof Error) {\r\n return message.message;\r\n }\r\n\r\n return safeJsonStringify(message);\r\n }\r\n\r\n /**\r\n * Surface the install instructions exactly once when the SDK is absent. The\r\n * logger can't log through itself here, so this writes to stderr — matching\r\n * how the file channels report write failures.\r\n */\r\n private reportMissingSdk(): void {\r\n if (isModuleExists === false && !this.warnedMissing) {\r\n this.warnedMissing = true;\r\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["../../../../../../logger/src/log-channel.ts"],"mappings":";;;uBAOsB,UAAA,iBACJ,sBAAA,GAAyB,sBAAA,aAC9B,WAAA;;AAFb;;EAMS,IAAA;EALS;;;EAUT,WAAA;EAyB6B;;;EApB7B,QAAA;EA0C4C;;;EAAA,UArCzC,qBAAA,EAAuB,OAAA;EA6EP;;;EAAA,UAxEhB,qBAAA,EAAuB,OAAA;EAgHyB;;;EAAA,UA3GhD,aAAA;EA7BY;;;cAkCH,cAAA,GAAiB,OAAA;EAnCK;;;EAAA,UAoD/B,IAAA,
|
|
1
|
+
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["../../../../../../logger/src/log-channel.ts"],"mappings":";;;uBAOsB,UAAA,iBACJ,sBAAA,GAAyB,sBAAA,aAC9B,WAAA;;AAFb;;EAMS,IAAA;EALS;;;EAUT,WAAA;EAyB6B;;;EApB7B,QAAA;EA0C4C;;;EAAA,UArCzC,qBAAA,EAAuB,OAAA;EA6EP;;;EAAA,UAxEhB,qBAAA,EAAuB,OAAA;EAgHyB;;;EAAA,UA3GhD,aAAA;EA7BY;;;cAkCH,cAAA,GAAiB,OAAA;EAnCK;;;EAAA,UAoD/B,IAAA,CAAA,CAAA,UAAgB,OAAA;EArCnB;;;EAAA,UA0CG,MAAA,iBAAuB,OAAA,CAAA,CAAS,GAAA,EAAK,CAAA,GAAI,OAAA,CAAQ,CAAA;EAhC1B;;;EAAA,UAyCvB,iBAAA,CAAkB,cAAA,EAAgB,OAAA;EA/BzB;;;EAAA,UA2CT,cAAA,CAAe,IAAA,EAAM,WAAA;EArBd;;;EAAA,SAwCD,GAAA,CAAI,IAAA,EAAM,WAAA,UAAqB,OAAA;EAxCI;;;EA6C5C,SAAA,CAAA,CAAA;EApCqB;;;;;;;EA6CrB,KAAA,CAAA,CAAA,UAAiB,OAAA;EATjB;;;;;;EAiBA,eAAA,CAAA,GAAmB,YAAA;;;;YAOhB,oBAAA,CAAA;;;;EAWkE;;;EAAA,UAAlE,uBAAA,CAAwB,cAAA,EAAgB,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA"}
|
package/esm/log-channel.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-channel.mjs","names":[],"sources":["../../../../../../logger/src/log-channel.ts"],"sourcesContent":["import type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\n\r\nexport abstract class LogChannel<\r\n Options extends BasicLogConfigurations = BasicLogConfigurations,\r\n> implements LogContract {\r\n /**\r\n * Channel name\r\n */\r\n public name!: string;\r\n\r\n /**\r\n * Channel description\r\n */\r\n public description?: string;\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = false;\r\n\r\n /**\r\n * Default Configurations\r\n */\r\n protected defaultConfigurations: Options = {} as Options;\r\n\r\n /**\r\n * Channel configurations\r\n */\r\n protected channelConfigurations: Options = {} as Options; //\r\n\r\n /**\r\n * Determine whether the channel is fully initialized\r\n */\r\n protected isInitialized = false;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(configurations?: Options) {\r\n if (configurations) {\r\n this.setConfigurations(configurations);\r\n }\r\n\r\n setTimeout(async () => {\r\n if (this.init) {\r\n await this.init();\r\n }\r\n\r\n this.isInitialized = true;\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Initialize the channel\r\n */\r\n protected init?(): void | Promise<void>;\r\n\r\n /**\r\n * Get config value\r\n */\r\n protected config<K extends keyof Options>(key: K): Options[K] {\r\n return (\r\n this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key]\r\n );\r\n }\r\n\r\n /**\r\n * Set configurations\r\n */\r\n protected setConfigurations(configurations: Options) {\r\n this.channelConfigurations = {\r\n ...this.channelConfigurations,\r\n ...configurations,\r\n };\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Determine if the message should be logged\r\n */\r\n protected shouldBeLogged(data: LoggingData): boolean {\r\n // check for debug mode\r\n const allowedLevels = this.config(\"levels\");\r\n\r\n if (allowedLevels?.length && !allowedLevels.includes(data.type))\r\n return false;\r\n\r\n const filter = this.config(\"filter\");\r\n\r\n if (filter) {\r\n return filter(data);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Log the given message\r\n */\r\n public abstract log(data: LoggingData): void | Promise<void>;\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync?(): void;\r\n\r\n /**\r\n * Asynchronously flush buffered messages.\r\n *\r\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\r\n * don't implement it. Buffering channels override this to drain their buffer\r\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\r\n */\r\n public flush?(): void | Promise<void>;\r\n\r\n /**\r\n * Read the channel's redact config (if any). Used by `Logger` to apply\r\n * per-channel additive redaction on top of the logger-wide floor.\r\n * Subclasses normally don't override this — set `redact` in your channel\r\n * configuration instead.\r\n */\r\n public getRedactConfig(): RedactConfig | undefined {\r\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\r\n }\r\n\r\n /**\r\n * Get date and time formats\r\n */\r\n protected getDateAndTimeFormat() {\r\n const dateFormat = this.config(\"dateFormat\");\r\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\r\n const time = dateFormat?.time ?? \"HH:mm:ss\";\r\n\r\n return { date, time };\r\n }\r\n\r\n /**\r\n * get basic configurations with the given ones\r\n */\r\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\r\n return {\r\n filter: () => true,\r\n ...configurations,\r\n } as any as Options;\r\n }\r\n}\r\n"],"mappings":";AAOA,IAAsB,aAAtB,MAEyB;;;;CAkCvB,AAAO,YAAY,gBAA0B;kBApB3B;+BAKyB,CAAC;+BAKD,CAAC;uBAKlB;EAMxB,IAAI,gBACF,KAAK,kBAAkB,cAAc;EAGvC,WAAW,YAAY;GACrB,IAAI,KAAK,MACP,MAAM,KAAK,KAAK;GAGlB,KAAK,gBAAgB;EACvB,GAAG,CAAC;CACN;;;;CAUA,AAAU,OAAgC,KAAoB;EAC5D,OACE,KAAK,sBAAsB,SAAS,KAAK,yBAAyB,CAAC,
|
|
1
|
+
{"version":3,"file":"log-channel.mjs","names":[],"sources":["../../../../../../logger/src/log-channel.ts"],"sourcesContent":["import type {\r\n BasicLogConfigurations,\r\n LogContract,\r\n LoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\n\r\nexport abstract class LogChannel<\r\n Options extends BasicLogConfigurations = BasicLogConfigurations,\r\n> implements LogContract {\r\n /**\r\n * Channel name\r\n */\r\n public name!: string;\r\n\r\n /**\r\n * Channel description\r\n */\r\n public description?: string;\r\n\r\n /**\r\n * Determine if channel is logging in terminal\r\n */\r\n public terminal = false;\r\n\r\n /**\r\n * Default Configurations\r\n */\r\n protected defaultConfigurations: Options = {} as Options;\r\n\r\n /**\r\n * Channel configurations\r\n */\r\n protected channelConfigurations: Options = {} as Options; //\r\n\r\n /**\r\n * Determine whether the channel is fully initialized\r\n */\r\n protected isInitialized = false;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(configurations?: Options) {\r\n if (configurations) {\r\n this.setConfigurations(configurations);\r\n }\r\n\r\n setTimeout(async () => {\r\n if (this.init) {\r\n await this.init();\r\n }\r\n\r\n this.isInitialized = true;\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Initialize the channel\r\n */\r\n protected init?(): void | Promise<void>;\r\n\r\n /**\r\n * Get config value\r\n */\r\n protected config<K extends keyof Options>(key: K): Options[K] {\r\n return (\r\n this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key]\r\n );\r\n }\r\n\r\n /**\r\n * Set configurations\r\n */\r\n protected setConfigurations(configurations: Options) {\r\n this.channelConfigurations = {\r\n ...this.channelConfigurations,\r\n ...configurations,\r\n };\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Determine if the message should be logged\r\n */\r\n protected shouldBeLogged(data: LoggingData): boolean {\r\n // check for debug mode\r\n const allowedLevels = this.config(\"levels\");\r\n\r\n if (allowedLevels?.length && !allowedLevels.includes(data.type))\r\n return false;\r\n\r\n const filter = this.config(\"filter\");\r\n\r\n if (filter) {\r\n return filter(data);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Log the given message\r\n */\r\n public abstract log(data: LoggingData): void | Promise<void>;\r\n\r\n /**\r\n * Synchronously flush messages\r\n */\r\n public flushSync?(): void;\r\n\r\n /**\r\n * Asynchronously flush buffered messages.\r\n *\r\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\r\n * don't implement it. Buffering channels override this to drain their buffer\r\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\r\n */\r\n public flush?(): void | Promise<void>;\r\n\r\n /**\r\n * Read the channel's redact config (if any). Used by `Logger` to apply\r\n * per-channel additive redaction on top of the logger-wide floor.\r\n * Subclasses normally don't override this — set `redact` in your channel\r\n * configuration instead.\r\n */\r\n public getRedactConfig(): RedactConfig | undefined {\r\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\r\n }\r\n\r\n /**\r\n * Get date and time formats\r\n */\r\n protected getDateAndTimeFormat() {\r\n const dateFormat = this.config(\"dateFormat\");\r\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\r\n const time = dateFormat?.time ?? \"HH:mm:ss\";\r\n\r\n return { date, time };\r\n }\r\n\r\n /**\r\n * get basic configurations with the given ones\r\n */\r\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\r\n return {\r\n filter: () => true,\r\n ...configurations,\r\n } as any as Options;\r\n }\r\n}\r\n"],"mappings":";AAOA,IAAsB,aAAtB,MAEyB;;;;CAkCvB,AAAO,YAAY,gBAA0B;kBApB3B;+BAKyB,CAAC;+BAKD,CAAC;uBAKlB;EAMxB,IAAI,gBACF,KAAK,kBAAkB,cAAc;EAGvC,WAAW,YAAY;GACrB,IAAI,KAAK,MACP,MAAM,KAAK,KAAK;GAGlB,KAAK,gBAAgB;EACvB,GAAG,CAAC;CACN;;;;CAUA,AAAU,OAAgC,KAAoB;EAC5D,OACE,KAAK,sBAAsB,SAAS,KAAK,yBAAyB,CAAC,GAAG;CAE1E;;;;CAKA,AAAU,kBAAkB,gBAAyB;EACnD,KAAK,wBAAwB;GAC3B,GAAG,KAAK;GACR,GAAG;EACL;EAEA,OAAO;CACT;;;;CAKA,AAAU,eAAe,MAA4B;EAEnD,MAAM,gBAAgB,KAAK,OAAO,QAAQ;EAE1C,IAAI,eAAe,UAAU,CAAC,cAAc,SAAS,KAAK,IAAI,GAC5D,OAAO;EAET,MAAM,SAAS,KAAK,OAAO,QAAQ;EAEnC,IAAI,QACF,OAAO,OAAO,IAAI;EAGpB,OAAO;CACT;;;;;;;CA2BA,AAAO,kBAA4C;EACjD,OAAO,KAAK,OAAO,QAAyB;CAC9C;;;;CAKA,AAAU,uBAAuB;EAC/B,MAAM,aAAa,KAAK,OAAO,YAAY;EAI3C,OAAO;GAAE,MAHI,YAAY,QAAQ;GAGlB,MAFF,YAAY,QAAQ;EAEb;CACtB;;;;CAKA,AAAU,wBAAwB,gBAA2C;EAC3E,OAAO;GACL,cAAc;GACd,GAAG;EACL;CACF;AACF"}
|
package/esm/logger.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.mts","names":[],"sources":["../../../../../../logger/src/logger.ts"],"mappings":";;;;cAoCa,MAAA;;AAAb;;EAIS,QAAA,EAAU,UAAA;EAEV,EAAA;EA4BoB;;;;;EAAA,QArBnB,iBAAA;EA0EY;;;;;;EAAA,QAlEZ,QAAA;EAkLI;;;;;;EAAA,QA1KJ,YAAA;EAoMuB;;;EA/LxB,UAAA,CAAW,OAAA,EAAS,UAAA;EAsNX;;;EA7MT,SAAA,CAAU,MAAA;IACf,QAAA,GAAW,UAAA;IACX,WAAA,GAAc,cAAA;IACd,QAAA,GAAW,QAAA;IACX,MAAA,GAAS,YAAA;EAAA;EAsPU;;;;;;;;;;;EAtNd,SAAA,CAAU,MAAA,EAAQ,YAAA;EAzElB;;;EAiFA,SAAA,
|
|
1
|
+
{"version":3,"file":"logger.d.mts","names":[],"sources":["../../../../../../logger/src/logger.ts"],"mappings":";;;;cAoCa,MAAA;;AAAb;;EAIS,QAAA,EAAU,UAAA;EAEV,EAAA;EA4BoB;;;;;EAAA,QArBnB,iBAAA;EA0EY;;;;;;EAAA,QAlEZ,QAAA;EAkLI;;;;;;EAAA,QA1KJ,YAAA;EAoMuB;;;EA/LxB,UAAA,CAAW,OAAA,EAAS,UAAA;EAsNX;;;EA7MT,SAAA,CAAU,MAAA;IACf,QAAA,GAAW,UAAA;IACX,WAAA,GAAc,cAAA;IACd,QAAA,GAAW,QAAA;IACX,MAAA,GAAS,YAAA;EAAA;EAsPU;;;;;;;;;;;EAtNd,SAAA,CAAU,MAAA,EAAQ,YAAA;EAzElB;;;EAiFA,SAAA,CAAA,GAAa,YAAA;EArDb;;;;;;;;;;EAmEA,WAAA,CAAY,KAAA,EAAO,QAAA;EAtDf;;;EA8DJ,WAAA,CAAA,GAAe,QAAA;EA9BL;;;EAqCV,WAAA,CAAY,QAAA,EAAU,UAAA;EAfH;;;EAAA,QAwBlB,gBAAA;EATD;;;;;;;;;EA4CM,GAAA,CAAI,IAAA,EAAM,WAAA,GAAW,OAAA;EAoChC;;;EADK,KAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EAA7B;;;EASK,IAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EAH7B;;;EAYK,IAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EAb7B;;;EAsBK,KAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EAhB7B;;;EAyBK,OAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EA1B7B;;;;;;;;;EA0CK,KAAA,CACL,YAAA,EAAc,kBAAA,WACd,MAAA,WACA,OAAA,QACA,OAAA,GAAU,MAAA,gBAAmB,OAAA;EAxBxB;;;;;;;;;EAwCA,MAAA,CACL,SAAA,WACA,MAAA,UACA,MAAA,UACA,OAAA,OACA,OAAA,GAAU,MAAA,gBACT,OAAA,CAAQ,MAAA,IAAU,MAAA;EAzBnB;;;;;;;;;;EAwCK,KAAA,CACL,MAAA,UACA,MAAA,YACE,KAAA,GAAQ,MAAA,kBAAwB,OAAA,CAAQ,MAAA;EAnBhC;;;EAiCL,OAAA,CAAQ,IAAA,WAAY,UAAA,CAdgB,sBAAA;EAlBtB;;;EAuCd,SAAA,CAAA;EArBK;;;;;;;;;;;;;;;;AA6GW;AAyBzB;;;EArFe,KAAA,CAAA,GAAS,OAAA;EAqFO;;;;;;;;;;;;;;;;;;EAlDtB,eAAA,CAAgB,MAAA,EAAQ,cAAA;;;;;EAyBxB,gBAAA,CAAA;AAAA;;;;;;;;;;;;;;;cAyBI,GAAA,EAAG,MAAe"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-keys.mjs","names":[],"sources":["../../../../../../../logger/src/redact/default-keys.ts"],"sourcesContent":["/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,mBAAmB,KAAqB;CACtD,OAAO,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"default-keys.mjs","names":[],"sources":["../../../../../../../logger/src/redact/default-keys.ts"],"sourcesContent":["/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,mBAAmB,KAAqB;CACtD,OAAO,IAAI,YAAY,EAAE,QAAQ,cAAc,EAAE;AACnD;;;;;;;;;;AAWA,MAAa,sBAAyC;CAEpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CAOA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,yBAA8C,IAAI,IAC7D,oBAAoB,IAAI,kBAAkB,CAC5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":";;;AAMA,MAAM,iBAAiB;;;;;;;;AAyBvB,SAAS,SAAS,OAAwB;CACxC,OACE,YAAY,OAAO,KAAK,KACxB,iBAAiB,eACjB,iBAAiB,OACjB,iBAAiB,OACjB,iBAAiB,WACjB,iBAAiB,WACjB,iBAAiB,WACjB,iBAAiB;AAErB;;;;;;;;;;;;;;;AAgBA,SAAS,WACP,OACA,uBAAO,IAAI,QAAqB,GAChC,WACA,QAAkB,CAAC,GAChB;CACH,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,MAAM,WAAW;CAEjB,IAAI,KAAK,IAAI,QAAQ,GACnB,OAAO,KAAK,IAAI,QAAQ;CAG1B,IAAI,iBAAiB,MACnB,OAAO,IAAI,KAAK,MAAM,QAAQ,CAAC;CAGjC,IAAI,SAAS,QAAQ,GACnB,OAAO;CAGT,IAAI,iBAAiB,OACnB,OAAO,WAAW,OAAO,MAAM,WAAW,KAAK;CAGjD,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,MAAM,MAAa,CAAC;EACpB,KAAK,IAAI,UAAU,GAAG;EACtB,MAAM,SAAS,MAAM,UAAU;GAG7B,IAAI,KAAK,WAAW,MAAM,MAAM,WAAW,CAAC,GAAG,OAAO,OAAO,KAAK,CAAC,CAAC,CAAC;EACvE,CAAC;EACD,OAAO;CACT;CAEA,MAAM,MAA2B,CAAC;CAClC,KAAK,IAAI,UAAU,GAAG;CACtB,YAAY,OAA8B,KAAK,MAAM,WAAW,KAAK;CACrE,OAAO;AACT;;;;;AAMA,SAAS,YACP,QACA,QACA,MACA,WACA,OACM;CACN,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;EACrC,MAAM,aAAa,CAAC,GAAG,OAAO,GAAG;EAEjC,IACE,aACA,UAAU,KAAK,IAAI,mBAAmB,GAAG,CAAC,KAC1C,CAAC,UAAU,gBAAgB,IAAI,WAAW,KAAK,GAAG,CAAC,GACnD;GACA,OAAO,OAAO,YAAY,OAAO,MAAM,UAAU,QAAQ,UAAU;GACnE;EACF;EAEA,OAAO,OAAO,WAAW,OAAO,MAAM,MAAM,WAAW,UAAU;CACnE;AACF;;;;;;;;;;;;;;;AAgBA,SAAS,WACP,OACA,MACA,WACA,OACO;CACP,IAAI;CAEJ,IAAI;EACF,OAAO,IAAK,MAAM,YAAiC,MAAM,OAAO;CAClE,QAAQ;EAGN,OAAO,IAAI,MAAM,MAAM,OAAO;CAChC;CAOA,KAAK,MAAM,CAAC,KAAK,WAAW;EAC1B,CAAC,WAAW,MAAM,OAAO;EACzB,CAAC,QAAQ,MAAM,IAAI;EACnB,CAAC,SAAS,MAAM,KAAK;CACvB,GACE,OAAO,eAAe,MAAM,KAAK;EAC/B,OAAO;EACP,UAAU;EACV,YAAY;EACZ,cAAc;CAChB,CAAC;CAGH,KAAK,IAAI,OAAO,IAAI;CACpB,YAAY,OAAyC,MAAa,MAAM,WAAW,KAAK;CAExF,OAAO;AACT;;;;;;;AAQA,SAAS,YAAY,OAAY,QAAsB,MAAqB;CAC1E,IAAI,OAAO,WAAW,YACpB,OAAO,OAAO,OAAO,KAAK,KAAK,GAAG,CAAC;CAErC,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAS,aACP,QACA,UACA,QACA,WACA,UACM;CACN,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC;CAGF,IAAI,SAAS,WAAW,GACtB;CAGF,MAAM,CAAC,MAAM,GAAG,QAAQ;CAExB,IAAI,SAAS,MAAM;EAIjB,IAAI,KAAK,SAAS,GAChB,aAAa,QAAQ,MAAM,QAAQ,WAAW,QAAQ;EAExD,MAAM,OAAO,MAAM,QAAQ,MAAM,IAC7B,OAAO,KAAK,GAAG,UAAU,OAAO,KAAK,CAAC,IACtC,OAAO,KAAK,MAAM;EACtB,KAAK,MAAM,OAAO,MAChB,aAAa,OAAO,MAAM,UAAU,QAAQ,CAAC,GAAG,WAAW,GAAG,GAAG,QAAQ;EAE3E;CACF;CAEA,MAAM,cACJ,SAAS,MACL,MAAM,QAAQ,MAAM,IAClB,OAAO,KAAK,GAAG,UAAU,OAAO,KAAK,CAAC,IACtC,OAAO,KAAK,MAAM,IACpB,MAAM,QAAQ,MAAM,IAElB,QAAQ,KAAK,IAAI,KAAK,OAAO,IAAI,IAAI,OAAO,SAC1C,CAAC,IAAI,IACL,CAAC,IACH,OAAO,UAAU,eAAe,KAAK,QAAQ,IAAI,IAC/C,CAAC,IAAI,IACL,CAAC;CAEX,KAAK,MAAM,OAAO,aAChB,IAAI,KAAK,WAAW,GAAG;EACrB,MAAM,YAAY,CAAC,GAAG,WAAW,GAAG;EACpC,OAAO,OAAO,YAAY,OAAO,MAAM,QAAQ,SAAS;EACxD,SAAS,IAAI,UAAU,KAAK,GAAG,CAAC;CAClC,OACE,aAAa,OAAO,MAAM,MAAM,QAAQ,CAAC,GAAG,WAAW,GAAG,GAAG,QAAQ;AAG3E;;;;;;;;;AAUA,SAAS,iBACP,OACA,MACA,uBAAO,IAAI,QAAgB,GAClB;CACT,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,MAAM,WAAW;CAEjB,IAAI,KAAK,IAAI,QAAQ,GAAG,OAAO;CAC/B,KAAK,IAAI,QAAQ;CAEjB,IAAI,iBAAiB,QAAQ,SAAS,QAAQ,GAC5C,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,SAAS,iBAAiB,MAAM,MAAM,IAAI,CAAC;CAGhE,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;EACpC,IAAI,KAAK,IAAI,mBAAmB,GAAG,CAAC,GAAG,OAAO;EAC9C,IAAI,iBAAiB,MAAM,MAAM,MAAM,IAAI,GAAG,OAAO;CACvD;CAEA,OAAO;AACT;;;;;;;AAQA,MAAM,8BAAc,IAAI,QAA2C;;;;;;;;AASnE,SAAgB,kBACd,QACiC;CACjC,MAAM,cAAc,QAAQ,gBAAgB;CAC5C,MAAM,QAAQ,QAAQ;CAEtB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B,OAAO,cAAc,yBAAyB;CAGhD,MAAM,SAAS,UAAU,YAAY,IAAI,MAAM;CAC/C,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,IAAI,IAAY,cAAc,yBAAyB,CAAC,CAAC;CAC1E,KAAK,MAAM,OAAO,OAChB,SAAS,IAAI,mBAAmB,GAAG,CAAC;CAGtC,IAAI,QAAQ,YAAY,IAAI,QAAQ,QAAQ;CAE5C,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,YACd,MACA,QACa;CACb,MAAM,QAAQ,QAAQ,SAAS,CAAC;CAChC,MAAM,OAAO,kBAAkB,MAAM;CAErC,IAAI,MAAM,WAAW,KAAK,CAAC,MACzB,OAAO;CAGT,MAAM,SAAS,QAAQ,UAAU;CAEjC,IAAI,SAAS;CACb,MAAM,2BAAW,IAAI,IAAY;CAEjC,IAAI,MAAM,SAAS,GAAG;EACpB,SAAS,WAAW,IAAI;EAExB,KAAK,MAAM,WAAW,OAAO;GAC3B,MAAM,WAAW,QAAQ,MAAM,GAAG,EAAE,QAAQ,YAAY,QAAQ,SAAS,CAAC;GAC1E,IAAI,SAAS,WAAW,GAAG;GAC3B,aAAa,QAAQ,UAAU,QAAQ,CAAC,GAAG,QAAQ;EACrD;CACF;CAEA,IAAI,QAAQ,iBAAiB,QAAQ,IAAI,GACvC,SAAS,WAAW,wBAAQ,IAAI,QAAQ,GAAG;EACzC;EACA;EACA,iBAAiB;CACnB,CAAC;CAGH,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,YACd,MACA,OAC0B;CAC1B,IAAI,CAAC,QAAQ,CAAC,OAAO,OAAO;CAE5B,IAAI,CAAC,MAIH,OAAO,MAAO,gBAAgB,QAC1B;EAAE,GAAG;EAAQ,aAAa;CAAK,IAC/B;CAGN,IAAI,CAAC,OAAO,OAAO;CAEnB,OAAO;EACL,OAAO,CAAC,GAAI,KAAK,SAAS,CAAC,GAAI,GAAI,MAAM,SAAS,CAAC,CAAE;EACrD,MAAM,CAAC,GAAI,KAAK,QAAQ,CAAC,GAAI,GAAI,MAAM,QAAQ,CAAC,CAAE;EAClD,aAAa,MAAM,gBAAgB,OAAO,OAAO,KAAK;EACtD,QAAQ,MAAM,UAAU,KAAK;CAC/B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\r\n\r\n/**\r\n * Options for {@link captureAnyUnhandledRejection}.\r\n */\r\nexport type CaptureUnhandledOptions = {\r\n /**\r\n * Take the process down with `process.exit(1)` after an `uncaughtException`\r\n * is logged. Defaults to `true` — an uncaught exception leaves the process\r\n * in an undefined state, so Node's own default is to exit non-zero.\r\n *\r\n * Registering an `uncaughtException` listener *suppresses* that default, so a\r\n * listener that only logs turns an unrecoverable crash into a silent\r\n * `exit 0` (which is how a config file throwing at boot looks like \"the\r\n * server just stopped\"). Keep this `true` in production so supervisors\r\n * restart and the failure is never silent. Set it to `false` where the\r\n * process is expected to recover on its own — e.g. a dev server that reloads\r\n * via HMR — to log the exception without exiting.\r\n */\r\n exitOnUncaughtException?: boolean;\r\n};\r\n\r\n/**\r\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\r\n * the forced exit. Bounded so a single stuck channel can't hang the process —\r\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\r\n * handler would not cover this path.\r\n */\r\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context, and make an\r\n * `uncaughtException` loud + visible instead of silently swallowed.\r\n *\r\n * Registers one listener for `unhandledRejection` and one for\r\n * `uncaughtException`; call once at startup after channels are configured.\r\n *\r\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\r\n * When no terminal channel has been configured yet — the early-boot window a\r\n * config file throwing at import time falls into — the stack is also written\r\n * to `console.error`, standing in for the Node default this listener\r\n * suppresses, so a fatal boot error is never invisible. A configured\r\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\r\n * channel exists (no double output).\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\r\n * not necessarily process-ending, so it stays at error and never exits.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection(); // exits on uncaught\r\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\r\n */\r\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\r\n const { exitOnUncaughtException = true } = options;\r\n\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n // Route through the logger first so file/Sentry channels capture it. A\r\n // crash handler must never itself throw (that would re-enter Node's\r\n // uncaught path), so the logging call is guarded.\r\n try {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n } catch {\r\n // fall through to the stderr fallback + exit below\r\n }\r\n\r\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\r\n // see when this fires before any terminal channel is configured — exactly\r\n // the early-boot window a config file throwing at import time falls into,\r\n // which is how a fatal boot error used to masquerade as a silent\r\n // `exit 0`. Stand in for the Node default this listener suppressed, but\r\n // only when no terminal channel already printed it, so a configured\r\n // `ConsoleLog` is never doubled.\r\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\r\n\r\n if (!hasTerminalChannel) {\r\n console.error(error);\r\n }\r\n\r\n if (!exitOnUncaughtException) {\r\n return;\r\n }\r\n\r\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\r\n // process supervisors restart and `warlock start` surfaces the failure.\r\n void Promise.race([\r\n log.flush(),\r\n new Promise<void>((resolve) => {\r\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\r\n timer.unref?.();\r\n }),\r\n ]).then(\r\n () => process.exit(1),\r\n () => process.exit(1),\r\n );\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,
|
|
1
|
+
{"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\r\n\r\n/**\r\n * Options for {@link captureAnyUnhandledRejection}.\r\n */\r\nexport type CaptureUnhandledOptions = {\r\n /**\r\n * Take the process down with `process.exit(1)` after an `uncaughtException`\r\n * is logged. Defaults to `true` — an uncaught exception leaves the process\r\n * in an undefined state, so Node's own default is to exit non-zero.\r\n *\r\n * Registering an `uncaughtException` listener *suppresses* that default, so a\r\n * listener that only logs turns an unrecoverable crash into a silent\r\n * `exit 0` (which is how a config file throwing at boot looks like \"the\r\n * server just stopped\"). Keep this `true` in production so supervisors\r\n * restart and the failure is never silent. Set it to `false` where the\r\n * process is expected to recover on its own — e.g. a dev server that reloads\r\n * via HMR — to log the exception without exiting.\r\n */\r\n exitOnUncaughtException?: boolean;\r\n};\r\n\r\n/**\r\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\r\n * the forced exit. Bounded so a single stuck channel can't hang the process —\r\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\r\n * handler would not cover this path.\r\n */\r\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context, and make an\r\n * `uncaughtException` loud + visible instead of silently swallowed.\r\n *\r\n * Registers one listener for `unhandledRejection` and one for\r\n * `uncaughtException`; call once at startup after channels are configured.\r\n *\r\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\r\n * When no terminal channel has been configured yet — the early-boot window a\r\n * config file throwing at import time falls into — the stack is also written\r\n * to `console.error`, standing in for the Node default this listener\r\n * suppresses, so a fatal boot error is never invisible. A configured\r\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\r\n * channel exists (no double output).\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\r\n * not necessarily process-ending, so it stays at error and never exits.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection(); // exits on uncaught\r\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\r\n */\r\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\r\n const { exitOnUncaughtException = true } = options;\r\n\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n // Route through the logger first so file/Sentry channels capture it. A\r\n // crash handler must never itself throw (that would re-enter Node's\r\n // uncaught path), so the logging call is guarded.\r\n try {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n } catch {\r\n // fall through to the stderr fallback + exit below\r\n }\r\n\r\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\r\n // see when this fires before any terminal channel is configured — exactly\r\n // the early-boot window a config file throwing at import time falls into,\r\n // which is how a fatal boot error used to masquerade as a silent\r\n // `exit 0`. Stand in for the Node default this listener suppressed, but\r\n // only when no terminal channel already printed it, so a configured\r\n // `ConsoleLog` is never doubled.\r\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\r\n\r\n if (!hasTerminalChannel) {\r\n console.error(error);\r\n }\r\n\r\n if (!exitOnUncaughtException) {\r\n return;\r\n }\r\n\r\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\r\n // process supervisors restart and `warlock start` surfaces the failure.\r\n void Promise.race([\r\n log.flush(),\r\n new Promise<void>((resolve) => {\r\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\r\n timer.unref?.();\r\n }),\r\n ]).then(\r\n () => process.exit(1),\r\n () => process.exit(1),\r\n );\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,EAAE,QAAQ;EAChB,CAAC,CACH,CAAC,EAAE,WACK,QAAQ,KAAK,CAAC,SACd,QAAQ,KAAK,CAAC,CACtB;CACF,CAAC;AACH"}
|
package/llms-full.txt
CHANGED
|
@@ -578,6 +578,10 @@ Structured logging for Node. Six severity levels (with `fatal` strictly above `e
|
|
|
578
578
|
|
|
579
579
|
Ships standalone — `@warlock.js/core` is not required. Drop it into any Node project.
|
|
580
580
|
|
|
581
|
+
## Server-only package
|
|
582
|
+
|
|
583
|
+
`@warlock.js/logger`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. This is build-boundary metadata read by `@warlock.js/web`'s Gate A (import resolution) and Gate C (emitted-bundle verification), not application behavior. App code that reaches the client bundle (including page and layout modules) must not value-import `@warlock.js/logger` — Gate A refuses the build. A type-only import (`import type { ... }`) is allowed. Server loaders, controllers, and modules may import it freely.
|
|
584
|
+
|
|
581
585
|
## When to reach for it
|
|
582
586
|
|
|
583
587
|
- Building a Node service that needs **structured** logs (key-value pairs, not bare strings) and you want them to land in multiple destinations (console for dev, JSON file for prod, third-party sink for audits) without rewriting the call sites.
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@mongez/copper": "^2.1.2",
|
|
6
6
|
"@mongez/reinforcements": "^4.0.1",
|
|
7
|
-
"@warlock.js/fs": "5.
|
|
7
|
+
"@warlock.js/fs": "5.2.2",
|
|
8
8
|
"dayjs": "^1.11.9",
|
|
9
9
|
"safe-stable-stringify": "^2.5.0"
|
|
10
10
|
},
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"author": "hassanzohdy",
|
|
31
31
|
"license": "MIT",
|
|
32
|
-
"
|
|
32
|
+
"warlock": {
|
|
33
|
+
"environment": "server"
|
|
34
|
+
},
|
|
35
|
+
"version": "5.2.2",
|
|
33
36
|
"main": "./cjs/index.cjs",
|
|
34
37
|
"module": "./esm/index.mjs",
|
|
35
38
|
"types": "./esm/index.d.mts",
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -9,6 +9,10 @@ Structured logging for Node. Six severity levels (with `fatal` strictly above `e
|
|
|
9
9
|
|
|
10
10
|
Ships standalone — `@warlock.js/core` is not required. Drop it into any Node project.
|
|
11
11
|
|
|
12
|
+
## Server-only package
|
|
13
|
+
|
|
14
|
+
`@warlock.js/logger`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. This is build-boundary metadata read by `@warlock.js/web`'s Gate A (import resolution) and Gate C (emitted-bundle verification), not application behavior. App code that reaches the client bundle (including page and layout modules) must not value-import `@warlock.js/logger` — Gate A refuses the build. A type-only import (`import type { ... }`) is allowed. Server loaders, controllers, and modules may import it freely.
|
|
15
|
+
|
|
12
16
|
## When to reach for it
|
|
13
17
|
|
|
14
18
|
- Building a Node service that needs **structured** logs (key-value pairs, not bare strings) and you want them to land in multiple destinations (console for dev, JSON file for prod, third-party sink for audits) without rewriting the call sites.
|