UNPKG

@feflow/cli

Version:
180 lines (164 loc) 4.83 kB
import bunyan from 'bunyan'; import { Writable } from 'stream'; import path from 'path'; import osenv from 'osenv'; import { spawn } from 'child_process'; import chalk from 'chalk'; import { LOG_REPORT_BEAT_GAP, FEFLOW_ROOT, LOG_FILE, HEART_BEAT_COLLECTION_LOG } from '../../shared/constant'; import { getKeyFormFile, setKeyToFile } from '../../shared/file'; import pkgJson from '../../../package.json'; const root = path.join(osenv.home(), FEFLOW_ROOT); const heartDBFile = path.join(root, HEART_BEAT_COLLECTION_LOG); let logReportProcess: ReturnType<typeof spawn> | null = null; const reportLog = path.join(__dirname, './report'); let hasCreateHeart = false; const logReportDbKey = 'log_report_beat_time'; const { debug, silent } = process.env; type LogLevelString = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; type LogLevel = LogLevelString | number; interface Stream { type?: string; level?: LogLevel; path?: string; stream?: ConsoleStream; closeOnExit?: boolean; period?: string; count?: number; name?: string; reEmitErrorEvents?: boolean; } export interface LoggerOptions { name?: string; silent?: boolean; debug?: boolean; } export interface Logger extends bunyan { name?: string; } export default function createLogger(options: LoggerOptions): Logger { const streams: Array<Stream> = []; streams.push({ level: 'error', path: path.join(root, LOG_FILE), }); streams.push({ level: 'info', path: path.join(root, LOG_FILE), }); if (!options.silent) { streams.push({ type: 'raw', level: options.debug ? 'trace' : 'info', stream: new ConsoleStream(options), }); } if (options.debug) { streams.push({ level: 'trace', path: path.join(root, 'debug.log'), }); } return bunyan.createLogger({ name: options.name || 'feflow-cli', streams, serializers: { err: bunyan.stdSerializers.err, }, }); } interface IObject { [key: string]: string; } const levelNames: IObject = { 10: 'Trace', 20: 'Debug', 30: 'Info', 40: 'Warn', 50: 'Error', 60: 'Fatal', }; const levelColors: IObject = { 10: 'gray', 20: 'gray', 30: 'green', 40: 'orange', 50: 'red', 60: 'red', }; class ConsoleStream extends Writable { private debug: Boolean; constructor(args: LoggerOptions) { super({ objectMode: true, }); this.debug = Boolean(args.debug); } // 上报 startReport() { const report = () => { // 子进程执行日志上报 logReportProcess = spawn(process.argv[0], [reportLog], { detached: true, // 使子进程在父进程退出后继续运行 stdio: 'ignore', // 保持后台运行 env: { ...process.env, // env 无法把 ctx 传进去,会自动 string 化 debug, silent, }, windowsHide: true, }); // 父进程不会等待子进程 logReportProcess.unref(); }; let cacheValidate = false; const nowTime = new Date().getTime(); const lastBeatTime = getKeyFormFile(heartDBFile, logReportDbKey); if (lastBeatTime) { // 在一次心跳时间内只允许创建一次子进程 cacheValidate = nowTime - lastBeatTime <= LOG_REPORT_BEAT_GAP; // 防止瞬时大量数据,导致lastBeatTime还未更新时触发多次子进程创建 if (!cacheValidate && !hasCreateHeart) { hasCreateHeart = true; setKeyToFile(heartDBFile, logReportDbKey, String(nowTime)); hasCreateHeart = false; report(); } } else if (!hasCreateHeart) { hasCreateHeart = true; setKeyToFile(heartDBFile, logReportDbKey, String(nowTime)); hasCreateHeart = false; report(); } } // 此方法必须保留,因为继承一个 writable stream 时必须重写其 _write 方法 // eslint-disable-next-line @typescript-eslint/naming-convention async _write(data: any, encoding: string, callback: (error?: Error | null) => void) { const { level } = data; const PLUGIN_NAME = `feflow-${pkgJson.name.split('/').pop()}`; const loggerName = data.name || PLUGIN_NAME; let msg = ''; if (this.debug) { msg += `${chalk.gray(data.time)} `; } msg += chalk.keyword(levelColors[level])(`[ Feflow ${levelNames[level]} ]`); msg += `[ ${loggerName} ] `; msg += `${data.msg}\n`; if (data.err) { const err = data.err.stack || data.err.message; if (err) msg += `${chalk.yellow(err)}\n`; } Object.assign(data, { level, msg: `[Feflow ${levelNames[level]}][${loggerName}]${data.msg}`, date: new Date().getTime(), name: loggerName, }); if (level >= 40) { process.stderr.write(msg); } else { process.stdout.write(msg); } this.startReport(); callback(); } }