import path from 'path'; import { Readable } from 'stream'; import * as fs from 'fs-extra'; import { injectable } from 'inversify'; import { appPath } from '../../core/app-path'; import { isDev } from '../../core/env'; import { LogLevel } from './log-level'; const loggingDir = path.resolve(appPath, 'logs'); const logFile = path.resolve(loggingDir, 'default.log'); const exceptionLogFile = path.resolve(loggingDir, 'exception.log'); const maxLogSize = 50000; /** * A logger not using winston to log to files in the appPath. */ @injectable() export class Logger implements LoggerInterface { public constructor() { fs.createFileSync(logFile); fs.createFileSync(exceptionLogFile); } private static writeStream(stream: NodeJS.ReadableStream, filePath: string): Promise { return new Promise((resolve) => { let buffer = fs.readFileSync(filePath, { flag: 'r', }); stream.on('data', (chunk) => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); }); stream.on('end', () => { const diff = buffer.byteLength - maxLogSize; if (diff > 0) { buffer = buffer.slice(diff); const firstLineBreakIndex = buffer.findIndex((value) => String.fromCharCode(value) === '\n'); buffer = buffer.slice(firstLineBreakIndex + 1); } fs.writeFileSync(filePath, buffer); resolve(); }); }); } private static writeLine(line: string): Promise { return Logger.writeStream(Readable.from(`${line}\n`), logFile); } private static formatLine(level: LogLevel, message: string): string { return `[${new Date().toISOString()}] ${LogLevel[level]}: ${message}`; } public getLogFile(): string { return logFile; } public getExceptionsLogFile(): string { return exceptionLogFile; } public exception(error: Error): Promise { return Logger.writeStream( Readable.from( `[${new Date().toISOString()}] ${(error as NodeJS.ErrnoException).code ?? 'Error'}: ${error.message} ${error.stack ?? 'no stack trace'}\n` ), exceptionLogFile ); } public log(level: LogLevel, message: string): Promise { if (!isDev() && level === LogLevel.debug) { return Promise.resolve(); } return Logger.writeLine(Logger.formatLine(level, message)).catch((error) => { // eslint-disable-next-line no-console -- I don't want logging to be a source of a fatal error, but i want to log it somewhere console.error(error); }); } public fatal(message: string): Promise { return this.log(LogLevel.fatal, message); } public error(message: string): Promise { return this.log(LogLevel.error, message); } public warning(message: string): Promise { return this.log(LogLevel.warning, message); } public notice(message: string): Promise { return this.log(LogLevel.notice, message); } public info(message: string): Promise { return this.log(LogLevel.info, message); } public debug(message: string): Promise { return this.log(LogLevel.debug, message); } }