UNPKG

@loglayer/transport-log-file-rotation

Version:

Log file rotation transport for the LogLayer logging library.

1 lines 28.7 kB
{"version":3,"file":"index.cjs","names":["LoggerlessTransport","FileStreamRotator"],"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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"}