UNPKG

@nivinjoseph/n-log

Version:
193 lines (167 loc) 6.43 kB
import { given } from "@nivinjoseph/n-defensive"; import { Exception } from "@nivinjoseph/n-exception"; import "@nivinjoseph/n-ext"; import { Duration, Make, Mutex } from "@nivinjoseph/n-util"; import Fs from "node:fs"; import Path from "node:path"; import { BaseLogger } from "./base-logger.js"; import { FileLoggerConfig } from "./file-logger-config.js"; import { LogPrefix } from "./log-prefix.js"; import { LogRecord } from "./log-record.js"; /** * Logger implementation that writes logs to files. * Features: * - Logs are written to files named by hour (YYYY-MM-DD-HH.log) * - Supports both plain text and JSON formatting * - Automatic log file rotation * - Configurable log retention period * - Thread-safe writing using mutex * - Debug logs only written in development environment */ export class FileLogger extends BaseLogger { private readonly _mutex = new Mutex(); private readonly _logDirPath: string; private readonly _retentionDays: number; private _lastPurgedAt = 0; /** * Creates a new instance of FileLogger * @param config - Configuration for the file logger * @param config.logDirPath - Absolute path to the directory where log files will be stored * @param config.retentionDays - Number of days to retain log files before automatic deletion * @param config.logDateTimeZone - Timezone for log timestamps (default: UTC) * @param config.useJsonFormat - Whether to format logs as JSON (default: false) */ public constructor(config: FileLoggerConfig) { super(config); const { logDirPath, retentionDays } = config; given(logDirPath, "logDirPath").ensureHasValue().ensureIsString() .ensure(t => Path.isAbsolute(t), "must be absolute"); given(retentionDays, "retentionDays").ensureHasValue().ensureIsNumber().ensure(t => t > 0); this._retentionDays = Number.parseInt(retentionDays.toString()); if (!Fs.existsSync(logDirPath)) Fs.mkdirSync(logDirPath); this._logDirPath = logDirPath; } /** * Logs a debug message to a file. * Only writes in development environment. * @param debug - The debug message to log * @returns A promise that resolves when the log is written */ public async logDebug(debug: string): Promise<void> { if (this.env === "dev") await this._writeToLog(LogPrefix.debug, debug); } /** * Logs an informational message to a file * @param info - The informational message to log * @returns A promise that resolves when the log is written */ public async logInfo(info: string): Promise<void> { await this._writeToLog(LogPrefix.info, info); } /** * Logs a warning message or exception to a file * @param warning - The warning message or exception to log * @returns A promise that resolves when the log is written */ public async logWarning(warning: string | Exception): Promise<void> { await this._writeToLog(LogPrefix.warning, this.getErrorMessage(warning)); } /** * Logs an error message or exception to a file * @param error - The error message or exception to log * @returns A promise that resolves when the log is written */ public async logError(error: string | Exception): Promise<void> { await this._writeToLog(LogPrefix.error, this.getErrorMessage(error)); } /** * Writes a log message to the appropriate log file * @param status - The log level/status * @param message - The message to log * @returns A promise that resolves when the log is written */ private async _writeToLog(status: LogPrefix, message: string): Promise<void> { given(status, "status").ensureHasValue().ensureIsEnum(LogPrefix); given(message, "message").ensureHasValue().ensureIsString(); const dateTimeValue = this.getDateTime(); if (this.useJsonFormat) { let level = ""; switch (status) { case LogPrefix.debug: level = "Debug"; break; case LogPrefix.info: level = "Info"; break; case LogPrefix.warning: level = "Warn"; break; case LogPrefix.error: level = "Error"; break; } let log: LogRecord = { source: this.source, service: this.service, env: this.env, level: level, message, ...dateTimeValue }; this.injectTrace(log, level === "Error"); if (this.logInjector) log = this.logInjector(log); message = JSON.stringify(log); } else { message = `${dateTimeValue.dateTime} ${status} ${message}`; } const logFileName = `${dateTimeValue.dateTime.substr(0, 13)}.log`; const logFilePath = Path.join(this._logDirPath, logFileName); await this._mutex.lock(); try { await Fs.promises.appendFile(logFilePath, `\n${message}`); await this._purgeLogs(); } catch (error) { console.error(error); } finally { this._mutex.release(); } } /** * Purges log files older than the retention period * @returns A promise that resolves when the purge is complete */ private async _purgeLogs(): Promise<void> { const now = Date.now(); if (this._lastPurgedAt && this._lastPurgedAt > (now - Duration.fromDays(this._retentionDays).toMilliSeconds())) return; const files = await Make.callbackToPromise<ReadonlyArray<string>>(Fs.readdir)(this._logDirPath); await files.forEachAsync(async (file) => { const filePath = Path.join(this._logDirPath, file); const stats = await Make.callbackToPromise<Fs.Stats>(Fs.stat)(filePath); if (stats.isFile() && stats.birthtimeMs < (now - Duration.fromDays(this._retentionDays).toMilliSeconds())) await Make.callbackToPromise(Fs.unlink)(filePath); }, 1); this._lastPurgedAt = now; } }