@loglayer/transport-log-file-rotation
Version:
Log file rotation transport for the LogLayer logging library.
1 lines • 28.6 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../src/LogFileRotationTransport.ts"],"sourcesContent":["import type { WriteStream } from \"node:fs\";\nimport { createReadStream, createWriteStream, writeFileSync } from \"node:fs\";\nimport { access, unlink } from \"node:fs/promises\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createGzip } from \"node:zlib\";\nimport type { LoggerlessTransportConfig, LogLayerTransportParams } from \"@loglayer/transport\";\nimport { LoggerlessTransport } from \"@loglayer/transport\";\nimport FileStreamRotator from \"file-stream-rotator\";\n\ninterface FileStreamRotatorOptions {\n filename: string;\n frequency?: string;\n verbose?: boolean;\n date_format?: string;\n size?: string;\n max_logs?: string;\n audit_file?: string;\n end_stream?: boolean;\n extension?: string;\n create_symlink?: boolean;\n symlink_name?: string;\n utc?: boolean;\n audit_hash_type?: \"md5\" | \"sha256\";\n file_options?: {\n flags?: string;\n encoding?: string;\n mode?: number;\n };\n}\n\nexport interface LogFileRotationCallbacks {\n /**\n * Called when a log file is rotated\n * @param oldFile - The path to the old log file\n * @param newFile - The path to the new log file\n */\n onRotate?: (oldFile: string, newFile: string) => void;\n /**\n * Called when a new log file is created\n * @param newFile - The path to the new log file\n */\n onNew?: (newFile: string) => void;\n /**\n * Called when a log file is opened\n */\n onOpen?: () => void;\n /**\n * Called when a log file is closed\n */\n onClose?: () => void;\n /**\n * Called when an error occurs\n * @param error - The error that occurred\n */\n onError?: (error: Error) => void;\n /**\n * Called when the stream is finished\n */\n onFinish?: () => void;\n /**\n * Called when a log file is removed due to retention policy\n * @param info - Information about the removed log file\n */\n onLogRemoved?: (info: { date: number; name: string; hash: string }) => void;\n}\n\nexport interface LogFileRotationFieldNames {\n /**\n * Field name for the log level\n * @default \"level\"\n */\n level?: string;\n /**\n * Field name for the log message\n * @default \"message\"\n */\n message?: string;\n /**\n * Field name for the timestamp\n * @default \"timestamp\"\n */\n timestamp?: string;\n}\n\nexport interface LogFileRotationLevelMap {\n /**\n * Mapping for the 'fatal' log level\n * @example 60 or \"FATAL\"\n */\n fatal?: string | number;\n /**\n * Mapping for the 'error' log level\n * @example 50 or \"ERROR\"\n */\n error?: string | number;\n /**\n * Mapping for the 'warn' log level\n * @example 40 or \"WARN\"\n */\n warn?: string | number;\n /**\n * Mapping for the 'info' log level\n * @example 30 or \"INFO\"\n */\n info?: string | number;\n /**\n * Mapping for the 'debug' log level\n * @example 20 or \"DEBUG\"\n */\n debug?: string | number;\n /**\n * Mapping for the 'trace' log level\n * @example 10 or \"TRACE\"\n */\n trace?: string | number;\n}\n\nexport interface LogFileRotationBatchConfig {\n /**\n * Maximum number of log entries to queue before writing.\n * Default: 1000\n */\n size?: number;\n /**\n * Maximum time in milliseconds to wait before writing queued logs.\n * Default: 5000 (5 seconds)\n */\n timeout?: number;\n}\n\nexport interface LogFileRotationTransportConfig extends LoggerlessTransportConfig {\n /**\n * The filename pattern to use for the log files.\n * Supports date format using numerical values.\n * Example: \"./logs/application-%DATE%.log\"\n */\n filename: string;\n /**\n * Static data to be included in every log entry.\n * Can be either:\n * - A function that returns an object containing static data\n * - A direct object containing static data\n *\n * The data will be merged with the log entry before any other data.\n * If using a function, it will be called for each log entry.\n * @example\n * ```typescript\n * // Using a function\n * staticData: () => ({\n * hostname: hostname(),\n * pid: process.pid\n * })\n *\n * // Using an object\n * staticData: {\n * hostname: hostname(),\n * pid: process.pid\n * }\n * ```\n */\n staticData?: (() => Record<string, any>) | Record<string, any>;\n /**\n * The frequency of rotation. Can be:\n * - 'daily' for daily rotation\n * - 'date' for rotation on date format change\n * - '[1-30]m' for rotation every X minutes\n * - '[1-12]h' for rotation every X hours\n */\n frequency?: string;\n /**\n * The date format to use in the filename.\n * Uses single characters for each date component:\n * - 'Y' for full year\n * - 'M' for month\n * - 'D' for day\n * - 'H' for hour\n * - 'm' for minutes\n * - 's' for seconds\n *\n * Common patterns:\n * - For daily rotation: use \"YMD\" (creates files like app-20240117.log)\n * - For hourly/minute rotation: use \"YMDHm\" (creates files like app-202401171430.log)\n *\n * @default \"YMD\"\n */\n dateFormat?: string;\n /**\n * The size at which to rotate.\n * Examples: \"10M\", \"100K\", \"100B\"\n * If frequency is specified, this will be ignored.\n */\n size?: string;\n /**\n * Maximum number of logs to keep.\n * Can be a number of files or days (e.g., \"10d\" for 10 days)\n */\n maxLogs?: string | number;\n /**\n * Location to store the log audit file.\n * If not set, it will be stored in the root of the application.\n */\n auditFile?: string;\n /**\n * File extension to be appended to the filename.\n * Useful when using size restrictions as the rotation adds a count at the end.\n */\n extension?: string;\n /**\n * Create a tailable symlink to the current active log file.\n * Default: false\n */\n createSymlink?: boolean;\n /**\n * Name to use when creating the symbolic link.\n * Default: 'current.log'\n */\n symlinkName?: string;\n /**\n * Use UTC time for date in filename.\n * Default: false\n */\n utc?: boolean;\n /**\n * Use specified hashing algorithm for audit.\n * Default: 'md5'\n * Use 'sha256' for FIPS compliance.\n */\n auditHashType?: \"md5\" | \"sha256\";\n /**\n * File mode to be used when creating log files.\n * Default: 0o640 (user read/write, group read, others none)\n */\n fileMode?: number;\n /**\n * Options passed to the file stream.\n * See: https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options\n */\n fileOptions?: {\n flags?: string;\n encoding?: string;\n mode?: number;\n };\n /**\n * Event callbacks for various file stream events\n */\n callbacks?: LogFileRotationCallbacks;\n /**\n * Custom field names for the log entry JSON\n * Default: { level: \"level\", message: \"message\", data: \"data\", timestamp: \"timestamp\" }\n */\n fieldNames?: LogFileRotationFieldNames;\n /**\n * Delimiter between log entries.\n * Default: \"\\n\"\n */\n delimiter?: string;\n /**\n * Custom function to generate timestamps for log entries.\n * Can return either a string (e.g., ISO string) or a number (e.g., Unix timestamp)\n * If not provided, defaults to new Date().toISOString()\n */\n timestampFn?: () => string | number;\n /**\n * Custom mapping for log levels.\n * Each log level can be mapped to either a string or number.\n * Example: { error: 50, warn: 40, info: 30, debug: 20, trace: 10, fatal: 60 }\n * Example: { error: \"ERROR\", warn: \"WARN\", info: \"INFO\", debug: \"DEBUG\", trace: \"TRACE\", fatal: \"FATAL\" }\n */\n levelMap?: LogFileRotationLevelMap;\n /**\n * Whether to compress rotated log files using gzip.\n * When enabled, rotated files will be compressed with .gz extension.\n * Default: false\n */\n compressOnRotate?: boolean;\n /**\n * Whether to enable verbose mode in the underlying file-stream-rotator.\n * When enabled, the rotator will log detailed information about its operations.\n * Default: false\n */\n verbose?: boolean;\n /**\n * Batch processing configuration.\n * If defined, batch processing will be enabled.\n * When batching is enabled, logs are queued in memory and written to disk in batches.\n * Queued logs are automatically flushed in the following situations:\n * - When the batch size is reached\n * - When the batch timeout is reached\n * - When the transport is disposed\n * - When the process exits (including SIGINT and SIGTERM signals)\n */\n batch?: LogFileRotationBatchConfig;\n}\n\n/**\n * A transport that writes logs to rotating files with support for time-based and size-based rotation.\n * Features include:\n * - Automatic log file rotation based on time (hourly, daily) or size\n * - Support for date patterns in filenames using numerical values (YYYY, MM, DD, etc.)\n * - Size-based rotation with support for KB, MB, and GB units\n * - Compression of rotated log files using gzip\n * - Maximum file count or age-based retention\n * - Automatic cleanup of old log files\n * - Batch processing of logs for improved performance\n * - Safe handling of process termination signals\n *\n * Each instance must have a unique filename to prevent race conditions.\n * If you need multiple loggers to write to the same file, share the same transport instance between them.\n */\nexport class LogFileRotationTransport extends LoggerlessTransport implements Disposable {\n /** Registry of active filenames to prevent multiple transports writing to the same file */\n private static activeFilenames = new Set<string>();\n /** The current write stream for the log file */\n private stream: WriteStream;\n /** Custom field names for log entries */\n private fieldNames: Required<LogFileRotationFieldNames>;\n /** Delimiter between log entries */\n private delimiter: string;\n /** Function to generate timestamps for log entries */\n private timestampFn: () => string | number;\n /** Custom mapping for log levels */\n private levelMap: LogFileRotationLevelMap;\n /** Whether to compress rotated files */\n private compressOnRotate: boolean;\n /** Whether a file is currently being compressed */\n private isCompressing: boolean;\n /** The base filename pattern for log files */\n private filename: string;\n /** Static data to be included in every log entry */\n private staticData?: (() => Record<string, any>) | Record<string, any>;\n /** Whether batch processing is enabled */\n private batchEnabled: boolean;\n /** Maximum number of log entries to queue before writing */\n private batchSize: number;\n /** Maximum time in milliseconds to wait before writing queued logs */\n private batchTimeout: number;\n /** Queue of log entries waiting to be written */\n private batchQueue: string[];\n /** Timer for batch flush timeout */\n private batchTimer: NodeJS.Timeout | null;\n /** Whether the transport is being disposed */\n private isDisposing: boolean;\n /** Event callbacks for various file stream events */\n private callbacks?: LogFileRotationCallbacks;\n /** Frequency of rotation (daily, hourly, etc.) */\n private frequency?: string;\n /** Whether to enable verbose mode */\n private verbose?: boolean;\n /** Date format for filename patterns */\n private dateFormat?: string;\n /** Size threshold for rotation */\n private size?: string;\n /** Maximum number of log files to keep */\n private maxLogs?: string | number;\n /** Path to the audit file */\n private auditFile?: string;\n /** File extension for log files */\n private extension?: string;\n /** Whether to create a symlink to current log */\n private createSymlink?: boolean;\n /** Name of the symlink file */\n private symlinkName?: string;\n /** Whether to use UTC time in filenames */\n private utc?: boolean;\n /** Hash algorithm for audit file */\n private auditHashType?: \"md5\" | \"sha256\";\n /** Options for file streams */\n private fileOptions?: {\n flags?: string;\n encoding?: string;\n mode?: number;\n };\n /** File mode to be used when creating log files */\n private fileMode?: number;\n\n /**\n * Generates the options for FileStreamRotator consistently across the transport\n * @returns FileStreamRotatorOptions object\n * @private\n */\n private getRotatorOptions(): FileStreamRotatorOptions {\n return {\n filename: this.filename,\n frequency: this.frequency,\n verbose: this.verbose ?? false,\n date_format: this.dateFormat,\n size: this.size,\n max_logs: this.maxLogs?.toString(),\n audit_file: this.auditFile || undefined,\n end_stream: true,\n extension: this.extension,\n create_symlink: this.createSymlink,\n symlink_name: this.symlinkName,\n utc: this.utc,\n audit_hash_type: this.auditHashType,\n file_options: {\n flags: \"a\",\n encoding: \"utf8\",\n mode: this.fileMode ?? 0o640,\n ...this.fileOptions,\n },\n };\n }\n\n /**\n * Creates a new LogFileRotationTransport instance.\n * @param params - Configuration options for the transport\n * @throws {Error} If the filename is already in use by another transport instance\n */\n constructor(params: LogFileRotationTransportConfig) {\n super(params);\n\n // Check if filename is already in use\n if (LogFileRotationTransport.activeFilenames.has(params.filename)) {\n throw new Error(\n `LogFileRotationTransport: Filename \"${params.filename}\" is already in use by another instance. To use the same file for multiple loggers, share the same transport instance between them.`,\n );\n }\n\n // Register the filename\n this.filename = params.filename;\n LogFileRotationTransport.activeFilenames.add(this.filename);\n\n // Set up field names with defaults\n this.fieldNames = {\n level: params.fieldNames?.level ?? \"level\",\n message: params.fieldNames?.message ?? \"message\",\n timestamp: params.fieldNames?.timestamp ?? \"timestamp\",\n };\n\n // Set up delimiter\n this.delimiter = params.delimiter ?? \"\\n\";\n\n // Set up timestamp function\n this.timestampFn = params.timestampFn ?? (() => new Date().toISOString());\n\n // Set up level mapping\n this.levelMap = params.levelMap ?? {};\n\n // Set up compression\n this.compressOnRotate = params.compressOnRotate ?? false;\n this.isCompressing = false;\n\n // Set up batching\n this.batchEnabled = !!params.batch;\n this.batchSize = params.batch?.size ?? 1000;\n this.batchTimeout = params.batch?.timeout ?? 5000;\n this.batchQueue = [];\n this.batchTimer = null;\n this.isDisposing = false;\n\n // Store other options\n this.callbacks = params.callbacks;\n this.frequency = params.frequency;\n this.verbose = params.verbose;\n this.dateFormat = params.dateFormat;\n this.size = params.size;\n this.maxLogs = params.maxLogs;\n this.auditFile = params.auditFile;\n this.extension = params.extension;\n this.createSymlink = params.createSymlink;\n this.symlinkName = params.symlinkName;\n this.utc = params.utc;\n this.auditHashType = params.auditHashType;\n this.fileOptions = params.fileOptions;\n this.fileMode = params.fileMode;\n this.staticData = params.staticData;\n\n // Set up exit handler for flushing\n if (this.batchEnabled) {\n // Handle normal process exit\n process.on(\"beforeExit\", () => {\n if (!this.isDisposing) {\n this.flush();\n }\n });\n\n // Handle SIGINT (Ctrl+C) and SIGTERM\n const handleSignal = (signal: string) => {\n if (!this.isDisposing) {\n // Synchronously flush logs to ensure they're written before exit\n this.flushSync();\n // Remove the filename from registry\n LogFileRotationTransport.activeFilenames.delete(this.filename);\n // Exit with the original signal\n process.exit(signal === \"SIGINT\" ? 130 : 143);\n }\n };\n\n process.on(\"SIGINT\", () => handleSignal(\"SIGINT\"));\n process.on(\"SIGTERM\", () => handleSignal(\"SIGTERM\"));\n }\n\n // Only create the stream if not in batch mode or if we have logs to write\n if (!this.batchEnabled) {\n this.initStream(this.getRotatorOptions());\n }\n }\n\n /**\n * Initializes the write stream and sets up event listeners.\n * This is called either immediately if batching is disabled,\n * or lazily when the first batch needs to be written if batching is enabled.\n * @param options - Options for the file stream rotator\n * @private\n */\n private initStream(options: FileStreamRotatorOptions): void {\n // FileStreamRotator.getStream() returns a WriteStream-compatible object\n this.stream = FileStreamRotator.getStream(options) as unknown as WriteStream;\n\n // Set up event listeners if callbacks are provided\n if (this.callbacks) {\n const { onRotate, onNew, onOpen, onClose, onError, onFinish, onLogRemoved } = this.callbacks;\n\n // Wrap the onRotate callback to handle compression\n if (this.compressOnRotate) {\n this.stream.on(\"rotate\", async (oldFile: string, newFile: string) => {\n try {\n this.isCompressing = true;\n const compressedPath = await this.compressFile(oldFile);\n await unlink(oldFile);\n onRotate?.(compressedPath, newFile);\n } catch (error) {\n this.callbacks?.onError?.(error as Error);\n } finally {\n this.isCompressing = false;\n }\n });\n } else if (onRotate) {\n this.stream.on(\"rotate\", onRotate);\n }\n\n if (onNew) {\n this.stream.on(\"new\", onNew);\n }\n if (onOpen) {\n this.stream.on(\"open\", onOpen);\n }\n if (onClose) {\n this.stream.on(\"close\", onClose);\n }\n if (onError) {\n this.stream.on(\"error\", onError);\n }\n if (onFinish) {\n this.stream.on(\"finish\", onFinish);\n }\n if (onLogRemoved) {\n this.stream.on(\"logRemoved\", onLogRemoved);\n }\n }\n }\n\n /**\n * Generates a unique path for a compressed log file.\n * If a file with .gz extension already exists, appends timestamp and counter.\n * @param filePath - The original log file path\n * @returns The unique path for the compressed file\n * @private\n */\n private async getUniqueCompressedFilePath(filePath: string): Promise<string> {\n let finalPath = `${filePath}.gz`;\n let counter = 0;\n\n try {\n while (true) {\n try {\n await access(finalPath);\n counter++;\n finalPath = `${filePath}.${Date.now()}.${counter}.gz`;\n } catch {\n break;\n }\n }\n } catch (_error) {\n finalPath = `${filePath}.${Date.now()}.gz`;\n }\n\n return finalPath;\n }\n\n /**\n * Compresses a log file using gzip.\n * @param filePath - Path to the file to compress\n * @returns Path to the compressed file\n * @private\n */\n private async compressFile(filePath: string): Promise<string> {\n const gzPath = await this.getUniqueCompressedFilePath(filePath);\n const gzip = createGzip();\n const source = createReadStream(filePath);\n const destination = createWriteStream(gzPath);\n\n await pipeline(source, gzip, destination);\n return gzPath;\n }\n\n /**\n * Flushes queued log entries to disk asynchronously.\n * This is used for normal batch processing operations.\n * @private\n */\n private flush(): void {\n if (!this.batchEnabled || this.batchQueue.length === 0) {\n return;\n }\n\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n\n // Initialize stream if it hasn't been created yet\n if (!this.stream) {\n this.initStream(this.getRotatorOptions());\n }\n\n const batchContent = this.batchQueue.join(\"\");\n this.stream.write(batchContent);\n this.batchQueue = [];\n }\n\n /**\n * Synchronously flush logs to disk.\n * This is used during process termination (SIGINT/SIGTERM) to ensure logs are written\n * before the process exits. This method uses synchronous file I/O to guarantee that\n * logs are written even during abrupt process termination.\n * @private\n */\n private flushSync(): void {\n if (!this.batchEnabled || this.batchQueue.length === 0) {\n return;\n }\n\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n\n // Initialize stream if it hasn't been created yet\n if (!this.stream) {\n this.initStream(this.getRotatorOptions());\n }\n\n const batchContent = this.batchQueue.join(\"\");\n // Use writeFileSync to ensure logs are written before process exit\n const rotator = this.stream as unknown as { currentFile: string };\n if (rotator.currentFile) {\n writeFileSync(rotator.currentFile, batchContent, { flag: \"a\" });\n }\n this.batchQueue = [];\n }\n\n /**\n * Schedules a batch flush operation.\n * This creates a timer that will flush the batch after the configured timeout.\n * The timer is unref'd to prevent keeping the process alive.\n * @private\n */\n private scheduleBatchFlush(): void {\n if (!this.batchTimer && !this.isDisposing) {\n this.batchTimer = setTimeout(() => {\n this.flush();\n }, this.batchTimeout);\n\n // Prevent timer from keeping the process alive\n if (this.batchTimer.unref) {\n this.batchTimer.unref();\n }\n }\n }\n\n /**\n * Processes and writes a log entry.\n * If batching is enabled, the entry is queued and written based on batch settings.\n * If batching is disabled, the entry is written immediately.\n * @param params - The log entry parameters\n * @returns The original messages array\n */\n shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams) {\n const logEntry = {\n [this.fieldNames.level]: this.levelMap[logLevel as keyof LogFileRotationLevelMap] ?? logLevel,\n [this.fieldNames.message]: messages.join(\" \") || \"\",\n [this.fieldNames.timestamp]: this.timestampFn(),\n ...(this.staticData ? (typeof this.staticData === \"function\" ? this.staticData() : this.staticData) : {}),\n ...(hasData ? data : {}),\n };\n\n const logString = `${JSON.stringify(logEntry)}${this.delimiter}`;\n\n if (this.batchEnabled) {\n this.batchQueue.push(logString);\n\n if (this.batchQueue.length >= this.batchSize) {\n this.flush();\n } else {\n this.scheduleBatchFlush();\n }\n } else {\n this.stream.write(logString);\n }\n\n return messages;\n }\n\n /**\n * Disposes of the transport, cleaning up resources and flushing any remaining logs.\n * This method:\n * 1. Prevents new batch flushes from being scheduled\n * 2. Cancels any pending batch flush\n * 3. Flushes any remaining logs\n * 4. Waits for any in-progress compression to complete\n * 5. Closes the write stream\n * 6. Removes the filename from the registry\n */\n [Symbol.dispose](): void {\n if (this.stream || this.batchEnabled) {\n this.isDisposing = true;\n\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n\n // Flush any remaining logs\n if (this.batchEnabled) {\n this.flush();\n }\n\n const checkAndEnd = () => {\n if (!this.isCompressing) {\n if (this.stream) {\n this.stream.end();\n }\n // Remove the filename from registry when disposed\n LogFileRotationTransport.activeFilenames.delete(this.filename);\n } else {\n setTimeout(checkAndEnd, 100);\n }\n };\n checkAndEnd();\n }\n }\n}\n"],"mappings":"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"}