@warlock.js/logger 5.29.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -2
- package/README.md +14 -17
- package/cjs/index.cjs +569 -57
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.d.mts +1 -1
- package/esm/channels/console-log.d.mts.map +1 -1
- package/esm/channels/console-log.mjs +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +129 -13
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +204 -23
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/file-name-pattern.d.mts +16 -0
- package/esm/channels/file-name-pattern.d.mts.map +1 -0
- package/esm/channels/file-name-pattern.mjs +41 -0
- package/esm/channels/file-name-pattern.mjs.map +1 -0
- package/esm/channels/index.mjs +1 -1
- package/esm/channels/json-file-log.d.mts +1 -1
- package/esm/channels/json-file-log.d.mts.map +1 -1
- package/esm/channels/json-file-log.mjs +25 -6
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.d.mts +1 -1
- package/esm/channels/sentry-log.d.mts.map +1 -1
- package/esm/channels/sentry-log.mjs +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/index.d.mts +8 -5
- package/esm/index.mjs +5 -3
- package/esm/log-channel.d.mts +24 -2
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +23 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +78 -18
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +163 -22
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/default-keys.d.mts +1 -1
- package/esm/redact/default-keys.d.mts.map +1 -1
- package/esm/redact/default-keys.mjs +1 -1
- package/esm/redact/default-keys.mjs.map +1 -1
- package/esm/redact/escape-log-value.d.mts +26 -0
- package/esm/redact/escape-log-value.d.mts.map +1 -0
- package/esm/redact/escape-log-value.mjs +37 -0
- package/esm/redact/escape-log-value.mjs.map +1 -0
- package/esm/redact/index.mjs +2 -0
- package/esm/redact/mask-email.d.mts +40 -0
- package/esm/redact/mask-email.d.mts.map +1 -0
- package/esm/redact/mask-email.mjs +70 -0
- package/esm/redact/mask-email.mjs.map +1 -0
- package/esm/redact/redact.d.mts +1 -1
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +10 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/esm/utils/clear-message.d.mts +1 -1
- package/esm/utils/clear-message.d.mts.map +1 -1
- package/esm/utils/clear-message.mjs +1 -1
- package/esm/utils/clear-message.mjs.map +1 -1
- package/esm/utils/safe-json-stringify.d.mts +1 -1
- package/esm/utils/safe-json-stringify.d.mts.map +1 -1
- package/esm/utils/safe-json-stringify.mjs +1 -1
- package/esm/utils/safe-json-stringify.mjs.map +1 -1
- package/llms-full.txt +247 -147
- package/llms.txt +2 -2
- package/package.json +4 -3
- package/skills/capture-unhandled-errors/SKILL.md +9 -10
- package/skills/configure-logger/SKILL.md +7 -10
- package/skills/filter-log-entries/SKILL.md +2 -2
- package/skills/flush-logs-on-shutdown/SKILL.md +34 -15
- package/skills/index.md +4 -1
- package/skills/logger-basics/SKILL.md +21 -24
- package/skills/overview/SKILL.md +17 -6
- package/skills/pick-log-channel/SKILL.md +36 -24
- package/skills/redact-sensitive-log-fields/SKILL.md +48 -18
- package/skills/ship-logs-to-sentry/SKILL.md +18 -18
- package/skills/test-logging-code/SKILL.md +4 -2
- package/skills/write-custom-log-channel/SKILL.md +51 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport dayjs from \"dayjs\";\nimport fs from \"fs\";\nimport { EOL } from \"os\";\nimport path from \"path\";\nimport { LogChannel } from \"../log-channel\";\nimport type {\n BasicLogConfigurations,\n LogContract,\n LoggingData,\n LogLevel,\n LogMessage,\n} from \"../types\";\n\n// TODO: Add max messages per file before rotation\n\nexport type FileLogConfig = BasicLogConfigurations & {\n storagePath?: string;\n /**\n * File name, without extension\n */\n name?: string;\n /**\n * chunk mode\n * If set to `single`, the logs will be created in a single file, unless the rotate is set to true\n * If set to `daily`, the logs will be created in a daily file, unless the rotate is set to true\n * If set to `hourly`, the logs will be created in an hourly file, unless the rotate is set to true\n * @default single\n */\n chunk?: \"single\" | \"daily\" | \"hourly\";\n /**\n * Whether to rotate the file\n *\n * @default true\n */\n rotate?: boolean;\n /**\n * File Extension\n *\n * @default log\n */\n extension?: string;\n /**\n * If rotate is set, the rotate name will be added to the file name suffixed with `-`\n *\n * @default DD-MM-YYYY\n */\n rotateFileName?: string;\n /**\n * Max file size before rotating the file\n *\n * @default 10MB\n */\n maxFileSize?: number;\n /**\n * Set the max messages that needs to be added before writing to the file\n *\n * @default 100\n */\n maxMessagesToWrite?: number;\n /**\n * Group logs by\n * Please note that the order matters here\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\n *\n * @default none\n */\n groupBy?: (\"level\" | \"module\" | \"action\")[];\n /**\n * Define what levels should be logged\n *\n * @default all\n */\n levels?: LogLevel[];\n /**\n * Date and time format\n */\n dateFormat?: {\n date?: string;\n time?: string;\n };\n};\n\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\n /**\n * {@inheritdoc}\n */\n public name = \"file\";\n\n /**\n * Messages buffer\n */\n protected messages: LogMessage[] = [];\n\n /**\n * Grouped messages\n */\n protected groupedMessages: Record<string, LogMessage[]> = {};\n\n /**\n * Default channel configurations\n */\n protected defaultConfigurations: FileLogConfig = {\n storagePath: process.cwd() + \"/storage/logs\",\n rotate: true,\n name: \"app\",\n extension: \"log\",\n chunk: \"single\",\n maxMessagesToWrite: 100,\n filter: () => true,\n maxFileSize: 10 * 1024 * 1024, // 10MB\n get rotateFileName() {\n return dayjs().format(\"DD-MM-YYYY\");\n },\n dateFormat: {\n date: \"DD-MM-YYYY\",\n time: \"HH:mm:ss\",\n },\n };\n\n /**\n * Last write time\n */\n protected lastWriteTime = Date.now();\n\n /**\n * A flag to determine if the file is being written\n */\n protected isWriting = false;\n\n /**\n * Handle for the periodic flush interval. Stored so it can be cleared\n * in `dispose()` — long-lived processes that create channels dynamically\n * would otherwise leak one timer per channel.\n */\n protected flushIntervalHandle?: NodeJS.Timeout;\n\n /**\n * Check file size for file rotation\n */\n protected async checkAndRotateFile(filePath = this.filePath) {\n if (!this.config(\"rotate\")) return;\n\n try {\n const stats = await fs.promises.stat(filePath);\n if (stats.size >= this.config(\"maxFileSize\")!) {\n await this.rotateLogFile();\n }\n } catch (error: any) {\n // ENOENT is expected when the file hasn't been created yet — there is\n // nothing to rotate, so stay silent. Surface anything else.\n if (error.code !== \"ENOENT\") {\n console.error(\"Error checking log file:\", error);\n }\n }\n }\n\n /**\n * Rotate log file\n */\n protected async rotateLogFile() {\n const fileName = `${this.fileName}-${this.config(\"rotateFileName\")}-${Date.now()}`;\n\n const extension = this.extension;\n\n const rotatedFilePath = path.join(this.storagePath, `${fileName}.${extension}`);\n\n await fs.promises.rename(this.filePath, rotatedFilePath).catch((error) => {\n console.error(\"Error rotating file:\", error);\n });\n }\n\n /**\n * Flush messages\n *\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\n * entries indefinitely. The handle is stored on the instance so `dispose()`\n * can stop it — without this, every channel leaks a timer for the lifetime\n * of the process.\n */\n protected initMessageFlush() {\n this.flushIntervalHandle = setInterval(() => {\n if (\n this.messages.length > 0 &&\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\n ) {\n this.writeMessagesToFile();\n }\n }, 5000);\n }\n\n /**\n * Stop the background flush interval and drain any buffered entries.\n *\n * Call this when discarding a channel (e.g. reconfiguring the logger at\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\n * call more than once.\n */\n public dispose(): void {\n if (this.flushIntervalHandle) {\n clearInterval(this.flushIntervalHandle);\n this.flushIntervalHandle = undefined;\n }\n\n this.flushSync();\n }\n\n /**\n * Get file path\n */\n public get filePath() {\n const fileName = this.fileName;\n\n const extension = this.extension;\n\n return path.join(this.storagePath, `${fileName}.${extension}`);\n }\n\n /**\n * Get max messages\n */\n protected get maxMessagesToWrite(): number {\n return this.config(\"maxMessagesToWrite\")!;\n }\n\n /**\n * Get file name\n */\n public get fileName(): string {\n const debugLevel = this.config(\"chunk\")!;\n\n switch (debugLevel) {\n case \"single\":\n default:\n return this.config(\"name\")!;\n case \"daily\":\n return dayjs().format(\"DD-MM-YYYY\");\n case \"hourly\":\n return dayjs().format(\"DD-MM-YYYY-HH-00-00-a\");\n }\n }\n\n /**\n * Get file extension\n */\n public get extension(): string {\n return this.config(\"extension\")!;\n }\n\n /**\n * Get content\n */\n protected get content() {\n return this.messages.map((message) => message.content).join(EOL) + EOL;\n }\n\n /**\n * Get storage path\n */\n public get storagePath(): string {\n return this.config(\"storagePath\")!;\n }\n\n /**\n * {@inheritdoc}\n */\n protected async init() {\n const logsDirectory = this.storagePath;\n\n await ensureDirectoryAsync(logsDirectory);\n\n this.initMessageFlush();\n }\n\n /**\n * Synchronously flush messages\n */\n public flushSync(): void {\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\n\n if (this.messagedShouldBeGrouped) {\n this.prepareGroupedMessages();\n // `Object.entries` rather than `for...in` + index: under\n // `noUncheckedIndexedAccess` an index read is `LogMessage[] | undefined`\n // even when the key came from the object itself, and entries hands over\n // the value already narrowed. Same iteration, no assertion.\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n fs.mkdirSync(directoryPath, { recursive: true });\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\n const content = groupMessages.map((message) => message.content).join(EOL) + EOL;\n fs.appendFileSync(filePath, content);\n }\n } else {\n fs.mkdirSync(this.storagePath, { recursive: true });\n fs.appendFileSync(this.filePath, this.content);\n }\n\n this.onSave();\n }\n\n /**\n * Asynchronously drain buffered entries to disk.\n *\n * The async analogue of {@link flushSync}: it reuses the same async writer\n * as the background interval, so a caller on a graceful-shutdown path can\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\n * its overridden `writeMessagesToFile` performs the JSON merge.\n */\n public async flush(): Promise<void> {\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\n return;\n }\n\n // Clear the in-flight lock so a deliberate drain is never short-circuited\n // by a half-finished background write, then await the async writer to\n // completion — unlike the fire-and-forget interval, callers depend on the\n // buffer reaching disk before this resolves.\n this.isWriting = false;\n\n await this.writeMessagesToFile();\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(data: LoggingData) {\n const { module, action, message, type: level, context } = data;\n\n if (!this.shouldBeLogged(data)) return;\n\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\n\n const date = dayjs().format(dateFormat + \" \" + time);\n\n let content = `[${date}] [${level}] [${module}][${action}]: `;\n\n let stack: string | undefined;\n\n // check if message is an instance of Error\n if (message instanceof Error) {\n // in that case we need to store the error message and stack trace\n content += message.message + EOL;\n content += `[trace]` + EOL;\n content += message.stack;\n stack = message.stack;\n } else {\n content += message;\n }\n\n this.messages.push({\n content,\n level,\n date,\n module,\n action,\n stack,\n context,\n timestamp: new Date().toISOString(),\n });\n\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\n }\n\n /**\n * Check if messages should be written\n */\n protected async checkIfMessagesShouldBeWritten() {\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\n await this.writeMessagesToFile();\n }\n }\n\n /**\n * Should be called after messages are saved\n */\n protected onSave() {\n this.messages = [];\n this.groupedMessages = {};\n this.isWriting = false;\n this.lastWriteTime = Date.now();\n }\n\n /**\n * Check if messages should be grouped\n */\n protected get messagedShouldBeGrouped(): boolean {\n return Number(this.config(\"groupBy\")?.length) > 0;\n }\n\n /**\n * Write messages to the file\n */\n protected async writeMessagesToFile() {\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\n\n this.isWriting = true;\n\n if (this.messagedShouldBeGrouped) {\n return await this.writeGroupedMessagesToFile();\n }\n\n await this.checkAndRotateFile(); // Ensure we check file size before writing\n\n try {\n await this.write(this.filePath, this.content);\n this.onSave();\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n // Implement fallback logic here\n this.isWriting = false;\n }\n }\n\n /**\n * Write grouped messages to the file\n */\n protected async writeGroupedMessagesToFile(): Promise<void> {\n // first step, is to group the messages\n this.prepareGroupedMessages();\n\n // now each key in the grouped messages, represents the directory path that should extend the storage path\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n\n await ensureDirectoryAsync(directoryPath);\n\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\n\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\n\n const content = groupMessages.map((message) => message.content).join(EOL) + EOL;\n\n try {\n await this.write(filePath, content);\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n }\n }\n\n this.onSave();\n this.isWriting = false;\n }\n\n /**\n * Prepare grouped messages\n */\n protected prepareGroupedMessages(): void {\n this.messages.forEach((message) => {\n const key = this.config(\"groupBy\")!\n .map((groupKey) => encodeURIComponent(message[groupKey]))\n .join(\"/\");\n\n this.groupedMessages[key] = this.groupedMessages[key] || [];\n this.groupedMessages[key].push(message);\n });\n }\n\n /**\n * Start writing to the file\n */\n protected async write(filePath: string, content: string) {\n return new Promise((resolve, reject) => {\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\n\n writer.write(content, (error) => {\n writer.end();\n if (error) {\n reject(error);\n } else {\n resolve(true);\n }\n });\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-log.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/channels/file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport dayjs from \"dayjs\";\nimport fs from \"fs\";\nimport { EOL } from \"os\";\nimport path from \"path\";\nimport { LogChannel } from \"../log-channel\";\nimport type {\n BasicLogConfigurations,\n LogContract,\n LoggingData,\n LogLevel,\n LogMessage,\n} from \"../types\";\nimport {\n escapeRegExp,\n fileNamePatternSource,\n renderFileName,\n type FileNameOption,\n} from \"./file-name-pattern\";\n\nexport type { FileNameOption } from \"./file-name-pattern\";\n\n/** Default file name of a `daily` chunk: sortable ISO date. */\nexport const DAILY_FILE_NAME = \"{YYYY}-{MM}-{DD}\";\n\n/** Default file name of an `hourly` chunk: sortable ISO date and hour. */\nexport const HOURLY_FILE_NAME = \"{YYYY}-{MM}-{DD}-{HH}\";\n\n/** Default date part of a size-rotated file's name. */\nexport const ROTATE_FILE_NAME = \"{YYYY}-{MM}-{DD}\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Which old log files `FileLog` removes, on boot and whenever it starts a new\n * file (a size rotation or a dated name rolling over). Only files in the log\n * directory (each group directory with `groupBy`) whose names match the\n * channel's file name pattern are considered; the file being written is\n * always kept.\n */\nexport type FileLogRetention = {\n /**\n * Keep at most this many matching files per directory, newest (by\n * modification time) first, counting the file being written.\n */\n maxFiles?: number;\n /**\n * Remove matching files last modified more than this many days ago.\n */\n maxAgeDays?: number;\n /**\n * Which file names (with extension) belong to this channel. Defaults to one\n * derived from `fileName` and `rotateFileName`; required when `fileName` is\n * a function.\n */\n match?: RegExp;\n};\n\nexport type FileLogConfig = BasicLogConfigurations & {\n storagePath?: string;\n /**\n * Base name, without extension: the file name of a `single` chunk and the\n * `{name}` token of a `fileName` pattern.\n *\n * @default app\n */\n name?: string;\n /**\n * The file name, without extension: a pattern such as\n * `\"app-{YYYY}-{MM}-{DD}\"` (tokens `{YYYY}` `{MM}` `{DD}` `{HH}` `{mm}`\n * `{name}`, local time) or a function of the current date. Evaluated at\n * every write, so a dated name starts a new file when it changes.\n *\n * @default by chunk: `single` → name, `daily` → `{YYYY}-{MM}-{DD}`, `hourly` → `{YYYY}-{MM}-{DD}-{HH}`\n */\n fileName?: FileNameOption;\n /**\n * chunk mode: picks the default `fileName`.\n * `single` writes one file named `name`, `daily` one file per day\n * (`YYYY-MM-DD`), `hourly` one per hour (`YYYY-MM-DD-HH`). Size rotation\n * applies to each.\n * @default single\n */\n chunk?: \"single\" | \"daily\" | \"hourly\";\n /**\n * Whether to rotate the file\n *\n * @default true\n */\n rotate?: boolean;\n /**\n * File Extension\n *\n * @default log\n */\n extension?: string;\n /**\n * Middle part of a size-rotated file's name:\n * `<file name>-<rotateFileName>-<epoch ms>.<extension>`. Same forms as\n * `fileName` (a token pattern or a function of the rotation date); text\n * without tokens is used as is.\n *\n * @default {YYYY}-{MM}-{DD}\n */\n rotateFileName?: FileNameOption;\n /**\n * Max file size before rotating the file\n *\n * @default 10MB\n */\n maxFileSize?: number;\n /**\n * Remove old log files on boot and whenever a new file is started.\n *\n * @default none (nothing is removed)\n */\n retention?: FileLogRetention;\n /**\n * Set the max messages that needs to be added before writing to the file\n *\n * @default 100\n */\n maxMessagesToWrite?: number;\n /**\n * Group logs by\n * Please note that the order matters here\n * For example, if you set `groupBy: ['level', 'module']`, the logs will be added in level name first, then by module\n *\n * @default none\n */\n groupBy?: (\"level\" | \"module\" | \"action\")[];\n /**\n * Define what levels should be logged\n *\n * @default all\n */\n levels?: LogLevel[];\n /**\n * Date and time format\n */\n dateFormat?: {\n date?: string;\n time?: string;\n };\n};\n\nexport class FileLog extends LogChannel<FileLogConfig> implements LogContract {\n /**\n * {@inheritdoc}\n */\n public name = \"file\";\n\n /**\n * Messages buffer\n */\n protected messages: LogMessage[] = [];\n\n /**\n * Grouped messages\n */\n protected groupedMessages: Record<string, LogMessage[]> = {};\n\n /**\n * Default channel configurations\n */\n protected defaultConfigurations: FileLogConfig = {\n storagePath: process.cwd() + \"/storage/logs\",\n rotate: true,\n name: \"app\",\n extension: \"log\",\n chunk: \"single\",\n maxMessagesToWrite: 100,\n filter: () => true,\n maxFileSize: 10 * 1024 * 1024, // 10MB\n rotateFileName: ROTATE_FILE_NAME,\n dateFormat: {\n date: \"DD-MM-YYYY\",\n time: \"HH:mm:ss\",\n },\n };\n\n /**\n * Last write time\n */\n protected lastWriteTime = Date.now();\n\n /**\n * A flag to determine if the file is being written\n */\n protected isWriting = false;\n\n /**\n * Handle for the periodic flush interval. Stored so it can be cleared\n * in `dispose()` — long-lived processes that create channels dynamically\n * would otherwise leak one timer per channel.\n */\n protected flushIntervalHandle?: NodeJS.Timeout;\n\n /**\n * Set when a size rotation started a new file, so the next write prunes.\n */\n protected retentionDue = false;\n\n /**\n * File name of the last write, to notice a dated name rolling over.\n */\n protected lastFileName?: string;\n\n /**\n * The running (or last) prune, so prunes never overlap.\n */\n protected pruning?: Promise<void>;\n\n /**\n * Whether the \"function `fileName` needs `retention.match`\" error was\n * already reported.\n */\n protected missingMatchReported = false;\n\n /**\n * {@inheritdoc}\n */\n protected setConfigurations(configurations: FileLogConfig) {\n assertRetention(configurations.retention);\n\n return super.setConfigurations(configurations);\n }\n\n /**\n * Check file size for file rotation\n */\n protected async checkAndRotateFile(filePath = this.filePath) {\n if (!this.config(\"rotate\")) return;\n\n try {\n const stats = await fs.promises.stat(filePath);\n if (stats.size >= this.config(\"maxFileSize\")!) {\n await this.rotateLogFile(filePath);\n }\n } catch (error: any) {\n // ENOENT is expected when the file hasn't been created yet — there is\n // nothing to rotate, so stay silent. Surface anything else.\n if (error.code !== \"ENOENT\") {\n console.error(\"Error checking log file:\", error);\n }\n }\n }\n\n /**\n * Rotate the given log file (the active one by default) aside as\n * `<file name>-<rotateFileName>-<epoch ms>.<extension>` in its directory.\n */\n protected async rotateLogFile(filePath = this.filePath) {\n const now = new Date();\n const extension = this.extension;\n const baseName = path.basename(filePath, `.${extension}`);\n const rotateName = renderFileName(\n this.config(\"rotateFileName\") ?? ROTATE_FILE_NAME,\n now,\n this.baseName,\n );\n\n const rotatedFilePath = path.join(\n path.dirname(filePath),\n `${baseName}-${rotateName}-${now.getTime()}.${extension}`,\n );\n\n await fs.promises.rename(filePath, rotatedFilePath).then(\n () => {\n this.retentionDue = true;\n },\n (error) => {\n console.error(\"Error rotating file:\", error);\n },\n );\n }\n\n /**\n * Flush messages\n *\n * Starts a periodic re-check so low-traffic channels don't sit on buffered\n * entries indefinitely. The handle is stored on the instance so `dispose()`\n * can stop it — without this, every channel leaks a timer for the lifetime\n * of the process.\n */\n protected initMessageFlush() {\n this.flushIntervalHandle = setInterval(() => {\n if (\n this.messages.length > 0 &&\n (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000)\n ) {\n this.writeMessagesToFile();\n }\n }, 5000);\n }\n\n /**\n * Stop the background flush interval and drain any buffered entries.\n *\n * Call this when discarding a channel (e.g. reconfiguring the logger at\n * runtime) so the 5-second timer doesn't keep the event loop alive. Safe to\n * call more than once.\n */\n public dispose(): void {\n if (this.flushIntervalHandle) {\n clearInterval(this.flushIntervalHandle);\n this.flushIntervalHandle = undefined;\n }\n\n this.flushSync();\n }\n\n /**\n * Get file path\n */\n public get filePath() {\n const fileName = this.fileName;\n\n const extension = this.extension;\n\n return path.join(this.storagePath, `${fileName}.${extension}`);\n }\n\n /**\n * Get max messages\n */\n protected get maxMessagesToWrite(): number {\n return this.config(\"maxMessagesToWrite\")!;\n }\n\n /**\n * The configured `name` option.\n */\n protected get baseName(): string {\n return this.config(\"name\") ?? \"app\";\n }\n\n /**\n * The `fileName` option, or the chunk's default. `undefined` means the\n * plain `name` of a `single` chunk, used as is (braces included).\n */\n protected get fileNamePattern(): FileNameOption | undefined {\n const configured = this.config(\"fileName\");\n\n if (configured !== undefined) return configured;\n\n switch (this.config(\"chunk\")) {\n case \"daily\":\n return DAILY_FILE_NAME;\n case \"hourly\":\n return HOURLY_FILE_NAME;\n case \"single\":\n default:\n return undefined;\n }\n }\n\n /**\n * Current file name, without extension\n */\n public get fileName(): string {\n const pattern = this.fileNamePattern;\n\n return pattern === undefined\n ? this.baseName\n : renderFileName(pattern, new Date(), this.baseName);\n }\n\n /**\n * Get file extension\n */\n public get extension(): string {\n return this.config(\"extension\")!;\n }\n\n /**\n * Get content\n */\n protected get content() {\n return this.messages.map((message) => message.content).join(EOL) + EOL;\n }\n\n /**\n * Get storage path\n */\n public get storagePath(): string {\n return this.config(\"storagePath\")!;\n }\n\n /**\n * {@inheritdoc}\n */\n protected async init() {\n const logsDirectory = this.storagePath;\n\n await ensureDirectoryAsync(logsDirectory);\n\n this.initMessageFlush();\n\n await this.prune();\n }\n\n /**\n * Remove old log files as configured by `retention`. Runs on boot and\n * whenever a new file is started; call it yourself to prune on demand.\n * Never rejects: failures go to the channel error handler.\n */\n public prune(): Promise<void> {\n const run = (this.pruning ?? Promise.resolve()).then(() => this.pruneFiles());\n\n this.pruning = run;\n\n return run;\n }\n\n /**\n * Prune once a write started a new file: after a size rotation, or when a\n * dated file name rolled over. Never throws.\n */\n protected async afterWrite(): Promise<void> {\n let fileName: string;\n\n try {\n fileName = this.fileName;\n } catch {\n return;\n }\n\n const rolledOver = this.lastFileName !== undefined && this.lastFileName !== fileName;\n\n this.lastFileName = fileName;\n\n if (!rolledOver && !this.retentionDue) return;\n\n this.retentionDue = false;\n\n await this.prune();\n }\n\n /**\n * Apply `retention` to every directory this channel writes to.\n */\n protected async pruneFiles(): Promise<void> {\n const retention = this.config(\"retention\");\n\n if (!retention || (retention.maxFiles === undefined && retention.maxAgeDays === undefined)) {\n return;\n }\n\n try {\n const matcher = this.retentionMatcher(retention);\n\n if (!matcher) return;\n\n const activeFile = `${this.fileName}.${this.extension}`;\n\n for (const directory of await this.retentionDirectories()) {\n await this.pruneDirectory(directory, matcher, retention, activeFile);\n }\n } catch (error) {\n this.reportRetentionError(error, this.storagePath);\n }\n }\n\n /**\n * The expression a file name must match to be pruned, or `undefined` when\n * none can be derived (a function `fileName` without `retention.match`).\n */\n protected retentionMatcher(retention: FileLogRetention): RegExp | undefined {\n if (retention.match) {\n // Drop `g`/`y`: their `lastIndex` would make `test()` stateful.\n return new RegExp(retention.match.source, retention.match.flags.replace(/[gy]/g, \"\"));\n }\n\n const pattern = this.fileNamePattern;\n\n if (typeof pattern === \"function\") {\n if (!this.missingMatchReported) {\n this.missingMatchReported = true;\n this.reportRetentionError(\n new Error(\n \"FileLog retention needs `retention.match` when `fileName` is a function; no files were pruned\",\n ),\n this.storagePath,\n );\n }\n\n return undefined;\n }\n\n const name = this.baseName;\n const fileSource =\n pattern === undefined ? escapeRegExp(name) : fileNamePatternSource(pattern, name);\n const rotateName = this.config(\"rotateFileName\") ?? ROTATE_FILE_NAME;\n const rotateSource =\n typeof rotateName === \"function\" ? \".+\" : fileNamePatternSource(rotateName, name);\n\n return new RegExp(\n `^${fileSource}(?:-${rotateSource}-\\\\d{13,})?\\\\.${escapeRegExp(this.extension)}$`,\n );\n }\n\n /**\n * The log directory, or each group directory when `groupBy` is set.\n */\n protected async retentionDirectories(): Promise<string[]> {\n let directories = [this.storagePath];\n const depth = this.config(\"groupBy\")?.length ?? 0;\n\n for (let level = 0; level < depth; level++) {\n const next: string[] = [];\n\n for (const directory of directories) {\n const entries = await fs.promises\n .readdir(directory, { withFileTypes: true })\n .catch((error: NodeJS.ErrnoException) => {\n if (error.code !== \"ENOENT\") this.reportRetentionError(error, directory);\n return [];\n });\n\n for (const entry of entries) {\n if (entry.isDirectory()) next.push(path.join(directory, entry.name));\n }\n }\n\n directories = next;\n }\n\n return directories;\n }\n\n /**\n * Remove the matching files of one directory that `retention` no longer\n * keeps. Each failure is reported and the rest still run.\n */\n protected async pruneDirectory(\n directory: string,\n matcher: RegExp,\n retention: FileLogRetention,\n activeFile: string,\n ): Promise<void> {\n let names: string[];\n\n try {\n names = await fs.promises.readdir(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n this.reportRetentionError(error, directory);\n }\n return;\n }\n\n const candidates: Array<{ filePath: string; name: string; modifiedAt: number }> = [];\n\n for (const name of names) {\n if (name === activeFile || !matcher.test(name)) continue;\n\n const filePath = path.join(directory, name);\n\n try {\n const stats = await fs.promises.stat(filePath);\n\n if (stats.isFile()) candidates.push({ filePath, name, modifiedAt: stats.mtimeMs });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n this.reportRetentionError(error, filePath);\n }\n }\n }\n\n // Newest first; the name breaks ties so the order is stable.\n candidates.sort((a, b) => b.modifiedAt - a.modifiedAt || (a.name < b.name ? 1 : -1));\n\n // The file being written counts toward `maxFiles` and is never removed.\n const keep =\n retention.maxFiles === undefined\n ? Infinity\n : retention.maxFiles - (names.includes(activeFile) ? 1 : 0);\n const oldestKept =\n retention.maxAgeDays === undefined ? -Infinity : Date.now() - retention.maxAgeDays * DAY_MS;\n\n for (const [index, candidate] of candidates.entries()) {\n if (index < keep && candidate.modifiedAt >= oldestKept) continue;\n\n try {\n await fs.promises.unlink(candidate.filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n this.reportRetentionError(error, candidate.filePath);\n }\n }\n }\n }\n\n /**\n * Send a retention failure to the channel error handler.\n */\n protected reportRetentionError(error: unknown, target: string): void {\n this.reportError(error, {\n type: \"error\",\n module: \"logger\",\n action: \"retention\",\n message: `Log retention failed for ${target}`,\n });\n }\n\n /**\n * Synchronously flush messages\n */\n public flushSync(): void {\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\n\n if (this.messagedShouldBeGrouped) {\n this.prepareGroupedMessages();\n // `Object.entries` rather than `for...in` + index: under\n // `noUncheckedIndexedAccess` an index read is `LogMessage[] | undefined`\n // even when the key came from the object itself, and entries hands over\n // the value already narrowed. Same iteration, no assertion.\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n fs.mkdirSync(directoryPath, { recursive: true });\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\n const content = groupMessages.map((message) => message.content).join(EOL) + EOL;\n fs.appendFileSync(filePath, content);\n }\n } else {\n fs.mkdirSync(this.storagePath, { recursive: true });\n fs.appendFileSync(this.filePath, this.content);\n }\n\n this.onSave();\n }\n\n /**\n * Asynchronously drain buffered entries to disk.\n *\n * The async analogue of {@link flushSync}: it reuses the same async writer\n * as the background interval, so a caller on a graceful-shutdown path can\n * `await channel.flush()` (or `await log.flush()`) and rely on the buffer\n * being on disk once it resolves. `JSONFileLog` inherits this unchanged —\n * its overridden `writeMessagesToFile` performs the JSON merge.\n */\n public async flush(): Promise<void> {\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) {\n return;\n }\n\n // Clear the in-flight lock so a deliberate drain is never short-circuited\n // by a half-finished background write, then await the async writer to\n // completion — unlike the fire-and-forget interval, callers depend on the\n // buffer reaching disk before this resolves.\n this.isWriting = false;\n\n await this.writeMessagesToFile();\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(data: LoggingData) {\n const { module, action, message, type: level, context } = data;\n\n if (!this.shouldBeLogged(data)) return;\n\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\n\n const date = dayjs().format(dateFormat + \" \" + time);\n\n let content = `[${date}] [${level}] [${module}][${action}]: `;\n\n let stack: string | undefined;\n\n // check if message is an instance of Error\n if (message instanceof Error) {\n // in that case we need to store the error message and stack trace\n content += message.message + EOL;\n content += `[trace]` + EOL;\n content += message.stack;\n stack = message.stack;\n } else {\n content += message;\n }\n\n this.messages.push({\n content,\n level,\n date,\n module,\n action,\n stack,\n context,\n timestamp: new Date().toISOString(),\n });\n\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\n }\n\n /**\n * Check if messages should be written\n */\n protected async checkIfMessagesShouldBeWritten() {\n if (this.messages.length >= this.maxMessagesToWrite || Date.now() - this.lastWriteTime > 5000) {\n await this.writeMessagesToFile();\n }\n }\n\n /**\n * Should be called after messages are saved\n */\n protected onSave() {\n this.messages = [];\n this.groupedMessages = {};\n this.isWriting = false;\n this.lastWriteTime = Date.now();\n }\n\n /**\n * Check if messages should be grouped\n */\n protected get messagedShouldBeGrouped(): boolean {\n return Number(this.config(\"groupBy\")?.length) > 0;\n }\n\n /**\n * Write messages to the file\n */\n protected async writeMessagesToFile() {\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\n\n this.isWriting = true;\n\n if (this.messagedShouldBeGrouped) {\n return await this.writeGroupedMessagesToFile();\n }\n\n try {\n const filePath = this.filePath;\n\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\n await this.write(filePath, this.content);\n this.onSave();\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n // Implement fallback logic here\n this.isWriting = false;\n return;\n }\n\n await this.afterWrite();\n }\n\n /**\n * Write grouped messages to the file\n */\n protected async writeGroupedMessagesToFile(): Promise<void> {\n // first step, is to group the messages\n this.prepareGroupedMessages();\n\n // now each key in the grouped messages, represents the directory path that should extend the storage path\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n\n try {\n await ensureDirectoryAsync(directoryPath);\n\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\n\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\n\n const content = groupMessages.map((message) => message.content).join(EOL) + EOL;\n\n await this.write(filePath, content);\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n }\n }\n\n this.onSave();\n this.isWriting = false;\n\n await this.afterWrite();\n }\n\n /**\n * Prepare grouped messages\n */\n protected prepareGroupedMessages(): void {\n this.messages.forEach((message) => {\n const key = this.config(\"groupBy\")!\n .map((groupKey) => encodeURIComponent(message[groupKey]))\n .join(\"/\");\n\n this.groupedMessages[key] = this.groupedMessages[key] || [];\n this.groupedMessages[key].push(message);\n });\n }\n\n /**\n * Start writing to the file\n */\n protected async write(filePath: string, content: string) {\n return new Promise((resolve, reject) => {\n const writer = fs.createWriteStream(filePath, { flags: \"a\" });\n\n writer.write(content, (error) => {\n writer.end();\n if (error) {\n reject(error);\n } else {\n resolve(true);\n }\n });\n });\n }\n}\n\n/**\n * Reject a `retention` that could never keep or remove anything sensibly.\n */\nfunction assertRetention(retention: FileLogRetention | undefined): void {\n if (!retention) return;\n\n const { maxFiles, maxAgeDays, match } = retention;\n\n if (maxFiles !== undefined && (!Number.isInteger(maxFiles) || maxFiles < 1)) {\n throw new TypeError(\"FileLog retention.maxFiles must be a positive integer\");\n }\n\n if (maxAgeDays !== undefined && (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0)) {\n throw new TypeError(\"FileLog retention.maxAgeDays must be a positive number\");\n }\n\n if (match !== undefined && !(match instanceof RegExp)) {\n throw new TypeError(\"FileLog retention.match must be a RegExp\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/channels/file-name-pattern.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A file name (without extension): a pattern string, or a function of the
|
|
4
|
+
* current date.
|
|
5
|
+
*
|
|
6
|
+
* Pattern tokens, rendered in local time and zero-padded:
|
|
7
|
+
* `{YYYY}` year, `{MM}` month, `{DD}` day, `{HH}` hour (00-23), `{mm}` minute,
|
|
8
|
+
* and `{name}` the channel's `name` option. Any other text is literal.
|
|
9
|
+
*
|
|
10
|
+
* @example "app-{YYYY}-{MM}-{DD}" // → "app-2026-10-17"
|
|
11
|
+
* @example (date) => `audit-${date.getFullYear()}` // → "audit-2026"
|
|
12
|
+
*/
|
|
13
|
+
type FileNameOption = string | ((date: Date) => string);
|
|
14
|
+
//#endregion
|
|
15
|
+
export { FileNameOption };
|
|
16
|
+
//# sourceMappingURL=file-name-pattern.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-name-pattern.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/channels/file-name-pattern.ts"],"mappings":";;AAWA;;;;AAAkD;;;;;;KAAtC,cAAA,cAA4B,IAAU,EAAJ,IAAI"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/channels/file-name-pattern.ts
|
|
2
|
+
const TOKEN = /\{(YYYY|MM|DD|HH|mm|name)\}/g;
|
|
3
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
4
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
/**
|
|
6
|
+
* Render a file name option for the given date.
|
|
7
|
+
*/
|
|
8
|
+
function renderFileName(option, date, name) {
|
|
9
|
+
if (typeof option === "function") return option(date);
|
|
10
|
+
return option.replace(TOKEN, (_match, token) => {
|
|
11
|
+
switch (token) {
|
|
12
|
+
case "YYYY": return String(date.getFullYear());
|
|
13
|
+
case "MM": return pad(date.getMonth() + 1);
|
|
14
|
+
case "DD": return pad(date.getDate());
|
|
15
|
+
case "HH": return pad(date.getHours());
|
|
16
|
+
case "mm": return pad(date.getMinutes());
|
|
17
|
+
default: return name;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Regular-expression source matching every name a pattern string can render:
|
|
23
|
+
* literal text is escaped, date tokens become digit runs and `{name}` the
|
|
24
|
+
* escaped channel name. The source is not anchored.
|
|
25
|
+
*/
|
|
26
|
+
function fileNamePatternSource(pattern, name) {
|
|
27
|
+
let source = "";
|
|
28
|
+
let lastIndex = 0;
|
|
29
|
+
for (const match of pattern.matchAll(TOKEN)) {
|
|
30
|
+
const index = match.index ?? 0;
|
|
31
|
+
const token = match[1];
|
|
32
|
+
source += escapeRegExp(pattern.slice(lastIndex, index));
|
|
33
|
+
source += token === "YYYY" ? "\\d{4}" : token === "name" ? escapeRegExp(name) : "\\d{2}";
|
|
34
|
+
lastIndex = index + match[0].length;
|
|
35
|
+
}
|
|
36
|
+
return source + escapeRegExp(pattern.slice(lastIndex));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { escapeRegExp, fileNamePatternSource, renderFileName };
|
|
41
|
+
//# sourceMappingURL=file-name-pattern.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-name-pattern.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/channels/file-name-pattern.ts"],"sourcesContent":["/**\n * A file name (without extension): a pattern string, or a function of the\n * current date.\n *\n * Pattern tokens, rendered in local time and zero-padded:\n * `{YYYY}` year, `{MM}` month, `{DD}` day, `{HH}` hour (00-23), `{mm}` minute,\n * and `{name}` the channel's `name` option. Any other text is literal.\n *\n * @example \"app-{YYYY}-{MM}-{DD}\" // → \"app-2026-10-17\"\n * @example (date) => `audit-${date.getFullYear()}` // → \"audit-2026\"\n */\nexport type FileNameOption = string | ((date: Date) => string);\n\nconst TOKEN = /\\{(YYYY|MM|DD|HH|mm|name)\\}/g;\n\nconst pad = (value: number) => String(value).padStart(2, \"0\");\n\nexport const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Render a file name option for the given date.\n */\nexport function renderFileName(option: FileNameOption, date: Date, name: string): string {\n if (typeof option === \"function\") return option(date);\n\n return option.replace(TOKEN, (_match, token: string) => {\n switch (token) {\n case \"YYYY\":\n return String(date.getFullYear());\n case \"MM\":\n return pad(date.getMonth() + 1);\n case \"DD\":\n return pad(date.getDate());\n case \"HH\":\n return pad(date.getHours());\n case \"mm\":\n return pad(date.getMinutes());\n default:\n return name;\n }\n });\n}\n\n/**\n * Regular-expression source matching every name a pattern string can render:\n * literal text is escaped, date tokens become digit runs and `{name}` the\n * escaped channel name. The source is not anchored.\n */\nexport function fileNamePatternSource(pattern: string, name: string): string {\n let source = \"\";\n let lastIndex = 0;\n\n for (const match of pattern.matchAll(TOKEN)) {\n const index = match.index ?? 0;\n const token = match[1];\n\n source += escapeRegExp(pattern.slice(lastIndex, index));\n source += token === \"YYYY\" ? \"\\\\d{4}\" : token === \"name\" ? escapeRegExp(name) : \"\\\\d{2}\";\n lastIndex = index + match[0].length;\n }\n\n return source + escapeRegExp(pattern.slice(lastIndex));\n}\n"],"mappings":";AAaA,MAAM,QAAQ;AAEd,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;AAE5D,MAAa,gBAAgB,SAAiB,KAAK,QAAQ,uBAAuB,MAAM;;;;AAKxF,SAAgB,eAAe,QAAwB,MAAY,MAAsB;CACvF,IAAI,OAAO,WAAW,YAAY,OAAO,OAAO,IAAI;CAEpD,OAAO,OAAO,QAAQ,QAAQ,QAAQ,UAAkB;EACtD,QAAQ,OAAR;GACE,KAAK,QACH,OAAO,OAAO,KAAK,YAAY,CAAC;GAClC,KAAK,MACH,OAAO,IAAI,KAAK,SAAS,IAAI,CAAC;GAChC,KAAK,MACH,OAAO,IAAI,KAAK,QAAQ,CAAC;GAC3B,KAAK,MACH,OAAO,IAAI,KAAK,SAAS,CAAC;GAC5B,KAAK,MACH,OAAO,IAAI,KAAK,WAAW,CAAC;GAC9B,SACE,OAAO;EACX;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,sBAAsB,SAAiB,MAAsB;CAC3E,IAAI,SAAS;CACb,IAAI,YAAY;CAEhB,KAAK,MAAM,SAAS,QAAQ,SAAS,KAAK,GAAG;EAC3C,MAAM,QAAQ,MAAM,SAAS;EAC7B,MAAM,QAAQ,MAAM;EAEpB,UAAU,aAAa,QAAQ,MAAM,WAAW,KAAK,CAAC;EACtD,UAAU,UAAU,SAAS,WAAW,UAAU,SAAS,aAAa,IAAI,IAAI;EAChF,YAAY,QAAQ,MAAM,EAAE,CAAC;CAC/B;CAEA,OAAO,SAAS,aAAa,QAAQ,MAAM,SAAS,CAAC;AACvD"}
|
package/esm/channels/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConsoleLog } from "./console-log.mjs";
|
|
2
|
-
import { FileLog } from "./file-log.mjs";
|
|
2
|
+
import { DAILY_FILE_NAME, FileLog, HOURLY_FILE_NAME, ROTATE_FILE_NAME } from "./file-log.mjs";
|
|
3
3
|
import { JSONFileLog } from "./json-file-log.mjs";
|
|
4
4
|
import { SentryLog } from "./sentry-log.mjs";
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LogContract, LoggingData } from "../types.mjs";
|
|
2
2
|
import { FileLog } from "./file-log.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/logger/src/channels/json-file-log.d.ts
|
|
5
5
|
declare class JSONFileLog extends FileLog implements LogContract {
|
|
6
6
|
/**
|
|
7
7
|
* {@inheritdoc}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file-log.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"json-file-log.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/channels/json-file-log.ts"],"mappings":";;;;cAQa,WAAA,SAAoB,OAAA,YAAmB,WAAA;;AAApD;;EAIS,IAAA;EA4DgB;;;EAAA,IAvDZ,SAAA;EAToB;;;EAgBxB,SAAA;EAhBwB;;;EAgElB,GAAA,CAAI,IAAA,EAAM,WAAA,GAAW,OAAA;EAhD3B;;;EAAA,UAiFS,mBAAA,IAAuB,OAAA;EAjCL;;;EAAA,UAsFlB,0BAAA,IAA8B,OAAA;AAAA"}
|
|
@@ -5,7 +5,7 @@ import dayjs from "dayjs";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
|
|
8
|
-
//#region
|
|
8
|
+
//#region ../../packages/logger/src/channels/json-file-log.ts
|
|
9
9
|
var JSONFileLog = class extends FileLog {
|
|
10
10
|
constructor(..._args) {
|
|
11
11
|
super(..._args);
|
|
@@ -84,10 +84,18 @@ var JSONFileLog = class extends FileLog {
|
|
|
84
84
|
if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;
|
|
85
85
|
this.isWriting = true;
|
|
86
86
|
if (this.messagedShouldBeGrouped) return await this.writeGroupedMessagesToFile();
|
|
87
|
-
|
|
87
|
+
let filePath;
|
|
88
|
+
try {
|
|
89
|
+
filePath = this.filePath;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("Failed to write log:", error);
|
|
92
|
+
this.isWriting = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await this.checkAndRotateFile(filePath);
|
|
88
96
|
let fileContents = { messages: [] };
|
|
89
|
-
if (await fileExistsAsync(
|
|
90
|
-
fileContents = await getJsonFileAsync(
|
|
97
|
+
if (await fileExistsAsync(filePath)) try {
|
|
98
|
+
fileContents = await getJsonFileAsync(filePath);
|
|
91
99
|
} catch (error) {
|
|
92
100
|
console.error("Error reading log file, reinitializing:", error);
|
|
93
101
|
fileContents = { messages: [] };
|
|
@@ -95,22 +103,32 @@ var JSONFileLog = class extends FileLog {
|
|
|
95
103
|
else fileContents = { messages: [] };
|
|
96
104
|
fileContents.messages.push(...this.messages);
|
|
97
105
|
try {
|
|
98
|
-
await fs.promises.writeFile(
|
|
106
|
+
await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));
|
|
99
107
|
this.onSave();
|
|
100
108
|
} catch (error) {
|
|
101
109
|
console.error("Failed to write log:", error);
|
|
102
110
|
this.isWriting = false;
|
|
111
|
+
return;
|
|
103
112
|
}
|
|
113
|
+
await this.afterWrite();
|
|
104
114
|
}
|
|
105
115
|
/**
|
|
106
116
|
* Write grouped messages to the file
|
|
107
117
|
*/
|
|
108
118
|
async writeGroupedMessagesToFile() {
|
|
119
|
+
let fileName;
|
|
120
|
+
try {
|
|
121
|
+
fileName = this.fileName;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error("Failed to write log:", error);
|
|
124
|
+
this.isWriting = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
109
127
|
this.prepareGroupedMessages();
|
|
110
128
|
for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {
|
|
111
129
|
const directoryPath = path.join(this.storagePath, key);
|
|
112
130
|
await ensureDirectoryAsync(directoryPath);
|
|
113
|
-
const filePath = path.join(directoryPath, `${
|
|
131
|
+
const filePath = path.join(directoryPath, `${fileName}.${this.extension}`);
|
|
114
132
|
await this.checkAndRotateFile(filePath);
|
|
115
133
|
let fileContents = { messages: [] };
|
|
116
134
|
if (await fileExistsAsync(filePath)) try {
|
|
@@ -129,6 +147,7 @@ var JSONFileLog = class extends FileLog {
|
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
this.onSave();
|
|
150
|
+
await this.afterWrite();
|
|
132
151
|
}
|
|
133
152
|
};
|
|
134
153
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file-log.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"json-file-log.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/channels/json-file-log.ts"],"sourcesContent":["import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\nimport dayjs from \"dayjs\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { LogContract, LogMessage, LoggingData } from \"../types\";\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\nimport { FileLog } from \"./file-log\";\n\nexport class JSONFileLog extends FileLog implements LogContract {\n /**\n * {@inheritdoc}\n */\n public name = \"fileJson\";\n\n /**\n * Get file extension\n */\n public get extension(): string {\n return \"json\";\n }\n\n /**\n * Synchronously flush messages\n */\n public flushSync(): void {\n if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;\n\n if (this.messagedShouldBeGrouped) {\n this.prepareGroupedMessages();\n // `Object.entries` rather than `for...in` + index: under\n // `noUncheckedIndexedAccess` the index read is `LogMessage[] |\n // undefined`, and spreading that into `push` is a type error rather than\n // a runtime one only because the key demonstrably exists. Entries says so\n // to the compiler instead of asserting it.\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n fs.mkdirSync(directoryPath, { recursive: true });\n const filePath = path.join(directoryPath, `${this.fileName}.${this.extension}`);\n\n let fileContents = { messages: [] as any[] };\n if (fs.existsSync(filePath)) {\n try {\n fileContents = JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\n } catch (e) {\n fileContents = { messages: [] };\n }\n }\n fileContents.messages.push(...groupMessages);\n fs.writeFileSync(filePath, safeJsonStringify(fileContents, 2));\n }\n } else {\n fs.mkdirSync(this.storagePath, { recursive: true });\n let fileContents = { messages: [] as any[] };\n if (fs.existsSync(this.filePath)) {\n try {\n fileContents = JSON.parse(fs.readFileSync(this.filePath, \"utf-8\"));\n if (!Array.isArray(fileContents.messages)) fileContents.messages = [];\n } catch (e) {\n fileContents = { messages: [] };\n }\n }\n fileContents.messages.push(...this.messages);\n fs.writeFileSync(this.filePath, safeJsonStringify(fileContents, 2));\n }\n\n this.onSave();\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(data: LoggingData) {\n let stack: string[] | undefined;\n\n if (data.message instanceof Error) {\n stack = data.message.stack?.split(\"\\n\");\n data.message = data.message.message;\n }\n\n const { module, action, message, type: level, context } = data;\n\n if (!this.shouldBeLogged(data)) return;\n\n const { date: dateFormat, time } = this.getDateAndTimeFormat();\n\n const date = dayjs().format(dateFormat + \" \" + time);\n\n this.messages.push({\n content: message,\n level,\n date,\n module,\n action,\n stack,\n context,\n timestamp: new Date().toISOString(),\n } as LogMessage);\n\n await this.checkIfMessagesShouldBeWritten(); // Immediate check on buffer size\n }\n\n /**\n * Write messages to the file\n */\n protected async writeMessagesToFile(): Promise<void> {\n if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;\n\n this.isWriting = true;\n\n if (this.messagedShouldBeGrouped) {\n return await this.writeGroupedMessagesToFile();\n }\n\n let filePath: string;\n\n try {\n filePath = this.filePath;\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n this.isWriting = false;\n return;\n }\n\n await this.checkAndRotateFile(filePath); // Ensure file rotation is handled\n\n let fileContents = { messages: [] as any[] };\n\n if (await fileExistsAsync(filePath)) {\n try {\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\n } catch (error) {\n console.error(\"Error reading log file, reinitializing:\", error);\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\n }\n } else {\n fileContents = { messages: [] }; // Reinitialize the file if corrupted\n }\n\n fileContents.messages.push(...this.messages);\n\n try {\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\n\n this.onSave();\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n // Implement fallback logic here\n this.isWriting = false;\n return;\n }\n\n await this.afterWrite();\n }\n\n /**\n * Write grouped messages to the file\n */\n protected async writeGroupedMessagesToFile(): Promise<void> {\n let fileName: string;\n\n try {\n fileName = this.fileName;\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n this.isWriting = false;\n return;\n }\n\n // first step, is to group the messages\n this.prepareGroupedMessages();\n\n // now each key in the grouped messages, represents the directory path that should extend the storage path\n for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {\n const directoryPath = path.join(this.storagePath, key);\n\n await ensureDirectoryAsync(directoryPath);\n\n const filePath = path.join(directoryPath, `${fileName}.${this.extension}`);\n\n await this.checkAndRotateFile(filePath); // Ensure we check file size before writing\n\n let fileContents: { messages: any[] } = { messages: [] };\n if (await fileExistsAsync(filePath)) {\n try {\n fileContents = (await getJsonFileAsync(filePath)) as { messages: any[] };\n } catch (error) {\n console.error(\"Error reading log file, reinitializing:\", error);\n fileContents = { messages: [] };\n }\n } else {\n fileContents = { messages: [] };\n }\n\n fileContents.messages.push(...groupMessages);\n\n try {\n await fs.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));\n } catch (error) {\n console.error(\"Failed to write log:\", error);\n this.isWriting = false;\n }\n }\n\n this.onSave();\n\n await this.afterWrite();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BasicLogConfigurations, LogLevel, LoggingData } from "../types.mjs";
|
|
2
2
|
import { LogChannel } from "../log-channel.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/logger/src/channels/sentry-log.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Sentry severity levels — the `@sentry/node` `SeverityLevel` union.
|
|
7
7
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-log.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sentry-log.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/channels/sentry-log.ts"],"mappings":";;;;;;AAC8E;KAgBzE,mBAAA;;;AAAmB;UAKP,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,IAAQ,OAAA;;;;EA2BX,GAAA,CAAI,IAAA,EAAM,WAAA,GAAc,OAAA;;;;;EAiDxB,KAAA,IAAS,OAAA;;;;UAWd,YAAA;;;;;;;;UAWA,YAAA;;;;;UAiCA,aAAA;;;;;UAeA,MAAA;;;;;;UAiBA,gBAAA;AAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LogChannel } from "../log-channel.mjs";
|
|
2
2
|
import { safeJsonStringify } from "../utils/safe-json-stringify.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/logger/src/channels/sentry-log.ts
|
|
5
5
|
let Sentry;
|
|
6
6
|
let isModuleExists = null;
|
|
7
7
|
let loadingPromise;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-log.mjs","names":[],"sources":["../../../../../../../logger/src/channels/sentry-log.ts"],"sourcesContent":["import { LogChannel } from \"../log-channel\";\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\n\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\n// @sentry/node is referenced ONLY through the local minimal types below — never\n// via `typeof import(\"@sentry/node\")`. This package is served as source\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\n// program; a static type reference to the SDK would force module resolution and\n// break every consumer who (correctly) never installs the optional peer with a\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\n// the same reason, and its result is cast to these shapes. The shapes mirror the\n// stable `@sentry/node` public API (unchanged across v8–v10).\n\n/**\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\n */\ntype SentrySeverityLevel =\n | \"fatal\"\n | \"error\"\n | \"warning\"\n | \"log\"\n | \"info\"\n | \"debug\";\n\n/**\n * The Sentry `Scope` surface used while building a single event.\n */\nexport interface SentryScopeLike {\n setLevel(level: SentrySeverityLevel): void;\n setTags(tags: Record<string, string>): void;\n setContext(name: string, context: Record<string, unknown> | null): void;\n}\n\n/**\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\n */\nexport type SentryBreadcrumb = {\n category?: string;\n message?: string;\n level?: SentrySeverityLevel;\n data?: Record<string, unknown>;\n};\n\n/**\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\n * namespace satisfies this shape, so an app can pass it straight through as\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\n */\nexport interface SentryForwarder {\n captureException(exception: unknown): string;\n captureMessage(message: string, level?: SentrySeverityLevel): string;\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\n withScope(callback: (scope: SentryScopeLike) => void): void;\n flush(timeout?: number): Promise<boolean>;\n}\n\n/**\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\n * fields; the index signature keeps any other SDK option valid without coupling\n * to the SDK's types.\n */\nexport type SentryInitOptions = {\n dsn?: string;\n environment?: string;\n release?: string;\n sampleRate?: number;\n [key: string]: unknown;\n};\n\n/**\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\n * lifecycle calls the channel uses when it owns initialization.\n */\ninterface SentryNamespace extends SentryForwarder {\n init(options?: SentryInitOptions): unknown;\n getClient(): unknown;\n}\n\nlet Sentry: SentryNamespace | undefined;\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst SENTRY_INSTALL_INSTRUCTIONS = `\nThe Sentry log channel requires the @sentry/node package.\nInstall it with:\n\n npm install @sentry/node\n\nOr with your preferred package manager:\n\n pnpm add @sentry/node\n yarn add @sentry/node\n`.trim();\n\n/**\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\n * import failure to \"not installed\" — the curated install message surfaces at\n * `log()` time, never as a boot-time module-resolution crash.\n */\nfunction loadSentry(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\n // statically resolves the optional peer — see the file header.\n const sentryModule: string = \"@sentry/node\";\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\nexport type SentryLogConfig = BasicLogConfigurations & {\n /**\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\n * channel forwards through it and never imports or re-initializes the SDK.\n */\n client?: SentryForwarder;\n /**\n * Initialize Sentry from these options instead of reusing a host client. The\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\n * guarded so it never clobbers an existing client.\n */\n options?: SentryInitOptions;\n /**\n * Levels delivered as Sentry *events* (these consume the error quota). Every\n * other level is recorded as a breadcrumb that rides along with the next\n * event, costing no quota.\n *\n * @default [\"fatal\", \"error\", \"warn\"]\n */\n eventLevels?: LogLevel[];\n /**\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\n *\n * @default 2000\n */\n flushTimeout?: number;\n};\n\n/**\n * Forwards log entries to Sentry.\n *\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\n * **events**: an `Error` message via `captureException` (preserving the real\n * stack), any other message via `captureMessage`. Every other level becomes a\n * **breadcrumb** — buffered and attached to the next event, consuming no error\n * quota. `module` / `action` are attached as searchable tags and the entry's\n * `context` as a structured Sentry context.\n *\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\n * graceful shutdown, `await log.flush()` drains pending events via\n * `Sentry.flush(timeout)`.\n *\n * @example\n * // Existing app — reuse the initialized Sentry client\n * import * as Sentry from \"@sentry/node\";\n * log.addChannel(new SentryLog({ client: Sentry }));\n *\n * @example\n * // New app — let the channel initialize Sentry\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\n */\nexport class SentryLog extends LogChannel<SentryLogConfig> {\n /**\n * {@inheritdoc}\n */\n public name = \"sentry\";\n\n /**\n * {@inheritdoc}\n */\n public description =\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\n\n /**\n * {@inheritdoc}\n */\n protected defaultConfigurations: SentryLogConfig = {\n eventLevels: [\"fatal\", \"error\", \"warn\"],\n flushTimeout: 2_000,\n };\n\n /**\n * The resolved forwarder — the injected `client` or the lazily-imported\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\n * SDK is absent (then `log()` surfaces the install message once).\n */\n private sentry?: SentryForwarder;\n\n /**\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\n * doesn't spam stderr on every entry.\n */\n private warnedMissing = false;\n\n /**\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\n * entry logged on the same tick as construction — e.g. at app boot, before\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\n * silently dropped. The `options` (lazy-import) path is inherently async and\n * still resolves in `init()`.\n */\n public constructor(configurations?: SentryLogConfig) {\n super(configurations);\n\n const injected = this.config(\"client\");\n\n if (injected) {\n this.sentry = injected;\n }\n }\n\n /**\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\n * `@sentry/node` and (only when explicit `options` are supplied and no client\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\n */\n protected async init(): Promise<void> {\n const injected = this.config(\"client\");\n\n if (injected) {\n this.sentry = injected;\n\n return;\n }\n\n await loadSentry();\n\n if (!Sentry) {\n return;\n }\n\n const options = this.config(\"options\");\n\n if (options && !Sentry.getClient()) {\n Sentry.init(options);\n }\n\n this.sentry = Sentry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(data: LoggingData): Promise<void> {\n if (!this.shouldBeLogged(data)) {\n return;\n }\n\n // Only wait for the optional SDK when we do not already have a forwarder.\n // An INJECTED client is resolved synchronously in the constructor, and that is\n // the whole point of doing it there: an entry logged on the same tick as\n // construction must still send. Awaiting loadSentry() unconditionally undid\n // that — with @sentry/node absent (it is an optional peer) the import can take\n // seconds, so a log with a perfectly good injected client stalled behind an SDK\n // it was never going to use.\n if (!this.sentry && isModuleExists === null) {\n // wait until module is fully loaded\n await loadSentry();\n }\n\n if (!this.sentry) {\n this.reportMissingSdk();\n\n return;\n }\n\n const { module, action, message, type: level, context } = data;\n\n if (this.isEventLevel(level)) {\n this.captureEvent(this.sentry, {\n module,\n action,\n message,\n level,\n context,\n });\n\n return;\n }\n\n this.sentry.addBreadcrumb({\n category: module,\n message: this.toText(message),\n level: this.toSentryLevel(level),\n data: context,\n });\n }\n\n /**\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\n */\n public async flush(): Promise<void> {\n if (!this.sentry) {\n return;\n }\n\n await this.sentry.flush(this.config(\"flushTimeout\"));\n }\n\n /**\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\n */\n private isEventLevel(level: LogLevel): boolean {\n return Boolean(this.config(\"eventLevels\")?.includes(level));\n }\n\n /**\n * Send an entry as a Sentry event. An `Error` message goes through\n * `captureException` so Sentry parses the real stack and groups properly;\n * any other message goes through `captureMessage`. `module` / `action` are\n * attached as tags and `context` as a structured context, scoped to this\n * event only via `withScope`.\n */\n private captureEvent(\n sentry: SentryForwarder,\n entry: {\n module: string;\n action: string;\n message: unknown;\n level: LogLevel;\n context?: Record<string, any>;\n },\n ): void {\n const { module, action, message, level, context } = entry;\n const sentryLevel = this.toSentryLevel(level);\n\n sentry.withScope((scope) => {\n scope.setLevel(sentryLevel);\n scope.setTags({ module, action });\n\n if (context) {\n scope.setContext(\"context\", context);\n }\n\n if (message instanceof Error) {\n sentry.captureException(message);\n } else {\n sentry.captureMessage(this.toText(message), sentryLevel);\n }\n });\n }\n\n /**\n * Map a logger level to a Sentry severity. `success` has no Sentry\n * equivalent, so it is reported as informational.\n */\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\n switch (level) {\n case \"warn\":\n return \"warning\";\n case \"success\":\n return \"info\";\n default:\n return level; // debug | info | error | fatal map 1:1\n }\n }\n\n /**\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\n * `.message`, a string as-is, anything else safely JSON-serialized.\n */\n private toText(message: unknown): string {\n if (typeof message === \"string\") {\n return message;\n }\n\n if (message instanceof Error) {\n return message.message;\n }\n\n return safeJsonStringify(message);\n }\n\n /**\n * Surface the install instructions exactly once when the SDK is absent. The\n * logger can't log through itself here, so this writes to stderr — matching\n * how the file channels report write failures.\n */\n private reportMissingSdk(): void {\n if (isModuleExists === false && !this.warnedMissing) {\n this.warnedMissing = true;\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sentry-log.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/channels/sentry-log.ts"],"sourcesContent":["import { LogChannel } from \"../log-channel\";\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\n\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\n// @sentry/node is referenced ONLY through the local minimal types below — never\n// via `typeof import(\"@sentry/node\")`. This package is served as source\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\n// program; a static type reference to the SDK would force module resolution and\n// break every consumer who (correctly) never installs the optional peer with a\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\n// the same reason, and its result is cast to these shapes. The shapes mirror the\n// stable `@sentry/node` public API (unchanged across v8–v10).\n\n/**\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\n */\ntype SentrySeverityLevel = \"fatal\" | \"error\" | \"warning\" | \"log\" | \"info\" | \"debug\";\n\n/**\n * The Sentry `Scope` surface used while building a single event.\n */\nexport interface SentryScopeLike {\n setLevel(level: SentrySeverityLevel): void;\n setTags(tags: Record<string, string>): void;\n setContext(name: string, context: Record<string, unknown> | null): void;\n}\n\n/**\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\n */\nexport type SentryBreadcrumb = {\n category?: string;\n message?: string;\n level?: SentrySeverityLevel;\n data?: Record<string, unknown>;\n};\n\n/**\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\n * namespace satisfies this shape, so an app can pass it straight through as\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\n */\nexport interface SentryForwarder {\n captureException(exception: unknown): string;\n captureMessage(message: string, level?: SentrySeverityLevel): string;\n addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\n withScope(callback: (scope: SentryScopeLike) => void): void;\n flush(timeout?: number): Promise<boolean>;\n}\n\n/**\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\n * fields; the index signature keeps any other SDK option valid without coupling\n * to the SDK's types.\n */\nexport type SentryInitOptions = {\n dsn?: string;\n environment?: string;\n release?: string;\n sampleRate?: number;\n [key: string]: unknown;\n};\n\n/**\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\n * lifecycle calls the channel uses when it owns initialization.\n */\ninterface SentryNamespace extends SentryForwarder {\n init(options?: SentryInitOptions): unknown;\n getClient(): unknown;\n}\n\nlet Sentry: SentryNamespace | undefined;\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst SENTRY_INSTALL_INSTRUCTIONS = `\nThe Sentry log channel requires the @sentry/node package.\nInstall it with:\n\n npm install @sentry/node\n\nOr with your preferred package manager:\n\n pnpm add @sentry/node\n yarn add @sentry/node\n`.trim();\n\n/**\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\n * import failure to \"not installed\" — the curated install message surfaces at\n * `log()` time, never as a boot-time module-resolution crash.\n */\nfunction loadSentry(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n // Indirect specifier (typed `string`, not a literal) so TypeScript never\n // statically resolves the optional peer — see the file header.\n const sentryModule: string = \"@sentry/node\";\n Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\nexport type SentryLogConfig = BasicLogConfigurations & {\n /**\n * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\n * namespace from an app that already calls `Sentry.init(...)`. When set, the\n * channel forwards through it and never imports or re-initializes the SDK.\n */\n client?: SentryForwarder;\n /**\n * Initialize Sentry from these options instead of reusing a host client. The\n * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\n * guarded so it never clobbers an existing client.\n */\n options?: SentryInitOptions;\n /**\n * Levels delivered as Sentry *events* (these consume the error quota). Every\n * other level is recorded as a breadcrumb that rides along with the next\n * event, costing no quota.\n *\n * @default [\"fatal\", \"error\", \"warn\"]\n */\n eventLevels?: LogLevel[];\n /**\n * Milliseconds `flush()` waits for the transport to drain on shutdown.\n *\n * @default 2000\n */\n flushTimeout?: number;\n};\n\n/**\n * Forwards log entries to Sentry.\n *\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\n * **events**: an `Error` message via `captureException` (preserving the real\n * stack), any other message via `captureMessage`. Every other level becomes a\n * **breadcrumb** — buffered and attached to the next event, consuming no error\n * quota. `module` / `action` are attached as searchable tags and the entry's\n * `context` as a structured Sentry context.\n *\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\n * graceful shutdown, `await log.flush()` drains pending events via\n * `Sentry.flush(timeout)`.\n *\n * @example\n * // Existing app — reuse the initialized Sentry client\n * import * as Sentry from \"@sentry/node\";\n * log.addChannel(new SentryLog({ client: Sentry }));\n *\n * @example\n * // New app — let the channel initialize Sentry\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\n */\nexport class SentryLog extends LogChannel<SentryLogConfig> {\n /**\n * {@inheritdoc}\n */\n public name = \"sentry\";\n\n /**\n * {@inheritdoc}\n */\n public description =\n \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\n\n /**\n * {@inheritdoc}\n */\n protected defaultConfigurations: SentryLogConfig = {\n eventLevels: [\"fatal\", \"error\", \"warn\"],\n flushTimeout: 2_000,\n };\n\n /**\n * The resolved forwarder — the injected `client` or the lazily-imported\n * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\n * SDK is absent (then `log()` surfaces the install message once).\n */\n private sentry?: SentryForwarder;\n\n /**\n * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\n * doesn't spam stderr on every entry.\n */\n private warnedMissing = false;\n\n /**\n * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\n * entry logged on the same tick as construction — e.g. at app boot, before\n * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\n * silently dropped. The `options` (lazy-import) path is inherently async and\n * still resolves in `init()`.\n */\n public constructor(configurations?: SentryLogConfig) {\n super(configurations);\n\n const injected = this.config(\"client\");\n\n if (injected) {\n this.sentry = injected;\n }\n }\n\n /**\n * Resolve the forwarder: reuse the injected client, otherwise lazily import\n * `@sentry/node` and (only when explicit `options` are supplied and no client\n * exists yet) initialize it. Never throws — the base runs `init()` inside an\n * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\n * `isInitialized` would never flip; a missing SDK is reported from `log()`.\n */\n protected async init(): Promise<void> {\n const injected = this.config(\"client\");\n\n if (injected) {\n this.sentry = injected;\n\n return;\n }\n\n await loadSentry();\n\n if (!Sentry) {\n return;\n }\n\n const options = this.config(\"options\");\n\n if (options && !Sentry.getClient()) {\n Sentry.init(options);\n }\n\n this.sentry = Sentry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(data: LoggingData): Promise<void> {\n if (!this.shouldBeLogged(data)) {\n return;\n }\n\n // Only wait for the optional SDK when we do not already have a forwarder.\n // An INJECTED client is resolved synchronously in the constructor, and that is\n // the whole point of doing it there: an entry logged on the same tick as\n // construction must still send. Awaiting loadSentry() unconditionally undid\n // that — with @sentry/node absent (it is an optional peer) the import can take\n // seconds, so a log with a perfectly good injected client stalled behind an SDK\n // it was never going to use.\n if (!this.sentry && isModuleExists === null) {\n // wait until module is fully loaded\n await loadSentry();\n }\n\n if (!this.sentry) {\n this.reportMissingSdk();\n\n return;\n }\n\n const { module, action, message, type: level, context } = data;\n\n if (this.isEventLevel(level)) {\n this.captureEvent(this.sentry, {\n module,\n action,\n message,\n level,\n context,\n });\n\n return;\n }\n\n this.sentry.addBreadcrumb({\n category: module,\n message: this.toText(message),\n level: this.toSentryLevel(level),\n data: context,\n });\n }\n\n /**\n * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\n * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\n */\n public async flush(): Promise<void> {\n if (!this.sentry) {\n return;\n }\n\n await this.sentry.flush(this.config(\"flushTimeout\"));\n }\n\n /**\n * Whether the level should be sent as a Sentry event (vs a breadcrumb).\n */\n private isEventLevel(level: LogLevel): boolean {\n return Boolean(this.config(\"eventLevels\")?.includes(level));\n }\n\n /**\n * Send an entry as a Sentry event. An `Error` message goes through\n * `captureException` so Sentry parses the real stack and groups properly;\n * any other message goes through `captureMessage`. `module` / `action` are\n * attached as tags and `context` as a structured context, scoped to this\n * event only via `withScope`.\n */\n private captureEvent(\n sentry: SentryForwarder,\n entry: {\n module: string;\n action: string;\n message: unknown;\n level: LogLevel;\n context?: Record<string, any>;\n },\n ): void {\n const { module, action, message, level, context } = entry;\n const sentryLevel = this.toSentryLevel(level);\n\n sentry.withScope((scope) => {\n scope.setLevel(sentryLevel);\n scope.setTags({ module, action });\n\n if (context) {\n scope.setContext(\"context\", context);\n }\n\n if (message instanceof Error) {\n sentry.captureException(message);\n } else {\n sentry.captureMessage(this.toText(message), sentryLevel);\n }\n });\n }\n\n /**\n * Map a logger level to a Sentry severity. `success` has no Sentry\n * equivalent, so it is reported as informational.\n */\n private toSentryLevel(level: LogLevel): SentrySeverityLevel {\n switch (level) {\n case \"warn\":\n return \"warning\";\n case \"success\":\n return \"info\";\n default:\n return level; // debug | info | error | fatal map 1:1\n }\n }\n\n /**\n * Coerce a message into the string Sentry's APIs expect — an `Error`'s\n * `.message`, a string as-is, anything else safely JSON-serialized.\n */\n private toText(message: unknown): string {\n if (typeof message === \"string\") {\n return message;\n }\n\n if (message instanceof Error) {\n return message.message;\n }\n\n return safeJsonStringify(message);\n }\n\n /**\n * Surface the install instructions exactly once when the SDK is absent. The\n * logger can't log through itself here, so this writes to stderr — matching\n * how the file channels report write failures.\n */\n private reportMissingSdk(): void {\n if (isModuleExists === false && !this.warnedMissing) {\n this.warnedMissing = true;\n console.error(SENTRY_INSTALL_INSTRUCTIONS);\n }\n }\n}\n"],"mappings":"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"}
|
package/esm/index.d.mts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { AutoFlushEvent, BasicLogConfigurations, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig } from "./types.mjs";
|
|
2
|
-
import { LogChannel } from "./log-channel.mjs";
|
|
1
|
+
import { AutoFlushEvent, BasicLogConfigurations, ChannelErrorHandler, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig } from "./types.mjs";
|
|
2
|
+
import { ChannelErrorReporter, LogChannel } from "./log-channel.mjs";
|
|
3
3
|
import { ConsoleLog, ConsoleLogConfig } from "./channels/console-log.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { FileNameOption } from "./channels/file-name-pattern.mjs";
|
|
5
|
+
import { DAILY_FILE_NAME, FileLog, FileLogConfig, FileLogRetention, HOURLY_FILE_NAME, ROTATE_FILE_NAME } from "./channels/file-log.mjs";
|
|
5
6
|
import { JSONFileLog } from "./channels/json-file-log.mjs";
|
|
6
7
|
import { SentryBreadcrumb, SentryForwarder, SentryInitOptions, SentryLog, SentryLogConfig, SentryScopeLike } from "./channels/sentry-log.mjs";
|
|
7
|
-
import { Logger, log } from "./logger.mjs";
|
|
8
|
+
import { DEFAULT_FLUSH_TIMEOUT_MS, Logger, log } from "./logger.mjs";
|
|
8
9
|
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./redact/default-keys.mjs";
|
|
9
10
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact/redact.mjs";
|
|
11
|
+
import { EscapeLogValueOptions, escapeLogValue } from "./redact/escape-log-value.mjs";
|
|
12
|
+
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./redact/mask-email.mjs";
|
|
10
13
|
import { CaptureUnhandledOptions, captureAnyUnhandledRejection } from "./utils/capture-unhandled-errors.mjs";
|
|
11
14
|
import { clearMessage } from "./utils/clear-message.mjs";
|
|
12
15
|
import { safeJsonStringify } from "./utils/safe-json-stringify.mjs";
|
|
13
|
-
export { AutoFlushEvent, BasicLogConfigurations, CaptureUnhandledOptions, ConsoleLog, ConsoleLogConfig, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, DebugMode, FileLog, FileLogConfig, JSONFileLog, LogChannel, LogContract, LogLevel, LogMessage, Logger, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig, SentryBreadcrumb, SentryForwarder, SentryInitOptions, SentryLog, SentryLogConfig, SentryScopeLike, applyRedact, captureAnyUnhandledRejection, clearMessage, log, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
|
16
|
+
export { AutoFlushEvent, BasicLogConfigurations, CaptureUnhandledOptions, ChannelErrorHandler, ChannelErrorReporter, ConsoleLog, ConsoleLogConfig, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, DebugMode, type EscapeLogValueOptions, FileLog, FileLogConfig, FileLogRetention, type FileNameOption, HOURLY_FILE_NAME, JSONFileLog, LogChannel, LogContract, LogLevel, LogMessage, Logger, LoggingData, MASK_EMAILS_MAX_LENGTH, OmittedLoggingData, ROTATE_FILE_NAME, RedactCensor, RedactConfig, SentryBreadcrumb, SentryForwarder, SentryInitOptions, SentryLog, SentryLogConfig, SentryScopeLike, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
package/esm/index.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { LogChannel } from "./log-channel.mjs";
|
|
2
2
|
import { ConsoleLog } from "./channels/console-log.mjs";
|
|
3
|
-
import { FileLog } from "./channels/file-log.mjs";
|
|
3
|
+
import { DAILY_FILE_NAME, FileLog, HOURLY_FILE_NAME, ROTATE_FILE_NAME } from "./channels/file-log.mjs";
|
|
4
4
|
import { safeJsonStringify } from "./utils/safe-json-stringify.mjs";
|
|
5
5
|
import { JSONFileLog } from "./channels/json-file-log.mjs";
|
|
6
6
|
import { SentryLog } from "./channels/sentry-log.mjs";
|
|
7
7
|
import "./channels/index.mjs";
|
|
8
8
|
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./redact/default-keys.mjs";
|
|
9
9
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact/redact.mjs";
|
|
10
|
+
import { escapeLogValue } from "./redact/escape-log-value.mjs";
|
|
11
|
+
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./redact/mask-email.mjs";
|
|
10
12
|
import "./redact/index.mjs";
|
|
11
13
|
import { clearMessage } from "./utils/clear-message.mjs";
|
|
12
|
-
import { Logger, log } from "./logger.mjs";
|
|
14
|
+
import { DEFAULT_FLUSH_TIMEOUT_MS, Logger, log } from "./logger.mjs";
|
|
13
15
|
import { captureAnyUnhandledRejection } from "./utils/capture-unhandled-errors.mjs";
|
|
14
16
|
import "./utils/index.mjs";
|
|
15
17
|
|
|
16
|
-
export { ConsoleLog, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, FileLog, JSONFileLog, LogChannel, Logger, SentryLog, applyRedact, captureAnyUnhandledRejection, clearMessage, log, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
|
18
|
+
export { ConsoleLog, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, FileLog, HOURLY_FILE_NAME, JSONFileLog, LogChannel, Logger, MASK_EMAILS_MAX_LENGTH, ROTATE_FILE_NAME, SentryLog, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
package/esm/log-channel.d.mts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { BasicLogConfigurations, LogContract, LoggingData, RedactConfig } from "./types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/logger/src/log-channel.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Receives a failure the channel hits outside a `log()` call (for example
|
|
6
|
+
* `FileLog` retention). The logger installs one that forwards to its
|
|
7
|
+
* `onChannelError` handler.
|
|
8
|
+
*/
|
|
9
|
+
type ChannelErrorReporter = (error: unknown, entry: LoggingData) => void;
|
|
4
10
|
declare abstract class LogChannel<Options extends BasicLogConfigurations = BasicLogConfigurations> implements LogContract {
|
|
5
11
|
/**
|
|
6
12
|
* Channel name
|
|
@@ -26,10 +32,26 @@ declare abstract class LogChannel<Options extends BasicLogConfigurations = Basic
|
|
|
26
32
|
* Determine whether the channel is fully initialized
|
|
27
33
|
*/
|
|
28
34
|
protected isInitialized: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Where {@link reportError} sends failures; set by the logger the channel
|
|
37
|
+
* is added to.
|
|
38
|
+
*/
|
|
39
|
+
protected errorReporter?: ChannelErrorReporter;
|
|
29
40
|
/**
|
|
30
41
|
* Constructor
|
|
31
42
|
*/
|
|
32
43
|
constructor(configurations?: Options);
|
|
44
|
+
/**
|
|
45
|
+
* Route this channel's own failures (those outside a `log()` call) to the
|
|
46
|
+
* given reporter. `Logger` calls this when the channel is added, so the
|
|
47
|
+
* failures reach `log.onChannelError(...)`.
|
|
48
|
+
*/
|
|
49
|
+
setErrorReporter(reporter: ChannelErrorReporter | undefined): void;
|
|
50
|
+
/**
|
|
51
|
+
* Report a failure that happened outside a `log()` call. Never throws:
|
|
52
|
+
* without a reporter it writes one line to stderr.
|
|
53
|
+
*/
|
|
54
|
+
protected reportError(error: unknown, entry: LoggingData): void;
|
|
33
55
|
/**
|
|
34
56
|
* Initialize the channel
|
|
35
57
|
*/
|
|
@@ -82,5 +104,5 @@ declare abstract class LogChannel<Options extends BasicLogConfigurations = Basic
|
|
|
82
104
|
protected withBasicConfigurations(configurations: Partial<Options>): Options;
|
|
83
105
|
}
|
|
84
106
|
//#endregion
|
|
85
|
-
export { LogChannel };
|
|
107
|
+
export { ChannelErrorReporter, LogChannel };
|
|
86
108
|
//# sourceMappingURL=log-channel.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["../../../../../../../packages/logger/src/log-channel.ts"],"mappings":";;;;;AAOA;;;KAAY,oBAAA,IAAwB,KAAA,WAAgB,KAAA,EAAO,WAAW;AAAA,uBAEhD,UAAA,iBACJ,sBAAA,GAAyB,sBAAA,aAC9B,WAAA;EAJ8C;;;EAQlD,IAAA;EANa;;;EAWb,WAAA;EAVkC;;;EAelC,QAAA;EA0B6B;;;EAAA,UArB1B,qBAAA,EAAuB,OAAA;EAsEA;;;EAAA,UAjEvB,qBAAA,EAAuB,OAAA;EAwEW;;;EAAA,UAnElC,aAAA;EA+Gc;;;;EAAA,UAzGd,aAAA,GAAgB,oBAAA;EAnCf;;;cAwCQ,cAAA,GAAiB,OAAA;EAzCpB;;;;;EA4DT,gBAAA,CAAiB,QAAA,EAAU,oBAAA;EAxCxB;;;;EAAA,UAgDA,WAAA,CAAY,KAAA,WAAgB,KAAA,EAAO,WAAA;EAhCnC;;;EAAA,UAiDA,IAAA,YAAgB,OAAA;EA5CP;;;EAAA,UAiDT,MAAA,iBAAuB,OAAA,EAAS,GAAA,EAAK,CAAA,GAAI,OAAA,CAAQ,CAAA;EAtBjD;;;EAAA,UA6BA,iBAAA,CAAkB,cAAA,EAAgB,OAAA;EAZlC;;;EAAA,UAwBA,cAAA,CAAe,IAAA,EAAM,WAAA;EAnBE;;;EAAA,SAqCjB,GAAA,CAAI,IAAA,EAAM,WAAA,UAAqB,OAAA;EArCY;;;EA0CpD,SAAA;EAvBG;;;;;;;EAgCH,KAAA,YAAiB,OAAA;EAAjB;;;;;;EAQA,eAAA,IAAmB,YAAA;EAkBhB;;;EAAA,UAXA,oBAAA;;;;;;;YAWA,uBAAA,CAAwB,cAAA,EAAgB,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA"}
|
package/esm/log-channel.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/logger/src/log-channel.ts
|
|
2
2
|
var LogChannel = class {
|
|
3
3
|
/**
|
|
4
4
|
* Constructor
|
|
@@ -15,6 +15,28 @@ var LogChannel = class {
|
|
|
15
15
|
}, 0);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
+
* Route this channel's own failures (those outside a `log()` call) to the
|
|
19
|
+
* given reporter. `Logger` calls this when the channel is added, so the
|
|
20
|
+
* failures reach `log.onChannelError(...)`.
|
|
21
|
+
*/
|
|
22
|
+
setErrorReporter(reporter) {
|
|
23
|
+
this.errorReporter = reporter;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Report a failure that happened outside a `log()` call. Never throws:
|
|
27
|
+
* without a reporter it writes one line to stderr.
|
|
28
|
+
*/
|
|
29
|
+
reportError(error, entry) {
|
|
30
|
+
try {
|
|
31
|
+
if (this.errorReporter) {
|
|
32
|
+
this.errorReporter(error, entry);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
36
|
+
process.stderr.write(`[@warlock.js/logger] channel "${this.name}" failed: ${reason}\n`);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
18
40
|
* Get config value
|
|
19
41
|
*/
|
|
20
42
|
config(key) {
|