From f7d2c7084bad4f81bea18e76059405e3e8611226 Mon Sep 17 00:00:00 2001 From: creations Date: Wed, 11 Jun 2025 10:13:27 -0400 Subject: [PATCH] - added fileNameFormat - fixed up some logic aka duplicate logic - actually verify config -add more ansiColors -default to no max files --- README.md | 17 +++- biome.json | 6 +- package.json | 6 ++ src/index.ts | 65 ++++++------ src/lib/char.ts | 173 +++++++++++++++++++++++++------- src/lib/config.ts | 245 +++++++++++++++++++++++++++++++++++++++------- src/lib/file.ts | 99 ++++++++++++------- types/index.ts | 31 +++++- 8 files changed, 491 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 8714f5a..05c9160 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A minimal, flexible logger for Node with: - Colored console output - Daily `.jsonl` file logging -- Configurable output patterns +- Configurable output patterns and file naming - Structured logs with caller metadata - Fully typed config with environment/file/constructor override @@ -14,7 +14,7 @@ A minimal, flexible logger for Node with: - Console and file logging with level-based filtering - Colored output with ANSI formatting -- Daily rotated `.jsonl` files +- Daily rotated `.jsonl` files with custom naming patterns - Supports runtime configuration merging - Auto-formatted output using custom patterns - Includes caller file, line, and column @@ -75,6 +75,7 @@ constructor > environment > logger.json > defaults "rotate": true, "maxFiles": 3, + "fileNameFormat": "yyyy-MM-dd", "console": true, "consoleColor": true, @@ -121,6 +122,7 @@ constructor > environment > logger.json > defaults | `LOG_DISABLE_FILE` | Disable file output (`true` or `false`) | | `LOG_ROTATE` | Enable daily rotation | | `LOG_MAX_FILES` | Max rotated files to keep | +| `LOG_FILE_NAME_FORMAT` | Custom file name format (default: yyyy-MM-dd) | | `LOG_CONSOLE` | Enable console output | | `LOG_CONSOLE_COLOR` | Enable ANSI color in console output | | `LOG_DATE_FORMAT` | Date format for display timestamp | @@ -133,6 +135,17 @@ constructor > environment > logger.json > defaults --- +### Custom File Naming +```json +{ + "fileNameFormat": "yyyy-MM-dd", // 2025-06-03.jsonl + "fileNameFormat": "yyyy-MM-dd_HH-mm", // 2025-06-03_18-30.jsonl + "fileNameFormat": "yyyyMMdd", // 20250603.jsonl +} +``` + +--- + ## Pattern Tokens These tokens are replaced in the log pattern: diff --git a/biome.json b/biome.json index 66455d1..1e70609 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "ignore": ["dist", "types"] + "ignore": ["dist"] }, "formatter": { "enabled": true, @@ -29,14 +29,14 @@ "recommended": true, "correctness": { "noUnusedImports": "error", - "useJsxKeyInIterable": "off", "noUnusedVariables": "error" }, "style": { "useConst": "error", "noVar": "error" } - } + }, + "ignore": ["types"] }, "javascript": { "formatter": { diff --git a/package.json b/package.json index 7afed7c..51855f7 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,12 @@ "type": "git", "url": "https://git.creations.works/atums/echo" }, + "keywords": ["logger", "logging", "typescript", "nodejs", "structured"], + "author": "creations.works", + "homepage": "https://git.creations.works/atums/echo", + "bugs": { + "url": "https://git.creations.works/atums/echo/issues" + }, "dependencies": { "date-fns-tz": "^3.2.0" } diff --git a/src/index.ts b/src/index.ts index a3b0719..37abdab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,17 +7,24 @@ import { statSync, } from "node:fs"; import { resolve } from "node:path"; -import { format, inspect } from "node:util"; -import { getCallerInfo, getTimestamp, parsePattern } from "@lib/char"; import { - ansiColors, + formatData, + getCallerInfo, + getConsoleMethod, + getTimestamp, + parsePattern, + processPattern, +} from "@lib/char"; +import { defaultConfig, loadEnvConfig, loadLoggerConfig, logLevelValues, + validateAndSanitizeConfig, } from "@lib/config"; import { FileLogger } from "@lib/file"; -import type { LogLevel, LoggerConfig } from "@types"; + +import type { LogLevel, LoggerConfig, PatternTokens } from "@types"; class Echo { private readonly directory: string; @@ -35,12 +42,18 @@ class Echo { const envConfig: LoggerConfig = loadEnvConfig(); - this.config = { + const mergedConfig = { ...defaultConfig, ...fileConfig, ...envConfig, ...overrideConfig, }; + const finalConfig = validateAndSanitizeConfig( + mergedConfig, + "merged configuration", + ); + + this.config = finalConfig as Required; this.directory = resolve(this.config.directory); @@ -75,9 +88,7 @@ class Echo { const line = parsePattern({ level, data, config: this.config }); if (this.config.console) { - console[level === "error" ? "error" : level === "warn" ? "warn" : "log"]( - line, - ); + console[getConsoleMethod(level)](line); } if (!this.config.disableFile && this.fileLogger) { @@ -113,39 +124,21 @@ class Echo { if (this.config.silent) return; const timestamps = getTimestamp(this.config); - - const normalizedTag = tag.toUpperCase(); - const tagColor = this.config.consoleColor - ? (ansiColors[this.config.customColors?.[normalizedTag] ?? "green"] ?? "") - : ""; - const contextColor = this.config.consoleColor ? ansiColors.cyan : ""; - const gray = this.config.consoleColor ? ansiColors.gray : ""; - const reset = this.config.consoleColor ? ansiColors.reset : ""; - - const resolvedData = - this.config.prettyPrint && typeof data === "object" && data !== null - ? inspect(data, { - depth: null, - colors: this.config.consoleColor, - breakLength: 1, - compact: false, - }) - : format(data); + const resolvedData = formatData(data, this.config); const pattern = this.config.customPattern ?? "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}"; - const line = pattern - .replace(/{timestamp}/g, timestamps.timestamp) - .replace(/{pretty-timestamp}/g, timestamps.prettyTimestamp) - .replace(/{tag}/g, tag) - .replace(/{context}/g, context) - .replace(/{data}/g, resolvedData) - .replace(/{color:gray}/g, gray) - .replace(/{color:tagColor}/g, tagColor) - .replace(/{color:contextColor}/g, contextColor) - .replace(/{reset}/g, reset); + const tokens: PatternTokens = { + timestamp: timestamps.timestamp, + prettyTimestamp: timestamps.prettyTimestamp, + tag, + context, + data: resolvedData, + }; + + const line = processPattern(pattern, tokens, this.config, undefined, tag); if (this.config.console) { console.log(line); diff --git a/src/lib/char.ts b/src/lib/char.ts index 00671eb..050b2c7 100644 --- a/src/lib/char.ts +++ b/src/lib/char.ts @@ -8,6 +8,7 @@ import type { LogLevelValue, LoggerConfig, PatternContext, + PatternTokens, } from "@types"; function getTimestamp(config: Required): { @@ -113,22 +114,121 @@ function generateShortId(length = 8): string { return id; } -function replaceColorTokens( - input: string, - level: LogLevel, +function formatData(data: unknown, config: Required): string { + return config.prettyPrint && typeof data === "object" && data !== null + ? inspect(data, { + depth: null, + colors: config.consoleColor, + breakLength: 1, + compact: false, + }) + : format(data); +} + +function getConsoleMethod(level: LogLevel): "log" | "warn" | "error" { + if (level === "error" || level === "fatal") return "error"; + if (level === "warn") return "warn"; + return "log"; +} + +function resolveColor( + colorKey: string, config: Required, + level?: LogLevel, + tag?: string, ): string { - return input - .replace(/{color:(\w+)}/g, (_, colorKey) => { - if (!config.consoleColor) return ""; - if (colorKey === "levelColor") { - const colorForLevel = - config.levelColor?.[level] ?? defaultLevelColor[level]; - return ansiColors[colorForLevel ?? ""] ?? ""; - } - return ansiColors[colorKey] ?? ""; - }) - .replace(/{reset}/g, config.consoleColor ? ansiColors.reset : ""); + if (!config.consoleColor) return ""; + + if (colorKey === "levelColor" && level) { + const colorForLevel = + config.levelColor?.[level] ?? defaultLevelColor[level]; + return ansiColors[colorForLevel ?? ""] ?? ""; + } + + if (colorKey === "tagColor" && tag) { + const normalizedTag = tag.toUpperCase(); + return ansiColors[config.customColors?.[normalizedTag] ?? "green"] ?? ""; + } + + if (colorKey === "contextColor") { + return ansiColors.cyan ?? ""; + } + + return ansiColors[colorKey] ?? ""; +} + +function serializeLogData(data: unknown): unknown { + if (data instanceof Error) { + return { + name: data.name, + message: data.message, + stack: data.stack, + }; + } + + if (typeof data === "string" || typeof data === "number") { + return data; + } + + return data; +} + +function processPattern( + pattern: string, + tokens: PatternTokens, + config: Required, + level?: LogLevel, + tag?: string, +): string { + let processed = pattern; + + if (tokens.timestamp) { + processed = processed.replace(/{timestamp}/g, tokens.timestamp); + } + if (tokens.prettyTimestamp) { + processed = processed.replace( + /{pretty-timestamp}/g, + tokens.prettyTimestamp, + ); + } + if (tokens.levelName) { + processed = processed.replace(/{level-name}/g, tokens.levelName); + } + if (tokens.level) { + processed = processed.replace(/{level}/g, tokens.level); + } + if (tokens.fileName) { + processed = processed.replace(/{file-name}/g, tokens.fileName); + } + if (tokens.line) { + processed = processed.replace(/{line}/g, tokens.line); + } + if (tokens.column) { + processed = processed.replace(/{column}/g, tokens.column); + } + if (tokens.data) { + processed = processed.replace(/{data}/g, tokens.data); + } + if (tokens.id) { + processed = processed.replace(/{id}/g, tokens.id); + } + if (tokens.tag) { + processed = processed.replace(/{tag}/g, tokens.tag); + } + if (tokens.context) { + processed = processed.replace(/{context}/g, tokens.context); + } + + processed = processed.replace(/{color:(\w+)}/g, (_, colorKey) => { + return resolveColor(colorKey, config, level, tag); + }); + + processed = processed.replace( + /{reset}/g, + config.consoleColor ? ansiColors.reset : "", + ); + + return processed; } function parsePattern(ctx: PatternContext): string { @@ -136,30 +236,33 @@ function parsePattern(ctx: PatternContext): string { const { id, fileName, line, column, timestamp, prettyTimestamp } = getCallerInfo(config); - const resolvedData: string = - config.prettyPrint && typeof data === "object" && data !== null - ? inspect(data, { - depth: null, - colors: config.consoleColor, - breakLength: 1, - compact: false, - }) - : format(data); + const resolvedData: string = formatData(data, config); const numericLevel: LogLevelValue = logLevelValues[level]; - const final = config.pattern - .replace(/{timestamp}/g, timestamp) - .replace(/{pretty-timestamp}/g, prettyTimestamp) - .replace(/{level-name}/g, level.toUpperCase()) - .replace(/{level}/g, String(numericLevel)) - .replace(/{file-name}/g, fileName) - .replace(/{line}/g, line) - .replace(/{data}/g, resolvedData) - .replace(/{id}/g, id) - .replace(/{column}/g, column); + const tokens: PatternTokens = { + timestamp, + prettyTimestamp, + levelName: level.toUpperCase(), + level: String(numericLevel), + fileName, + line, + column, + data: resolvedData, + id, + }; - return replaceColorTokens(final, level, config); + return processPattern(config.pattern, tokens, config, level); } -export { parsePattern, getCallerInfo, getTimestamp, generateShortId }; +export { + parsePattern, + getCallerInfo, + getTimestamp, + generateShortId, + formatData, + getConsoleMethod, + resolveColor, + serializeLogData, + processPattern, +}; diff --git a/src/lib/config.ts b/src/lib/config.ts index c372a51..e5a9f0f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -10,7 +10,7 @@ const logLevelValues = { error: 50, fatal: 60, silent: 70, -}; +} as const; const defaultLevelColor: Record = { trace: "cyan", @@ -35,7 +35,12 @@ const ansiColors: Record = { cyan: "\x1b[36m", white: "\x1b[37m", gray: "\x1b[90m", -}; + bold: "\x1b[1m", + underline: "\x1b[4m", + inverse: "\x1b[7m", + hidden: "\x1b[8m", + strikethrough: "\x1b[9m", +} as const; const defaultConfig: Required = { directory: "logs", @@ -43,7 +48,8 @@ const defaultConfig: Required = { disableFile: false, rotate: true, - maxFiles: 3, + maxFiles: null, + fileNameFormat: "yyyy-MM-dd", console: true, consoleColor: true, @@ -72,12 +78,129 @@ const defaultConfig: Required = { prettyPrint: true, }; +function isValidLogLevel(level: string): level is LogLevel { + return level in logLevelValues; +} + +function isValidColor(color: string): color is keyof typeof ansiColors { + return color in ansiColors; +} + +function parseNumericEnv( + value: string | undefined, + min = 0, +): number | undefined { + if (!value) return undefined; + + const parsed = Number.parseInt(value, 10); + if (Number.isNaN(parsed) || parsed < min) { + return undefined; + } + + return parsed; +} + +function parseBooleanEnv(value: string | undefined): boolean | undefined { + if (!value) return undefined; + return value.toLowerCase() === "true"; +} + +function validateAndSanitizeConfig( + config: LoggerConfig, + source: string, +): LoggerConfig { + const sanitized = { ...config }; + const warnings: string[] = []; + + if (sanitized.level && !isValidLogLevel(sanitized.level)) { + warnings.push( + `Invalid log level "${sanitized.level}" in ${source}, using default "info"`, + ); + sanitized.level = "info"; + } + + if (sanitized.maxFiles !== undefined && sanitized.maxFiles !== null) { + if ( + typeof sanitized.maxFiles !== "number" || + sanitized.maxFiles < 1 || + !Number.isInteger(sanitized.maxFiles) + ) { + warnings.push( + `Invalid maxFiles value "${sanitized.maxFiles}" in ${source}, setting to null`, + ); + sanitized.maxFiles = null; + } + } + + if (sanitized.levelColor) { + const validLevelColors: Partial> = + {}; + + for (const [level, color] of Object.entries(sanitized.levelColor)) { + if (!isValidLogLevel(level)) { + warnings.push( + `Invalid log level "${level}" in levelColor from ${source}, skipping`, + ); + continue; + } + if (!isValidColor(color)) { + warnings.push( + `Invalid color "${color}" for level "${level}" in ${source}, using default`, + ); + validLevelColors[level as LogLevel] = + defaultLevelColor[level as LogLevel]; + } else { + validLevelColors[level as LogLevel] = color; + } + } + + sanitized.levelColor = validLevelColors; + } + + if (sanitized.customColors) { + const validCustomColors: Record = {}; + + for (const [tag, color] of Object.entries(sanitized.customColors)) { + if (!isValidColor(color)) { + warnings.push( + `Invalid color "${color}" for tag "${tag}" in ${source}, skipping`, + ); + continue; + } + validCustomColors[tag] = color; + } + + sanitized.customColors = validCustomColors; + } + + if (warnings.length > 0) { + console.warn( + `[@atums/echo] Configuration warnings:\n ${warnings.join("\n ")}`, + ); + } + + return sanitized; +} + function loadLoggerConfig(configPath = "logger.json"): LoggerConfig { try { const fullPath: string = resolve(process.cwd(), configPath); const raw: string = readFileSync(fullPath, "utf-8"); - return JSON.parse(raw); - } catch { + const parsed = JSON.parse(raw); + + if (typeof parsed !== "object" || parsed === null) { + console.warn(`[@atums/echo] Invalid config file format: ${configPath}`); + return {}; + } + + return validateAndSanitizeConfig(parsed, `config file "${configPath}"`); + } catch (error) { + if (error instanceof Error && !error.message.includes("ENOENT")) { + console.warn( + `[@atums/echo] Failed to load config file ${configPath}:`, + error.message, + ); + } return {}; } } @@ -85,50 +208,97 @@ function loadLoggerConfig(configPath = "logger.json"): LoggerConfig { function loadEnvConfig(): LoggerConfig { const config: LoggerConfig = {}; - if (process.env.LOG_LEVEL) config.level = process.env.LOG_LEVEL as LogLevel; - if (process.env.LOG_DIRECTORY) config.directory = process.env.LOG_DIRECTORY; - if (process.env.LOG_DISABLE_FILE) - config.disableFile = process.env.LOG_DISABLE_FILE === "true"; - if (process.env.LOG_ROTATE) config.rotate = process.env.LOG_ROTATE === "true"; - if (process.env.LOG_MAX_FILES) - config.maxFiles = Number.parseInt(process.env.LOG_MAX_FILES, 10); - if (process.env.LOG_CONSOLE) - config.console = process.env.LOG_CONSOLE === "true"; - if (process.env.LOG_CONSOLE_COLOR) - config.consoleColor = process.env.LOG_CONSOLE_COLOR === "true"; - if (process.env.LOG_DATE_FORMAT) + if (process.env.LOG_LEVEL && isValidLogLevel(process.env.LOG_LEVEL)) { + config.level = process.env.LOG_LEVEL; + } + + if (process.env.LOG_DIRECTORY) { + config.directory = process.env.LOG_DIRECTORY; + } + + config.disableFile = parseBooleanEnv(process.env.LOG_DISABLE_FILE); + config.rotate = parseBooleanEnv(process.env.LOG_ROTATE); + config.console = parseBooleanEnv(process.env.LOG_CONSOLE); + config.consoleColor = parseBooleanEnv(process.env.LOG_CONSOLE_COLOR); + config.silent = parseBooleanEnv(process.env.LOG_SILENT); + config.prettyPrint = parseBooleanEnv(process.env.LOG_PRETTY_PRINT); + + const maxFiles = parseNumericEnv(process.env.LOG_MAX_FILES, 1); + if (maxFiles !== undefined) { + config.maxFiles = maxFiles; + } + + if (process.env.LOG_FILE_NAME_FORMAT) { + config.fileNameFormat = process.env.LOG_FILE_NAME_FORMAT; + } + + if (process.env.LOG_DATE_FORMAT) { config.dateFormat = process.env.LOG_DATE_FORMAT; - if (process.env.LOG_TIMEZONE) config.timezone = process.env.LOG_TIMEZONE; - if (process.env.LOG_SILENT) config.silent = process.env.LOG_SILENT === "true"; - if (process.env.LOG_PATTERN) config.pattern = process.env.LOG_PATTERN; - if (process.env.LOG_PRETTY_PRINT) - config.prettyPrint = process.env.LOG_PRETTY_PRINT === "true"; + } + + if (process.env.LOG_TIMEZONE) { + config.timezone = process.env.LOG_TIMEZONE; + } + + if (process.env.LOG_PATTERN) { + config.pattern = process.env.LOG_PATTERN; + } + + if (process.env.LOG_CUSTOM_PATTERN) { + config.customPattern = process.env.LOG_CUSTOM_PATTERN; + } if (process.env.LOG_LEVEL_COLOR) { const colors = process.env.LOG_LEVEL_COLOR.split(","); - for (const color of colors) { - const [level, colorName] = color.split(":"); - if (logLevelValues[level as LogLevel] !== undefined) { - config.levelColor = { - ...config.levelColor, - [level as LogLevel]: colorName as keyof typeof ansiColors, - }; + const levelColor: Partial> = {}; + + for (const colorPair of colors) { + const [level, colorName] = colorPair.split(":"); + + if ( + level && + colorName && + isValidLogLevel(level) && + isValidColor(colorName) + ) { + levelColor[level] = colorName; + } else { + console.warn(`[@atums/echo] Invalid level color pair: ${colorPair}`); } } + + if (Object.keys(levelColor).length > 0) { + config.levelColor = levelColor; + } } if (process.env.LOG_CUSTOM_COLORS) { const colors = process.env.LOG_CUSTOM_COLORS.split(","); - for (const color of colors) { - const [tag, colorName] = color.split(":"); - config.customColors = { - ...config.customColors, - [tag]: colorName as keyof typeof ansiColors, - }; + const customColors: Record = {}; + + for (const colorPair of colors) { + const [tag, colorName] = colorPair.split(":"); + + if (tag && colorName && isValidColor(colorName)) { + customColors[tag] = colorName; + } else { + console.warn(`[@atums/echo] Invalid custom color pair: ${colorPair}`); + } + } + + if (Object.keys(customColors).length > 0) { + config.customColors = customColors; } } - return config; + const sanitizedConfig = validateAndSanitizeConfig( + config, + "environment variables", + ); + + return Object.fromEntries( + Object.entries(sanitizedConfig).filter(([_, value]) => value !== undefined), + ) as LoggerConfig; } export { @@ -138,4 +308,7 @@ export { loadEnvConfig, logLevelValues, ansiColors, + validateAndSanitizeConfig, + isValidLogLevel, + isValidColor, }; diff --git a/src/lib/file.ts b/src/lib/file.ts index 1a7e372..6f1d70c 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -7,6 +7,7 @@ import { unlinkSync, } from "node:fs"; import { join } from "node:path"; +import { serializeLogData } from "@lib/char"; import type { LogLevel, LoggerConfig } from "@types"; import { format } from "date-fns-tz"; @@ -14,15 +15,30 @@ class FileLogger { private stream: WriteStream | null = null; private filePath = ""; private date = ""; + private fileNameFormat = "yyyy-MM-dd"; constructor(private readonly config: Required) { if (!existsSync(this.config.directory)) { mkdirSync(this.config.directory, { recursive: true }); } + + if (this.config.fileNameFormat) { + try { + format(new Date(), this.config.fileNameFormat, { + timeZone: this.config.timezone, + }); + this.fileNameFormat = this.config.fileNameFormat; + } catch (error) { + throw new Error( + `[@atums/echo] Invalid fileNameFormat: ${this.config.fileNameFormat}. Error: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } } private getLogFilePath(dateStr: string): string { - return join(this.config.directory, `${dateStr}.jsonl`); + const fileName = `${dateStr}.jsonl`; + return join(this.config.directory, fileName); } private resetStream(path: string): void { @@ -31,13 +47,32 @@ class FileLogger { this.filePath = path; } + private generateFileRegex(): RegExp { + const pattern = this.fileNameFormat + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + .replace(/\\y\\y\\y\\y/g, "\\d{4}") + .replace(/\\M\\M/g, "\\d{2}") + .replace(/\\d\\d/g, "\\d{2}") + .replace(/\\H\\H/g, "\\d{2}") + .replace(/\\m\\m/g, "\\d{2}") + .replace(/\\s\\s/g, "\\d{2}") + .replace(/\\S\\S\\S/g, "\\d{3}"); + + return new RegExp(`^${pattern}\\.jsonl$`); + } + private pruneOldLogs(): void { - if (this.config.maxFiles && this.config.maxFiles < 1) { + if (this.config.maxFiles === null) { + return; + } + + if (this.config.maxFiles < 1) { throw new Error("[@atums/echo] maxFiles must be >= 1 if set."); } + const fileRegex = this.generateFileRegex(); const files = readdirSync(this.config.directory) - .filter((file) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file)) + .filter((file) => fileRegex.test(file)) .sort(); const excess = files.slice( @@ -52,6 +87,13 @@ class FileLogger { } } + private getFilePath(dateStr?: string): string { + if (this.config.rotate && dateStr) { + return this.getLogFilePath(dateStr); + } + return join(this.config.directory, "log.jsonl"); + } + public write( level: LogLevel | string, data: unknown, @@ -67,27 +109,6 @@ class FileLogger { if (this.config.disableFile) return; const now = new Date(); - let path: string; - - if (this.config.rotate) { - const dateStr = format(now, "yyyy-MM-dd", { - timeZone: this.config.timezone, - }); - path = this.getLogFilePath(dateStr); - - if (!this.stream || this.filePath !== path || this.date !== dateStr) { - this.date = dateStr; - this.resetStream(path); - this.pruneOldLogs(); - } - } else { - path = join(this.config.directory, "log.jsonl"); - - if (!this.stream || this.filePath !== path) { - this.resetStream(path); - } - } - const line = `${JSON.stringify({ timestamp: new Date(meta.timestamp).getTime(), level, @@ -95,18 +116,28 @@ class FileLogger { file: meta.fileName, line: meta.line, column: meta.column, - data: - data instanceof Error - ? { - name: data.name, - message: data.message, - stack: data.stack, - } - : typeof data === "string" || typeof data === "number" - ? data - : data, + data: serializeLogData(data), })}\n`; + let path: string; + const dateStr = this.config.rotate + ? format(now, this.fileNameFormat, { timeZone: this.config.timezone }) + : undefined; + + const needsRotation = this.config.rotate && this.date !== dateStr; + path = this.getFilePath(dateStr); + + if (!this.stream || needsRotation || this.filePath !== path) { + if (this.config.rotate && dateStr) { + this.date = dateStr; + } + this.resetStream(path); + + if (needsRotation) { + this.pruneOldLogs(); + } + } + try { this.stream?.write(line); } catch (err) { diff --git a/types/index.ts b/types/index.ts index 0d73707..99ac624 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,6 +1,6 @@ -import { ansiColors, logLevelValues } from "@lib/config"; +import type { ansiColors, logLevelValues } from "@lib/config"; -type LogLevelValue = typeof logLevelValues[keyof typeof logLevelValues]; +type LogLevelValue = (typeof logLevelValues)[keyof typeof logLevelValues]; type LogLevel = keyof typeof logLevelValues; type LoggerConfig = { @@ -9,7 +9,8 @@ type LoggerConfig = { disableFile?: boolean; rotate?: boolean; - maxFiles?: number; + maxFiles?: number | null; + fileNameFormat?: string; console?: boolean; consoleColor?: boolean; @@ -28,10 +29,30 @@ type LoggerConfig = { prettyPrint?: boolean; }; -interface PatternContext { +type PatternContext = { level: LogLevel; data: unknown; config: Required; +}; + +interface PatternTokens { + timestamp?: string; + prettyTimestamp?: string; + levelName?: string; + level?: string; + fileName?: string; + line?: string; + column?: string; + data?: string; + id?: string; + tag?: string; + context?: string; } -export type { LogLevel, LogLevelValue, LoggerConfig, PatternContext }; +export type { + LogLevel, + LogLevelValue, + LoggerConfig, + PatternContext, + PatternTokens, +};