import * as fs from 'fs'; import * as path from 'path'; import {Readable, Writable} from 'stream'; import * as chalk from 'chalk'; type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'NEVER'; export interface LogEntry { level: LogLevel; message: string; stacktrace?: string; } export class DevLogger { public readonly logFile: string; private fileStream: fs.WriteStream; private logs: Readable; constructor() { const logsDir = path.resolve(fs.realpathSync(process.cwd()), 'logs/'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir); } this.logFile = path.resolve(fs.realpathSync(process.cwd()), 'logs/dev.log'); this.logs = new Readable(); this.logs._read = () => {/* no op */}; this.fileStream = fs.createWriteStream(this.logFile, {encoding: 'utf8', flags: 'a+'}); const consoleWriter = new Writable({ write: (chunk, _encoding, next) => { process.stdout.write(this.decorateLog(chunk.toString())); next(); } }); this.logs.pipe(consoleWriter); this.logs.pipe(this.fileStream); } public log = (line: string) => { let json: any; try { json = JSON.parse(line); } catch (e) { console.log(line); return; } const level = json.level; const message = json.message; const stacktrace = json.stacktrace; if (level === 'NEVER') { return; } this.logs.push(`[${level}] ${message}${message.endsWith('\n') ? '' : '\n'}`); if (stacktrace !== undefined) { this.logs.push(`${stacktrace}${stacktrace.endsWith('\n') ? '' : '\n'}`); } } private decorateLog(message: string): string { if (message.startsWith('[warn]')) { return message.replace('[warn]', chalk.yellow('[warn]')); } if (message.startsWith('[error]')) { return message.replace('[error]', chalk.red('[error]')); } return message; } }