UNPKG

file-stream-rotator

Version:

Automated stream rotation useful for log files

269 lines (236 loc) 9.64 kB
import { EventEmitter, Stream } from "node:stream"; import * as fs from "fs"; import path = require('path'); import type { FileStreamRotatorOptions, FileStreamRotatorConfig, AuditSettings } from "./types"; import { KeepLogFiles, Frequency } from "./enums"; import DefaultOptions from "./DefaultOptions"; import Rotator from "./Rotator"; import AuditManager from "./AuditManager" import { Logger, makeDirectory } from "./helper"; import { FSWatcher } from "node:fs"; export default class FileStreamRotator extends EventEmitter { static getStream(options: Partial<FileStreamRotatorOptions>): FileStreamRotator { return new FileStreamRotator(options) } private config: FileStreamRotatorConfig = {} private fs?: fs.WriteStream private rotator: Rotator private currentFile?: string private auditManager: AuditManager // private logWatcher?: FSWatcher constructor(options: Partial<FileStreamRotatorOptions>, debug: boolean = false){ super() this.config = this.parseOptions(options) Logger.getInstance(options.verbose, debug) this.auditManager = new AuditManager(this.config.auditSettings ?? DefaultOptions.auditSettings({}), this) let lastEntry = this.auditManager.config.files.slice(-1).shift() this.rotator = new Rotator((this.config.rotationSettings ?? DefaultOptions.rotationSettings({})), lastEntry) this.rotate() // this does not seem to work anymore. // this.on("open", (filename: string) => { // // monitor for file deletion // if (this.config.options?.watch_log) { // console.log(">>> setting up watcher", filename) // this.logWatcher = this.createLogWatcher(filename, this.processWatcherEvents.bind(this)) // } // }) } private parseOptions(options: Partial<FileStreamRotatorOptions>): FileStreamRotatorConfig { let config: FileStreamRotatorConfig = {} config.options = DefaultOptions.fileStreamRotatorOptions(options) config.fileOptions = DefaultOptions.fileOptions(options.file_options ?? {}) let auditSettings: AuditSettings = DefaultOptions.auditSettings({}) if (options.audit_file) { auditSettings.auditFilename = options.audit_file } if (options.audit_hash_type) { auditSettings.hashType = options.audit_hash_type } if (options.extension){ auditSettings.extension = options.extension } if (options.max_logs) { let params = DefaultOptions.extractParam(options.max_logs) auditSettings.keepSettings = { type: params.letter?.toLowerCase() == "d" ? KeepLogFiles.days : KeepLogFiles.fileCount, amount: params.number } } config.auditSettings = auditSettings config.rotationSettings = DefaultOptions.rotationSettings({filename: options.filename, extension: options.extension}) if (options.date_format && !options.frequency){ config.rotationSettings.frequency = Frequency.date } else { config.rotationSettings.frequency = Frequency.none } if (options.date_format) { config.rotationSettings.format = options.date_format } config.rotationSettings.utc = options.utc ?? false switch(options.frequency){ case "daily": config.rotationSettings.frequency = Frequency.daily break case "custom": case "date": config.rotationSettings.frequency = Frequency.date break case "test": config.rotationSettings.frequency = Frequency.minutes config.rotationSettings.amount = 1 break default: if (options.frequency){ let params = DefaultOptions.extractParam(options.frequency) if (params.letter?.match(/^([mh])$/)) { config.rotationSettings.frequency = params.letter == "h" ? Frequency.hours : Frequency.minutes config.rotationSettings.amount = params.number } } } if (options.size) { let params = DefaultOptions.extractParam(options.size) switch(params.letter){ case 'k': config.rotationSettings!.maxSize = params.number*1024 break case 'm': config.rotationSettings!.maxSize = params.number*1024*1024 break case 'g': config.rotationSettings!.maxSize = params.number*1024*1024*1024 break } } this.rotator = new Rotator(config.rotationSettings) let oldFile = this.rotator.getNewFilename() return config } rotate(force: boolean = false) { let oldFile = this.currentFile this.rotator.rotate(force) this.currentFile = this.rotator.getNewFilename() // oldfile same as new file. do nothing if (this.currentFile == oldFile) { return } // close old file and watcher if exists. if (this.fs) { // if (this.logWatcher) { // this.logWatcher.close() // } if(this.config.options?.end_stream === true){ this.fs.end(); }else{ this.fs.destroy(); } } // add old file to audit if (oldFile){ this.auditManager.addLog(oldFile) } this.createNewLog(this.currentFile) this.emit('new', this.currentFile) this.emit('rotate', oldFile, this.currentFile, force) } private createNewLog(filename: string) { // create new directory if required makeDirectory(filename) // add mew file tp audit this.auditManager.addLog(filename) // create new file let streamOptions: any = {} if (this.config.fileOptions) { streamOptions = this.config.fileOptions } this.fs = fs.createWriteStream(filename, streamOptions) // setup dependencies: proxy events, emit events this.bubbleEvents(this.fs, filename) // setup symlink if (this.config.options?.create_symlink){ this.createCurrentSymLink(filename) } } write(str: string, encoding?: BufferEncoding) { if (this.fs) { if(this.rotator.shouldRotate()){ this.rotate() } this.fs.write(str, encoding ?? "utf8") this.rotator.addBytes(Buffer.byteLength(str, encoding)) if (this.rotator.hasMaxSizeReached()){ this.rotate() } } } end(str: string) { if (this.fs){ this.fs.end(str); this.fs = undefined } } private bubbleEvents(emitter: EventEmitter, filename: string) { emitter.on('close',() => { this.emit('close') }) emitter.on('finish',() => { this.emit('finish') }) emitter.on('error',(err) => { this.emit('error',err) }) emitter.on('open',(fd) => { this.emit('open',filename) }) } private createCurrentSymLink(logfile?: string) { if (!logfile) { return } let symLinkName = this.config.options?.symlink_name ?? "current.log" let logPath = path.dirname(logfile) let logfileName = path.basename(logfile) let current = logPath + path.sep + symLinkName try { if (fs.existsSync(current)){ let stats = fs.lstatSync(current) if(stats.isSymbolicLink()){ fs.unlinkSync(current) fs.symlinkSync(logfileName, current) return } Logger.verbose("Could not create symlink file as file with the same name exists: ", current); } else { fs.symlinkSync(logfileName, current) } } catch (err: any) { Logger.verbose("[Could not create symlink file: ", current, ' -> ', logfileName); Logger.debug("error creating sym link", current, err) } } /* // does not seem to work anymore private createLogWatcher(logfile: string, processEvent: ((event: fs.WatchEventType, file: string) => void)): FSWatcher | undefined{ if(!logfile) return try { if (!fs.existsSync(logfile)) { Logger.verbose("Watcher error: file does not exist" + logfile); return } let stats = fs.lstatSync(logfile) return fs.watch(logfile, (event, filename) => { processEvent(event, filename) }) }catch(err){ Logger.verbose("Could not add watcher for " + logfile, err); return } } private processWatcherEvents(event: fs.WatchEventType, filename: string) { if (!this.currentFile) { return } if (filename == this.currentFile){ if (event == "rename") { if (this.logWatcher) { this.logWatcher.close() } this.createNewLog(this.currentFile) } } } */ test(): {config: FileStreamRotatorConfig, rotator: Rotator} { return {config: this.config, rotator: this.rotator} } }