koatty_logger
Version:
Logger for koatty.
782 lines (778 loc) • 19.3 kB
JavaScript
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