UNPKG

koatty_logger

Version:
727 lines (719 loc) 20.5 kB
/*! * @Author: richen * @Date: 2025-06-02 17:42:08 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ 'use strict'; var helper = require('koatty_lib'); var util = require('util'); var winston = require('winston'); var path = require('path'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var helper__namespace = /*#__PURE__*/_interopNamespaceDefault(helper); /* * @Description: * @Usage: * @Author: richen * @Date: 2021-11-04 20:31:43 * @LastEditTime: 2023-01-08 14:50:16 */ /** * ShieldField * * @export * @param {string} str * @returns {*} {ShieldFieldRes} */ function ShieldField(str) { const strArr = Object.assign([], str); const l = strArr.length; let start, end, res; if (l <= 1) { start = "*"; end = ""; res = "*"; } else if (l == 2) { start = strArr.slice(1).join(""); end = "*"; res = `${start}${end}`; } else { let num = Math.floor(l / 3); const mo = Math.floor(l % 3); let startNum = num; if (mo > 0) { num = num + 1; } if (startNum > 4) { num = num + (startNum - 4); startNum = 4; } const endNum = l - num - startNum; if (endNum > 4) { num = num + (endNum - 4); // endNum = 4; } // console.log(startNum, num, endNum) start = strArr.slice(0, startNum).join(""); end = strArr.slice(num + startNum).join(""); res = `${start}${"*".repeat(num)}${end}`; } return { res, start, end }; } /** * ShieldLog * * @export * @param {*} splat * @param {Set<string>} fields * @param {string} [keyName] * @param {number} [depth=0] 递归深度 * @param {number} [maxDepth=10] 最大递归深度 * @returns {*} {*} */ function ShieldLog(splat, fields, keyName, depth = 0, maxDepth = 10) { if (fields.size === 0) { return splat; } if (!splat) return splat; // 防止过深递归 if (depth > maxDepth) { return '[Object: too deep]'; } if (Array.isArray(splat)) { // 使用map代替for循环,更简洁高效 return splat.map(item => ShieldLog(item, fields, undefined, depth + 1, maxDepth)); } if (helper__namespace.isError(splat)) { return splat.message; } if (typeof splat !== "object") { if (fields.has(keyName || "")) { return ShieldField(splat).res; } return `${splat}`; } // 优化对象克隆:使用Object.create保持原型链,避免constructor调用 const cloneSplat = Object.create(Object.getPrototypeOf(splat)); // 使用Object.keys代替for...in,避免原型链属性 for (const key of Object.keys(splat)) { // 递归拷贝 cloneSplat[key] = ShieldLog(splat[key], fields, key, depth + 1, maxDepth); } return cloneSplat; } /* * @Author: richen * @Date: 2020-11-20 17:40:48 * @LastEditors: Please set LastEditors * @LastEditTime: 2024-10-31 16:30:52 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> */ const DailyRotateFile = helper__namespace.safeRequire("winston-daily-rotate-file"); const { combine, timestamp, printf } = winston.format; const LogLevelObj = { "debug": 7, "info": 6, "warning": 4, "error": 3 }; // defaultLoggerOpt const defaultLoggerOpt = { File: { level: "info", filename: "./logs/log.log", handleExceptions: true, json: true, datePattern: 'YYYY-MM-DD-HH', // zippedArchive: true, maxSize: '20m', // maxFiles: '7d', colorize: false, timestamp: true }, Console: { level: "debug", handleExceptions: true, json: true, colorize: true, timestamp: true } }; /** * Logger * * @class Logger */ class Logger { // 日志级别 logLevel = "debug"; // 默认打开日志 enableLog = true; // 日志对象 emptyObj = {}; logger; transports = {}; // 文件日志 logFilePath = ""; // 脱敏字段 sensFields = new Set(); // 基础日志目录,用于安全验证 baseLogDir = path.resolve(process.cwd(), "logs"); // 批量写入相关属性 batchConfig = { enabled: false, maxSize: 100, flushInterval: 1000, // 1秒 maxWaitTime: 5000 // 5秒 }; logBuffer = []; flushTimer = null; lastFlushTime = Date.now(); isDestroyed = false; /** * Creates an instance of Logger. * @param {LoggerOpt} [opt] * @memberof Logger */ constructor(opt) { const level = (process.env.LOGS_LEVEL || "").toLowerCase(); if (level && LogLevelObj[level]) { this.logLevel = level; } if (process.env.LOGS_PATH) { this.logFilePath = process.env.LOGS_PATH; } if (!helper__namespace.isTrueEmpty(opt) && opt) { this.logLevel = opt.logLevel ?? this.logLevel; this.logFilePath = opt.logFilePath ?? this.logFilePath; this.sensFields = opt.sensFields ?? this.sensFields; // 配置批量写入 if (opt.batchConfig) { this.batchConfig = { ...this.batchConfig, ...opt.batchConfig }; } } this.logger = this.createLogger(); // 如果启用批量写入,开始定时器 if (this.batchConfig.enabled) { this.startBatchTimer(); } } /** * enable */ enable(b = true) { this.enableLog = b; } /** * getLevel */ getLevel() { return this.logLevel; } /** * setLevel */ setLevel(level) { this.logLevel = level; if (this.transports.Console) { this.transports.Console.level = level; } if (this.transports.File) { this.transports.File.level = level; } } /** * getLogFilePath */ getLogFilePath() { return this.logFilePath; } /** * setLogFile */ setLogFilePath(f) { // 验证路径安全性 const safePath = this.validateLogPath(f); this.logFilePath = safePath; this.logger.close(); this.logger = this.createLogger(); } /** * getSensFields */ getSensFields() { return this.sensFields; } /** * setSensFields */ setSensFields(fields) { this.sensFields = new Set([...this.sensFields, ...fields]); } /** * clearSensFields - 清理敏感字段,防止内存泄漏 */ clearSensFields() { this.sensFields.clear(); } /** * resetSensFields - 重置敏感字段为指定列表 */ resetSensFields(fields) { this.sensFields.clear(); this.sensFields = new Set(fields); } /** * destroy - 销毁Logger实例,释放资源 */ destroy() { try { this.isDestroyed = true; // 停止批量写入定时器 this.stopBatchTimer(); // 异步刷新缓冲区,但不等待完成(避免阻塞销毁流程) if (this.logBuffer.length > 0) { this.flushBatch().catch(e => { console.error('Error flushing logs during destroy:', e); }); } // 关闭winston logger if (this.logger) { this.logger.close(); } // 清理内存引用 this.sensFields.clear(); this.transports = {}; this.enableLog = false; this.logBuffer = []; } catch (e) { console.error('Error destroying logger:', e); } } /** * enableBatch - 启用/禁用批量写入 */ enableBatch(enabled = true) { if (enabled && !this.batchConfig.enabled) { this.batchConfig.enabled = true; this.startBatchTimer(); } else if (!enabled && this.batchConfig.enabled) { this.batchConfig.enabled = false; this.stopBatchTimer(); // 异步刷新剩余的日志 this.flushBatch().catch(e => { console.error('Error flushing logs when disabling batch:', e); }); } } /** * setBatchConfig - 设置批量写入配置 */ setBatchConfig(config) { const wasEnabled = this.batchConfig.enabled; this.batchConfig = { ...this.batchConfig, ...config }; // 如果配置改变,重新启动定时器 if (this.batchConfig.enabled && wasEnabled) { this.stopBatchTimer(); this.startBatchTimer(); } else if (this.batchConfig.enabled && !wasEnabled) { this.startBatchTimer(); } else if (!this.batchConfig.enabled && wasEnabled) { this.stopBatchTimer(); this.flushBatch().catch(e => { console.error('Error flushing logs when disabling batch via config:', e); }); } } /** * getBatchConfig - 获取批量写入配置 */ getBatchConfig() { return { ...this.batchConfig }; } /** * getBatchStatus - 获取批量写入状态 */ getBatchStatus() { return { enabled: this.batchConfig.enabled || false, bufferSize: this.logBuffer.length, maxSize: this.batchConfig.maxSize, timeSinceLastFlush: Date.now() - this.lastFlushTime }; } /** * flushBatch - 立即刷新批量写入缓冲区 */ async flushBatch() { if (this.logBuffer.length === 0 || this.isDestroyed) { return; } const logsToFlush = [...this.logBuffer]; this.logBuffer = []; this.lastFlushTime = Date.now(); // 异步批量处理日志 - 避免阻塞主线程 return new Promise((resolve) => { setImmediate(() => { try { // 批量写入所有日志 logsToFlush.forEach(entry => { this.writeLogEntry(entry); }); resolve(); } catch (e) { console.error('Error in batch flush:', e); resolve(); // 即使出错也要resolve,避免阻塞 } }); }); } /** * startBatchTimer - 启动批量写入定时器 */ startBatchTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); } this.flushTimer = setInterval(() => { const now = Date.now(); const timeSinceLastFlush = now - this.lastFlushTime; // 检查是否需要刷新(超时或缓冲区满) if (this.logBuffer.length > 0 && (timeSinceLastFlush >= this.batchConfig.maxWaitTime || this.logBuffer.length >= this.batchConfig.maxSize)) { // 异步刷新,不阻塞定时器 this.flushBatch().catch(e => { console.error('Error in timer flush:', e); }); } }, this.batchConfig.flushInterval); } /** * stopBatchTimer - 停止批量写入定时器 */ stopBatchTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } } /** * addToBuffer - 添加日志到缓冲区 */ addToBuffer(level, name, args) { if (this.isDestroyed) { return; } const logEntry = { level, name, args, timestamp: Date.now() }; this.logBuffer.push(logEntry); // 如果缓冲区达到最大大小,立即异步刷新 if (this.logBuffer.length >= this.batchConfig.maxSize) { this.flushBatch().catch(e => { console.error('Error in immediate flush:', e); }); } } /** * writeLogEntry - 写入单个日志条目 */ writeLogEntry(entry) { try { const { level, name, args } = entry; const logName = name !== '' ? name.toUpperCase() : level.toUpperCase(); // 对输入参数进行安全过滤 const sanitizedArgs = args.map(arg => this.sanitizeInput(arg)); // format sanitizedArgs.unshift(logName); // 批量写入时直接调用winston(在批量刷新时已经是异步的) this.logger[level](sanitizedArgs); } catch (e) { console.error('Error writing log entry:', e); } } /** * sanitizeInput - 过滤危险字符,防止日志注入 */ sanitizeInput(input) { if (typeof input === 'string') { // 移除控制字符和换行符,防止日志注入 return input.replace(/[\r\n\t\x00-\x1f\x7f]/g, ' '); } return input; } /** * validateLogPath - 验证日志路径安全性 */ validateLogPath(logPath) { if (!logPath) { throw new Error('Log path cannot be empty'); } // 规范化路径 const normalizedPath = path.normalize(logPath); // 如果是相对路径,基于baseLogDir解析 const resolvedPath = path.isAbsolute(normalizedPath) ? normalizedPath : path.resolve(this.baseLogDir, normalizedPath); // 确保路径在允许的目录内 if (!resolvedPath.startsWith(this.baseLogDir)) { throw new Error(`Log path must be within ${this.baseLogDir}`); } // 过滤危险字符 if (/[<>:"|?*\x00-\x1f]/.test(normalizedPath)) { throw new Error('Log path contains invalid characters'); } return resolvedPath; } /** * log Debug * * @returns {*} * @memberof Logger */ Debug(...args) { return this.printLog("debug", "", args); } /** * debug */ debug(...args) { return this.printLog("debug", "", args); } /** * log Info * * @returns {*} * @memberof Logger */ Info(...args) { return this.printLog("info", "", args); } /** * info */ info(...args) { return this.printLog("info", "", args); } /** * log Warn * * @returns {*} * @memberof Logger */ Warn(...args) { return this.printLog("warning", "", args); } /** * warn */ warn(...args) { return this.printLog("warning", "", args); } /** * log Error * * @returns {*} * @memberof Logger */ Error(...args) { return this.printLog("error", "", args); } /** * error */ error(...args) { return this.printLog("error", "", args); } /** * log Log * * Logger.Log('msg') * * Logger.Log('name', 'msg') * * Logger.Log('name', 'msg1', 'msg2'...) * * @param {...any[]} args * @returns {*} * @memberof Logger */ Log(name, ...args) { // tslint:disable-next-line: one-variable-per-declaration let level = "info"; if (LogLevelObj[name]) { level = name; name = ""; } return this.printLog(level, name, args); } /** * log */ log(name, ...args) { let level = "info"; if (LogLevelObj[name]) { level = name; name = ""; } return this.printLog(level, name, args); } /** * print console * * @private * @param {LogLevelType} level * @param {string} name * @param {any[]|string} args * @memberof Logger */ printLog(level, name, args) { try { if (!this.enableLog || this.isDestroyed) { return; } // 如果启用批量写入,添加到缓冲区 if (this.batchConfig.enabled) { this.addToBuffer(level, name, args); } else { // 默认异步写入 this.writeLogAsync(level, name, args); } } catch (e) { console.error('Error in printLog:', e); } } /** * writeLogAsync - 异步写入单个日志条目 */ async writeLogAsync(level, name, args) { try { const logName = name !== '' ? name.toUpperCase() : level.toUpperCase(); // 对输入参数进行安全过滤 const sanitizedArgs = args.map(arg => this.sanitizeInput(arg)); // format sanitizedArgs.unshift(logName); // Winston的日志方法本身就是异步的,我们使用Promise.resolve确保异步执行 return new Promise((resolve, reject) => { try { // 使用setImmediate确保异步执行,避免阻塞主线程 setImmediate(() => { try { this.logger[level](sanitizedArgs); resolve(); } catch (error) { reject(error); } }); } catch (error) { reject(error); } }).catch(e => { console.error('Error in async log write:', e); }); } catch (e) { console.error('Error in writeLogAsync:', e); } } /** * 格式化 * * @private * @param {string} level * @param {string} label * @param {string} timestamp * @param {any[]|string} args * @returns {string} * @memberof Logger */ format(level, label, timestamp, args) { try { label = label ? `[${label}]` : ''; const params = [`[${timestamp}]`, label, ...ShieldLog(args, this.sensFields)]; // if (level === "debug") { // Error.captureStackTrace(this.emptyObj); // const matchResult = (this.emptyObj.stack.slice(this.emptyObj.stack.lastIndexOf("koatty_logger"))).match(/\(.*?\)/g) || []; // params.push(matchResult.join(" ")); // } return util.format.apply(null, params); } catch (e) { // console.error(e.stack); this.logger.error(e.stack); return ""; } } /** * createLogger * @returns */ createLogger() { const trans = []; if (this.logFilePath != "") { defaultLoggerOpt.File.level = this.logLevel; defaultLoggerOpt.File.filename = `${(this.logFilePath || './logs/')}/log-%DATE%.log`; this.transports.File = new DailyRotateFile(defaultLoggerOpt.File); trans.push(this.transports.File); } else { defaultLoggerOpt.Console.level = this.logLevel; this.transports.Console = new winston.transports.Console(defaultLoggerOpt.Console); trans.push(this.transports.Console); } return winston.createLogger({ levels: LogLevelObj, transports: trans, format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS Z", }), winston.format.json(), printf(({ level, message, label, timestamp }) => { return this.format(level, label, timestamp, message); })), }); } } /* * @Description: * @Usage: * @Author: richen * @Date: 2021-12-18 20:03:31 * @LastEditTime: 2024-10-31 16:32:08 */ //DefaultLogger const DefaultLogger = new Logger(); exports.DefaultLogger = DefaultLogger; exports.Logger = Logger;