koatty_logger
Version:
Logger for koatty.
727 lines (719 loc) • 20.5 kB
JavaScript
/*!
* @Author: richen
* @Date: 2025-06-02 17:42:08
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
;
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;