UNPKG

@nivinjoseph/n-log

Version:
150 lines 5.84 kB
import { given } from "@nivinjoseph/n-defensive"; 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 { LogPrefix } from "./log-prefix.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 { _mutex = new Mutex(); _logDirPath; _retentionDays; _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) */ constructor(config) { 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 */ async logDebug(debug) { 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 */ async logInfo(info) { 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 */ async logWarning(warning) { 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 */ async logError(error) { 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 */ async _writeToLog(status, message) { 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 = { 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 */ async _purgeLogs() { const now = Date.now(); if (this._lastPurgedAt && this._lastPurgedAt > (now - Duration.fromDays(this._retentionDays).toMilliSeconds())) return; const files = await Make.callbackToPromise(Fs.readdir)(this._logDirPath); await files.forEachAsync(async (file) => { const filePath = Path.join(this._logDirPath, file); const stats = await Make.callbackToPromise(Fs.stat)(filePath); if (stats.isFile() && stats.birthtimeMs < (now - Duration.fromDays(this._retentionDays).toMilliSeconds())) await Make.callbackToPromise(Fs.unlink)(filePath); }, 1); this._lastPurgedAt = now; } } //# sourceMappingURL=file-logger.js.map