2026-04-28 10:57:17 +08:00
|
|
|
/**
|
|
|
|
|
* 统一日志模块
|
2026-05-07 17:14:54 +08:00
|
|
|
* 基于 winston 的日志系统,支持:
|
|
|
|
|
* - 文件轮转、级别控制、模块标识
|
|
|
|
|
* - JSON结构化输出(生产环境)
|
|
|
|
|
* - 请求追踪ID传递
|
|
|
|
|
* - 模块级别精细控制
|
|
|
|
|
* - 日志采样过滤
|
2026-04-28 10:57:17 +08:00
|
|
|
* 全项目唯一日志入口
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const { createLogger, format, transports } = require('winston');
|
|
|
|
|
const DailyRotateFile = require('winston-daily-rotate-file');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '../logs');
|
|
|
|
|
|
|
|
|
|
const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
|
|
|
|
|
|
|
|
|
|
const LOG_MAX_FILE_SIZE = process.env.LOG_MAX_FILE_SIZE || '20m';
|
|
|
|
|
|
|
|
|
|
const LOG_MAX_FILES = process.env.LOG_MAX_FILES || '30d';
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-07 17:14:54 +08:00
|
|
|
* 模块级别配置
|
|
|
|
|
* 支持按模块精细控制日志级别,JSON格式
|
|
|
|
|
* 示例:MODULE_LOG_LEVELS={"device":"debug","backup":"warn"}
|
2026-04-28 10:57:17 +08:00
|
|
|
*/
|
2026-05-07 17:14:54 +08:00
|
|
|
let MODULE_LOG_LEVELS = {};
|
|
|
|
|
try {
|
|
|
|
|
MODULE_LOG_LEVELS = process.env.MODULE_LOG_LEVELS
|
|
|
|
|
? JSON.parse(process.env.MODULE_LOG_LEVELS)
|
|
|
|
|
: {};
|
|
|
|
|
} catch {
|
|
|
|
|
MODULE_LOG_LEVELS = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日志采样配置
|
|
|
|
|
* 高频日志采样输出,rate表示采样率(0-1)
|
|
|
|
|
* 示例:LOG_SAMPLING={"HTTP":0.1,"device":0.5}
|
|
|
|
|
*/
|
|
|
|
|
let LOG_SAMPLING = {};
|
|
|
|
|
try {
|
|
|
|
|
LOG_SAMPLING = process.env.LOG_SAMPLING
|
|
|
|
|
? JSON.parse(process.env.LOG_SAMPLING)
|
|
|
|
|
: {};
|
|
|
|
|
} catch {
|
|
|
|
|
LOG_SAMPLING = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const samplingCounters = {};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取模块日志级别
|
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
|
* @returns {string} 日志级别
|
|
|
|
|
*/
|
|
|
|
|
const getModuleLevel = (moduleName) => {
|
|
|
|
|
return MODULE_LOG_LEVELS[moduleName] || LOG_LEVEL;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日志级别优先级映射
|
|
|
|
|
*/
|
|
|
|
|
const LEVEL_PRIORITY = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断是否应该输出该级别日志
|
|
|
|
|
* @param {string} level - 当前日志级别
|
|
|
|
|
* @param {string} minLevel - 最低输出级别
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
const shouldLog = (level, minLevel) => {
|
|
|
|
|
return (LEVEL_PRIORITY[level] || 99) <= (LEVEL_PRIORITY[minLevel] || 99);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日志采样判断
|
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
|
* @param {string} level - 日志级别
|
|
|
|
|
* @returns {boolean} 是否应该输出
|
|
|
|
|
*/
|
|
|
|
|
const shouldSample = (moduleName, level) => {
|
|
|
|
|
if (level === 'error' || level === 'warn') return true;
|
|
|
|
|
|
|
|
|
|
const samplingConfig = LOG_SAMPLING[moduleName];
|
|
|
|
|
if (!samplingConfig || samplingConfig.rate >= 1) return true;
|
|
|
|
|
|
|
|
|
|
samplingCounters[moduleName] = (samplingCounters[moduleName] || 0) + 1;
|
|
|
|
|
return samplingCounters[moduleName] % Math.floor(1 / samplingConfig.rate) === 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* JSON格式输出(生产环境文件日志)
|
|
|
|
|
*/
|
|
|
|
|
const jsonFormat = format.combine(
|
2026-04-28 10:57:17 +08:00
|
|
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
|
|
|
|
|
format.errors({ stack: true }),
|
2026-05-07 17:14:54 +08:00
|
|
|
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'module', 'requestId'] }),
|
|
|
|
|
format.json()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文本格式输出(开发环境控制台)
|
|
|
|
|
*/
|
|
|
|
|
const consoleFormat = format.combine(
|
|
|
|
|
format.colorize(),
|
|
|
|
|
format.timestamp({ format: 'HH:mm:ss.SSS' }),
|
|
|
|
|
format.printf(({ timestamp, level, message, module, requestId, metadata }) => {
|
2026-04-28 10:57:17 +08:00
|
|
|
const moduleTag = module ? `[${module}]` : '';
|
2026-05-07 17:14:54 +08:00
|
|
|
const requestTag = requestId ? `[${requestId}]` : '';
|
2026-04-28 10:57:17 +08:00
|
|
|
const metaStr = metadata && Object.keys(metadata).length
|
|
|
|
|
? ' ' + JSON.stringify(metadata)
|
|
|
|
|
: '';
|
2026-05-07 17:14:54 +08:00
|
|
|
return `${timestamp} ${level} ${moduleTag}${requestTag} ${message}${metaStr}`;
|
2026-04-28 10:57:17 +08:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建 winston logger 实例
|
|
|
|
|
*/
|
|
|
|
|
const logger = createLogger({
|
|
|
|
|
level: LOG_LEVEL,
|
2026-05-07 17:14:54 +08:00
|
|
|
format: jsonFormat,
|
2026-04-28 10:57:17 +08:00
|
|
|
defaultMeta: { service: 'idc-management' },
|
|
|
|
|
transports: [
|
|
|
|
|
new DailyRotateFile({
|
|
|
|
|
dirname: path.join(LOG_DIR, 'app'),
|
|
|
|
|
filename: 'application-%DATE%.log',
|
|
|
|
|
datePattern: 'YYYY-MM-DD',
|
|
|
|
|
maxFiles: LOG_MAX_FILES,
|
|
|
|
|
maxSize: LOG_MAX_FILE_SIZE,
|
2026-05-07 17:14:54 +08:00
|
|
|
level: 'debug',
|
2026-04-28 10:57:17 +08:00
|
|
|
}),
|
|
|
|
|
new DailyRotateFile({
|
|
|
|
|
dirname: path.join(LOG_DIR, 'error'),
|
|
|
|
|
filename: 'error-%DATE%.log',
|
|
|
|
|
datePattern: 'YYYY-MM-DD',
|
|
|
|
|
maxFiles: '60d',
|
|
|
|
|
maxSize: LOG_MAX_FILE_SIZE,
|
|
|
|
|
level: 'error',
|
|
|
|
|
}),
|
2026-05-07 17:14:54 +08:00
|
|
|
new DailyRotateFile({
|
|
|
|
|
dirname: path.join(LOG_DIR, 'request'),
|
|
|
|
|
filename: 'request-%DATE%.log',
|
|
|
|
|
datePattern: 'YYYY-MM-DD',
|
|
|
|
|
maxFiles: '15d',
|
|
|
|
|
maxSize: LOG_MAX_FILE_SIZE,
|
|
|
|
|
level: 'debug',
|
|
|
|
|
}),
|
2026-04-28 10:57:17 +08:00
|
|
|
],
|
|
|
|
|
exceptionHandlers: [
|
|
|
|
|
new DailyRotateFile({
|
|
|
|
|
dirname: path.join(LOG_DIR, 'error'),
|
|
|
|
|
filename: 'exceptions-%DATE%.log',
|
|
|
|
|
datePattern: 'YYYY-MM-DD',
|
|
|
|
|
maxFiles: LOG_MAX_FILES,
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
rejectionHandlers: [
|
|
|
|
|
new DailyRotateFile({
|
|
|
|
|
dirname: path.join(LOG_DIR, 'error'),
|
|
|
|
|
filename: 'rejections-%DATE%.log',
|
|
|
|
|
datePattern: 'YYYY-MM-DD',
|
|
|
|
|
maxFiles: LOG_MAX_FILES,
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-23 10:27:47 +08:00
|
|
|
if (process.env.NODE_ENV !== 'production' || process.env.CONSOLE_LOG === 'true') {
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.add(new transports.Console({
|
2026-06-23 10:27:47 +08:00
|
|
|
format: process.env.NODE_ENV === 'production' ? jsonFormat : consoleFormat,
|
|
|
|
|
level: LOG_LEVEL,
|
2026-04-28 10:57:17 +08:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建带模块名的子 logger
|
2026-05-07 17:14:54 +08:00
|
|
|
* 支持模块级别精细控制和日志采样
|
2026-04-28 10:57:17 +08:00
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
|
* @returns {Object} 包含 debug/info/warn/error 方法的对象
|
|
|
|
|
*/
|
2026-05-07 17:14:54 +08:00
|
|
|
logger.module = (moduleName) => {
|
|
|
|
|
const moduleLevel = getModuleLevel(moduleName);
|
|
|
|
|
|
|
|
|
|
const logWithLevel = (level, msg, meta = {}) => {
|
|
|
|
|
if (!shouldLog(level, moduleLevel)) return;
|
|
|
|
|
if (!shouldSample(moduleName, level)) return;
|
|
|
|
|
logger.log(level, msg, { module: moduleName, ...meta });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
debug: (msg, meta) => logWithLevel('debug', msg, meta),
|
|
|
|
|
info: (msg, meta) => logWithLevel('info', msg, meta),
|
|
|
|
|
warn: (msg, meta) => logWithLevel('warn', msg, meta),
|
|
|
|
|
error: (msg, meta) => logWithLevel('error', msg, meta),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建带请求ID的 logger
|
|
|
|
|
* 用于全链路请求追踪
|
|
|
|
|
* @param {string} requestId - 请求追踪ID
|
|
|
|
|
* @param {string} moduleName - 模块名称(可选)
|
|
|
|
|
* @returns {Object} 包含日志方法的对象
|
|
|
|
|
*/
|
|
|
|
|
logger.withRequestId = (requestId, moduleName = null) => {
|
|
|
|
|
const baseLogger = moduleName ? logger.module(moduleName) : logger;
|
|
|
|
|
|
|
|
|
|
const wrapMethod = (method) => (msg, meta = {}) => {
|
|
|
|
|
baseLogger[method](msg, { requestId, ...meta });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
debug: wrapMethod('debug'),
|
|
|
|
|
info: wrapMethod('info'),
|
|
|
|
|
warn: wrapMethod('warn'),
|
|
|
|
|
error: wrapMethod('error'),
|
|
|
|
|
};
|
|
|
|
|
};
|
2026-04-28 10:57:17 +08:00
|
|
|
|
|
|
|
|
module.exports = logger;
|