import type { LogHandlerOptions, LogMessage } from '../../common/log/log-base' import type { LogRotationOptions } from './log-file-rotation' import { createWriteStream, mkdirSync } from 'node:fs' import { basename, dirname, resolve } from 'node:path' import process from 'node:process' import { renderMessages } from '../../common/data/message' import { LogLevelError, LogLevelInfo, LogLevelWarn } from '../../common/log/log-base' import { useLevelFilter, useNamespaceFilter } from '../../common/log/log-filter' import { getLogRotationConfig } from './log-file-rotation' import { createRotationStream } from './log-rotation' const namespaces: Record = {} export interface LogFileHandlerOptions extends LogHandlerOptions { /** * Optional rotation options for log files. When provided, enables automatic log rotation. * Can be: * - `true`: Use default rotation settings (10MB size, 5 files, gzip compression) * - Rotation options object: Customize rotation behavior * - Shortcut strings: 'daily' | 'weekly' | 'monthly' | 'size' * * Examples: * - Enable with defaults: { rotation: true } * - Rotate daily: { rotation: { interval: '1d' } } * - Rotate when file reaches 10MB: { rotation: { size: '10M' } } * - Keep max 5 files: { rotation: { maxFiles: 5 } } * - Compress rotated files: { rotation: { compress: 'gzip' } } */ rotation?: LogRotationOptions } export function LoggerFileHandler(path: string, opt: LogFileHandlerOptions = {}) { const { level = undefined, filter = undefined, time = true, pretty = false, rotation, } = opt path = resolve(process.cwd(), path) const pathFolder = dirname(path) mkdirSync(pathFolder, { recursive: true }) // Use rotating stream if rotation options are provided const rotationOpts = getLogRotationConfig(rotation) let stream: ReturnType | ReturnType if (rotationOpts) { // ensure rotation writes into the same folder rotationOpts.path = pathFolder stream = createRotationStream(basename(path), rotationOpts) } else { stream = createWriteStream(path, { flags: 'a' }) } const matchesNamespace = useNamespaceFilter(filter) const matchesLevel = useLevelFilter(level) return (msg: LogMessage) => { if (!matchesLevel(msg.level)) return if (!matchesNamespace(msg.name)) return const timeNow = time ? `${new Date().toISOString()} ` : '' const name = msg.name || '' const ninfo = namespaces[name || ''] if (ninfo == null) namespaces[name] = ninfo const args: string[] = [ `[${name || '*'}]`, renderMessages(msg.messages, { pretty }), ] function write(...args: string[]): void { stream.write(`${args.join('\t')}\n`) } switch (msg.level) { case LogLevelInfo: write(`${timeNow}I|* `, ...args) break case LogLevelWarn: write(`${timeNow}W|** `, ...args) break case LogLevelError: write(`${timeNow}E|***`, ...args) break default: write(`${timeNow}D| `, ...args) break } } }