// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. const { open, openSync, close, renameSync, stat } = Deno; type File = Deno.File; type Writer = Deno.Writer; type OpenOptions = Deno.OpenOptions; import { getLevelByName, LevelName, LogLevels } from "./levels.ts"; import { LogRecord } from "./logger.ts"; import { red, yellow, blue, bold } from "../fmt/colors.ts"; import { existsSync, exists } from "../fs/exists.ts"; const DEFAULT_FORMATTER = "{levelName} {msg}"; type FormatterFunction = (logRecord: LogRecord) => string; type LogMode = "a" | "w" | "x"; interface HandlerOptions { formatter?: string | FormatterFunction; } export class BaseHandler { level: number; levelName: LevelName; formatter: string | FormatterFunction; constructor(levelName: LevelName, options: HandlerOptions = {}) { this.level = getLevelByName(levelName); this.levelName = levelName; this.formatter = options.formatter || DEFAULT_FORMATTER; } handle(logRecord: LogRecord): void { if (this.level > logRecord.level) return; const msg = this.format(logRecord); return this.log(msg); } format(logRecord: LogRecord): string { if (this.formatter instanceof Function) { return this.formatter(logRecord); } return this.formatter.replace(/{(\S+)}/g, (match, p1): string => { const value = logRecord[p1 as keyof LogRecord]; // do not interpolate missing values if (!value) { return match; } return String(value); }); } log(_msg: string): void {} async setup(): Promise {} async destroy(): Promise {} } export class ConsoleHandler extends BaseHandler { format(logRecord: LogRecord): string { let msg = super.format(logRecord); switch (logRecord.level) { case LogLevels.INFO: msg = blue(msg); break; case LogLevels.WARNING: msg = yellow(msg); break; case LogLevels.ERROR: msg = red(msg); break; case LogLevels.CRITICAL: msg = bold(red(msg)); break; default: break; } return msg; } log(msg: string): void { console.log(msg); } } export abstract class WriterHandler extends BaseHandler { protected _writer!: Writer; #encoder = new TextEncoder(); abstract log(msg: string): void; } interface FileHandlerOptions extends HandlerOptions { filename: string; mode?: LogMode; } export class FileHandler extends WriterHandler { protected _file!: File; protected _filename: string; protected _mode: LogMode; protected _openOptions: OpenOptions; #encoder = new TextEncoder(); constructor(levelName: LevelName, options: FileHandlerOptions) { super(levelName, options); this._filename = options.filename; // default to append mode, write only this._mode = options.mode ? options.mode : "a"; this._openOptions = { createNew: this._mode === "x", create: this._mode !== "x", append: this._mode === "a", truncate: this._mode !== "a", write: true, }; } async setup(): Promise { this._file = await open(this._filename, this._openOptions); this._writer = this._file; } log(msg: string): void { Deno.writeAllSync(this._file, this.#encoder.encode(msg + "\n")); } destroy(): Promise { this._file.close(); return Promise.resolve(); } } interface RotatingFileHandlerOptions extends FileHandlerOptions { maxBytes: number; maxBackupCount: number; } export class RotatingFileHandler extends FileHandler { #maxBytes: number; #maxBackupCount: number; #currentFileSize = 0; #encoder = new TextEncoder(); constructor(levelName: LevelName, options: RotatingFileHandlerOptions) { super(levelName, options); this.#maxBytes = options.maxBytes; this.#maxBackupCount = options.maxBackupCount; } async setup(): Promise { if (this.#maxBytes < 1) { throw new Error("maxBytes cannot be less than 1"); } if (this.#maxBackupCount < 1) { throw new Error("maxBackupCount cannot be less than 1"); } await super.setup(); if (this._mode === "w") { // Remove old backups too as it doesn't make sense to start with a clean // log file, but old backups for (let i = 1; i <= this.#maxBackupCount; i++) { if (await exists(this._filename + "." + i)) { await Deno.remove(this._filename + "." + i); } } } else if (this._mode === "x") { // Throw if any backups also exist for (let i = 1; i <= this.#maxBackupCount; i++) { if (await exists(this._filename + "." + i)) { Deno.close(this._file.rid); throw new Deno.errors.AlreadyExists( "Backup log file " + this._filename + "." + i + " already exists" ); } } } else { this.#currentFileSize = (await stat(this._filename)).size; } } handle(logRecord: LogRecord): void { if (this.level > logRecord.level) return; const msg = this.format(logRecord); const msgByteLength = this.#encoder.encode(msg).byteLength + 1; if (this.#currentFileSize + msgByteLength > this.#maxBytes) { this.rotateLogFiles(); this.#currentFileSize = msgByteLength; } else { this.#currentFileSize += msgByteLength; } return this.log(msg); } rotateLogFiles(): void { close(this._file.rid); for (let i = this.#maxBackupCount - 1; i >= 0; i--) { const source = this._filename + (i === 0 ? "" : "." + i); const dest = this._filename + "." + (i + 1); if (existsSync(source)) { renameSync(source, dest); } } this._file = openSync(this._filename, this._openOptions); this._writer = this._file; } }