UNPKG

perfect-logger

Version:

A zero-dependency, isomorphic logger for Node.js and Browsers with plugin support.

204 lines (203 loc) 8.54 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileAppender = void 0; const BaseAppender_1 = require("./BaseAppender"); const environment_1 = require("../utils/environment"); const constants_1 = require("../constants"); const safeStringify_1 = require("../utils/safeStringify"); // Node.js modules are conditionally required let fsPromises = null; let fsModule = null; let pathModule = null; if ((0, environment_1.isNode)()) { try { fsModule = require('fs'); fsPromises = require('fs').promises; pathModule = require('path'); } catch (e) { console.error('FileAppender is only available in Node.js environments.'); } } const DEFAULT_FORMAT = '{date} | {time} | {level} | {namespace} | {message}'; const DEFAULT_FILENAME = 'app.log'; class FileAppender extends BaseAppender_1.BaseAppender { constructor(config = {}) { super('FileAppender', config, { minLevel: constants_1.LogLevel.INFO }); this.currentFileSize = 0; this.currentFileDateMarker = null; if (!fsPromises || !fsModule || !pathModule || !process) { throw new Error('FileAppender cannot be used in this environment.'); } this.logDirectory = config.logDirectory || pathModule.join(process.cwd(), 'logs'); this.fileName = config.fileName || DEFAULT_FILENAME; this.formatTemplate = config.format || DEFAULT_FORMAT; this.rotation = config.rotation; this.maxSize = config.maxSize || null; this.maxFiles = config.maxFiles || null; this.dateFormatter = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: this.timezone, }); this.timeFormatter = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false, timeZone: this.timezone, }); this.currentFilePath = this.getCurrentFilename(); this.initializeState(); } async initializeState() { if (!fsModule.existsSync(this.logDirectory)) { fsModule.mkdirSync(this.logDirectory, { recursive: true }); } this.currentFilePath = this.getCurrentFilename(); if (this.rotation) { this.currentFileDateMarker = this.getDateMarker(new Date()); } try { const stats = await fsPromises.stat(this.currentFilePath); this.currentFileSize = stats.size; } catch (e) { this.currentFileSize = 0; } } handle(entry) { this.handleBatch([entry]); } async handleBatch(entries) { if (!fsPromises) return; const logLines = entries .filter(entry => entry.level >= this.minLevel) .map(entry => this.formatLog(entry)) .join('\n'); if (!logLines) { return; } const logBuffer = Buffer.from(logLines + '\n', 'utf-8'); try { await this.checkForRotation(logBuffer.length); await fsPromises.appendFile(this.currentFilePath, logBuffer); this.currentFileSize += logBuffer.length; } catch (e) { console.error('Error writing to log file:', e); } } async checkForRotation(bytesToAdd) { const timeBoundaryReached = this.rotation && this.getDateMarker(new Date()) !== this.currentFileDateMarker; const sizeBoundaryReached = this.maxSize !== null && (this.currentFileSize + bytesToAdd > this.maxSize); if (timeBoundaryReached || sizeBoundaryReached) { await this.rotate(timeBoundaryReached); } } async rotate(timeBased) { if (!pathModule) return; const oldPath = this.currentFilePath; // Determine the new path for the current log file if (timeBased) { this.currentFileDateMarker = this.getDateMarker(new Date()); } this.currentFilePath = this.getCurrentFilename(); this.currentFileSize = 0; // Archive the old file try { // If file doesn't exist, no need to rotate it. await fsPromises.access(oldPath); } catch { return; } const ext = pathModule.extname(this.fileName); const baseName = pathModule.basename(this.fileName, ext); const archives = await this.getArchives(baseName); const archiveName = timeBased ? `${baseName}-${this.getDateMarker(new Date(Date.now() - 1))}${ext}` // Use previous date marker : `${baseName}.${archives.length + 1}${ext}`; const archivePath = pathModule.join(this.logDirectory, archiveName); try { await fsPromises.rename(oldPath, archivePath); } catch (e) { console.error(`Failed to rotate log file from ${oldPath} to ${archivePath}`, e); return; } // Prune await this.prune([archivePath, ...archives]); } async prune(archives) { if (!this.maxFiles || !pathModule) return; const filesToProcess = archives.sort().reverse(); // Newest first // Delete oldest files if (filesToProcess.length > this.maxFiles) { const filesToDelete = filesToProcess.slice(this.maxFiles); for (const file of filesToDelete) { try { await fsPromises.unlink(file); } catch (e) { console.error(`Failed to delete old log file: ${file}`, e); } } } } async getArchives(baseName) { if (!pathModule) return []; const files = await fsPromises.readdir(this.logDirectory); const ext = pathModule.extname(this.fileName); const regex = new RegExp(`^${baseName}[-.]`); return files .filter(f => f.startsWith(baseName) && f !== this.fileName && regex.test(f)) .map(f => pathModule.join(this.logDirectory, f)); } getCurrentFilename() { if (!pathModule) return ''; if (this.rotation && this.maxSize === null) { // Purely time-based return pathModule.join(this.logDirectory, `${pathModule.basename(this.fileName, pathModule.extname(this.fileName))}-${this.getDateMarker(new Date())}${pathModule.extname(this.fileName)}`); } return pathModule.join(this.logDirectory, this.fileName); } getDateMarker(date) { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); if (this.rotation === 'hourly') { const hour = date.getHours().toString().padStart(2, '0'); return `${year}-${month}-${day}T${hour}`; } return `${year}-${month}-${day}`; } formatLog(entry) { var _a, _b, _c; const parts = this.dateFormatter.formatToParts(entry.timestamp); const year = (_a = parts.find(p => p.type === 'year')) === null || _a === void 0 ? void 0 : _a.value; const month = (_b = parts.find(p => p.type === 'month')) === null || _b === void 0 ? void 0 : _b.value; const day = (_c = parts.find(p => p.type === 'day')) === null || _c === void 0 ? void 0 : _c.value; const date = `${year}/${month}/${day}`; const baseTime = this.timeFormatter.format(entry.timestamp); const milliseconds = entry.timestamp.getMilliseconds().toString().padStart(3, '0'); const time = `${baseTime}.${milliseconds}`; const level = (constants_1.LogLevel[entry.level] || 'UNKNOWN'); let logLine = this.formatTemplate .replace('{date}', date) .replace('{time}', time) .replace('{level}', level) .replace('{namespace}', entry.namespace) .replace('{message}', entry.message) .replace('{context}', '') .replace('{error}', ''); if (entry.context) { const prettyContext = (0, safeStringify_1.safeStringify)(entry.context, null, 4); const indentedContext = prettyContext.split('\n').map(line => `~ ${line}`).join('\n'); logLine += `\n${indentedContext}`; } if (entry.error) { logLine += `\n${entry.error.stack || entry.error.message}`; } return logLine; } } exports.FileAppender = FileAppender;