UNPKG

koatty_logger

Version:
782 lines (778 loc) 19.3 kB
import * as helper from 'koatty_lib'; import util from 'util'; import { format, transports, createLogger } from 'winston'; import DailyRotateFile from 'winston-daily-rotate-file'; import path from 'path'; /*! * @Author: richen * @Date: 2026-04-24 08:20:16 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); 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); } start = strArr.slice(0, startNum).join(""); end = strArr.slice(num + startNum).join(""); res = `${start}${"*".repeat(num)}${end}`; } return { res, start, end }; } __name(ShieldField, "ShieldField"); 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)) { return splat.map((item) => ShieldLog(item, fields, void 0, depth + 1, maxDepth)); } if (helper.isError(splat)) { return splat.message; } if (typeof splat !== "object") { if (fields.has(keyName || "")) { return ShieldField(splat).res; } return `${splat}`; } const cloneSplat = Object.create(Object.getPrototypeOf(splat)); for (const key of Object.keys(splat)) { cloneSplat[key] = ShieldLog(splat[key], fields, key, depth + 1, maxDepth); } return cloneSplat; } __name(ShieldLog, "ShieldLog"); var { combine, timestamp, printf } = format; var LogLevelObj = { "debug": 7, "info": 6, "warn": 4, "error": 3 }; var 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 } }; var Logger = class { static { __name(this, "Logger"); } // 日志级别 logLevel = "debug"; // 默认打开日志 enableLog = true; // 日志对象 emptyObj = {}; logger; transports = {}; // 文件日志 logFilePath = ""; // 脱敏字段 sensFields = /* @__PURE__ */ new Set(); // 基础日志目录,用于安全验证 baseLogDir = path.resolve(process.cwd(), "logs"); // 批量写入相关属性 batchConfig = { enabled: false, maxSize: 100, flushInterval: 1e3, maxWaitTime: 5e3 // 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] !== void 0) { this.logLevel = level; } if (process.env.LOGS_PATH) { this.logFilePath = process.env.LOGS_PATH; } if (!helper.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 = /* @__PURE__ */ 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); }); } 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(); } }); }); } /** * 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)); sanitizedArgs.unshift(logName); 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); if (normalizedPath.includes("..")) { throw new Error("Log path must not contain path traversal sequences"); } if (!path.isAbsolute(normalizedPath)) { const resolvedPath = path.resolve(this.baseLogDir, normalizedPath); if (!resolvedPath.startsWith(this.baseLogDir)) { throw new Error(`Relative log path must be within ${this.baseLogDir}`); } if (/[<>:"|?*\x00-\x1f]/.test(normalizedPath)) { throw new Error("Log path contains invalid characters"); } return resolvedPath; } if (/[<>|?*\x00-\x1f]/.test(normalizedPath)) { throw new Error("Log path contains invalid characters"); } return normalizedPath; } /** * 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("warn", "", args); } /** * warn */ warn(...args) { return this.printLog("warn", "", args); } /** * log Error * * @returns {*} * @memberof Logger */ Error(...args) { return this.printLog("error", "", args); } /** * error */ error(...args) { return this.printLog("error", "", args); } /** * log Fatal - for critical errors that cause application termination * Automatically exits the process after logging * * @returns {*} * @memberof Logger */ Fatal(...args) { if (this.batchConfig.enabled) { this.flushBatch().catch(() => { }); } console.error("\x1B[31m[FATAL]\x1B[0m", ...args); this.printLog("error", "FATAL", args); setImmediate(() => process.exit(1)); } /** * fatal */ fatal(...args) { if (this.batchConfig.enabled) { this.flushBatch().catch(() => { }); } console.error("\x1B[31m[FATAL]\x1B[0m", ...args); this.printLog("error", "FATAL", args); setImmediate(() => process.exit(1)); } /** * log Log * * Logger.Log('msg') * * Logger.Log('name', 'msg') * * Logger.Log('name', 'msg1', 'msg2'...) * * @param {...any[]} args * @returns {*} * @memberof Logger */ Log(name, ...args) { let level = "info"; if (LogLevelObj[name] !== void 0) { level = name; name = ""; } return this.printLog(level, name, args); } /** * log */ log(name, ...args) { let level = "info"; if (LogLevelObj[name] !== void 0) { 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)); sanitizedArgs.unshift(logName); return new Promise((resolve, reject) => { try { 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, timestamp2, args) { try { label = label ? `[${label}]` : ""; const params = [ `[${timestamp2}]`, label, ...ShieldLog(args, this.sensFields) ]; return util.format.apply(null, params); } catch (e) { 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 transports.Console(defaultLoggerOpt.Console); trans.push(this.transports.Console); } return createLogger({ levels: LogLevelObj, transports: trans, format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS Z" }), format.json(), printf(({ level, message, label, timestamp: timestamp2 }) => { return this.format(level, label, timestamp2, message); })) }); } }; var defaultLoggerInstance = null; function getDefaultLogger() { if (!defaultLoggerInstance) { defaultLoggerInstance = new Logger(); } return defaultLoggerInstance; } __name(getDefaultLogger, "getDefaultLogger"); // src/decorator.ts var LOG_DECORATOR_TYPE = "Log"; var customLoggerCache = /* @__PURE__ */ new WeakMap(); function getOrCreateCustomLogger(ctor, propertyName, config) { let map = customLoggerCache.get(ctor); if (!map) { map = /* @__PURE__ */ new Map(); customLoggerCache.set(ctor, map); } let logger = map.get(propertyName); if (!logger) { const opt = { ...config }; if (opt.sensFields && Array.isArray(opt.sensFields)) { opt.sensFields = new Set(opt.sensFields); } logger = new Logger(opt); map.set(propertyName, logger); } return logger; } __name(getOrCreateCustomLogger, "getOrCreateCustomLogger"); function logPropertyWrapper(_originalDescriptor, config, propertyName, target) { const key = String(propertyName); const privateKey = `_log_${key}`; return { get() { if (this[privateKey] !== void 0) { return this[privateKey]; } if (!config || typeof config === "object" && Object.keys(config).length === 0) { return getDefaultLogger(); } const ctor = this?.constructor ?? target?.constructor; if (!ctor) { return new Logger(config); } return getOrCreateCustomLogger(ctor, key, config); }, set(value) { this[privateKey] = value; }, enumerable: true, configurable: true }; } __name(logPropertyWrapper, "logPropertyWrapper"); var storedPropertyManager = null; function registerLogDecorator(propertyManager) { if (!propertyManager || typeof propertyManager.registerWrapper !== "function") { return; } try { propertyManager.registerWrapper(LOG_DECORATOR_TYPE, logPropertyWrapper); storedPropertyManager = propertyManager; } catch { } } __name(registerLogDecorator, "registerLogDecorator"); function unregisterLogDecorator() { storedPropertyManager = null; } __name(unregisterLogDecorator, "unregisterLogDecorator"); function createDualField(legacyHandler, tc39Handler) { return (...args) => { if (args.length === 2 && args[1] && typeof args[1] === "object" && "kind" in args[1]) { return tc39Handler(args[1]); } return legacyHandler(args[0], args[1]); }; } __name(createDualField, "createDualField"); function Log(options) { return createDualField((target, propertyKey) => { const pm = storedPropertyManager; if (!pm || typeof pm.registerDecorator !== "function") { return; } try { return pm.registerDecorator(target, propertyKey, { wrapperTypes: [ LOG_DECORATOR_TYPE ], config: options }); } catch { return; } }, (context) => { const fieldName = String(context.name); context.addInitializer(function() { const pm = storedPropertyManager; if (!pm || typeof pm.registerDecorator !== "function") { return; } try { const proto = Object.getPrototypeOf(this); pm.registerDecorator(proto, fieldName, { wrapperTypes: [ LOG_DECORATOR_TYPE ], config: options }); } catch { } }); }); } __name(Log, "Log"); // src/index.ts var DefaultLogger = getDefaultLogger(); export { DefaultLogger, Log, Logger, getDefaultLogger, registerLogDecorator, unregisterLogDecorator }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map