feat(log): 增强日志系统功能与追踪能力

- 新增模块级别日志控制和采样配置
- 为操作日志添加requestId字段支持全链路追踪
- 优化请求日志中间件,增强结构化记录能力
- 实现日志降级机制,数据库不可用时写入文件
- 添加相关数据库迁移脚本和模型索引
This commit is contained in:
zhang1106
2026-05-07 17:14:54 +08:00
parent 54b0a8e7bb
commit 03e320af9e
8 changed files with 2809 additions and 58 deletions
+142 -32
View File
@@ -1,6 +1,11 @@
/**
* 统一日志模块
* 基于 winston 的日志系统,支持文件轮转、级别控制、模块标识
* 基于 winston 的日志系统,支持
* - 文件轮转、级别控制、模块标识
* - JSON结构化输出(生产环境)
* - 请求追踪ID传递
* - 模块级别精细控制
* - 日志采样过滤
* 全项目唯一日志入口
*/
@@ -8,32 +13,107 @@ 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';
/**
* 统一日志格式:时间 | 级别 | 模块 | 消息 | 元数据
* 模块级别配置
* 支持按模块精细控制日志级别,JSON格式
* 示例:MODULE_LOG_LEVELS={"device":"debug","backup":"warn"}
*/
const logFormat = format.combine(
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(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
format.errors({ stack: true }),
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'module'] }),
format.printf(({ timestamp, level, message, module, metadata, stack }) => {
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 }) => {
const moduleTag = module ? `[${module}]` : '';
const requestTag = requestId ? `[${requestId}]` : '';
const metaStr = metadata && Object.keys(metadata).length
? ' ' + JSON.stringify(metadata)
: '';
const stackStr = stack ? `\n${stack}` : '';
return `${timestamp} ${level.toUpperCase().padEnd(5)} ${moduleTag} ${message}${metaStr}${stackStr}`;
return `${timestamp} ${level} ${moduleTag}${requestTag} ${message}${metaStr}`;
})
);
@@ -42,19 +122,17 @@ const logFormat = format.combine(
*/
const logger = createLogger({
level: LOG_LEVEL,
format: logFormat,
format: jsonFormat,
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,
level: 'info',
level: 'debug',
}),
// 错误日志 - 独立存储
new DailyRotateFile({
dirname: path.join(LOG_DIR, 'error'),
filename: 'error-%DATE%.log',
@@ -63,8 +141,15 @@ const logger = createLogger({
maxSize: LOG_MAX_FILE_SIZE,
level: 'error',
}),
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',
}),
],
// 未捕获异常处理
exceptionHandlers: [
new DailyRotateFile({
dirname: path.join(LOG_DIR, 'error'),
@@ -73,7 +158,6 @@ const logger = createLogger({
maxFiles: LOG_MAX_FILES,
}),
],
// 未处理 Promise 拒绝处理
rejectionHandlers: [
new DailyRotateFile({
dirname: path.join(LOG_DIR, 'error'),
@@ -84,30 +168,56 @@ const logger = createLogger({
],
});
// 开发环境添加控制台输出(带颜色)
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({
format: format.combine(
format.colorize(),
format.timestamp({ format: 'HH:mm:ss' }),
format.printf(({ timestamp, level, message, module }) => {
const moduleTag = module ? `[${module}]` : '';
return `${timestamp} ${level} ${moduleTag} ${message}`;
})
),
format: consoleFormat,
level: 'debug',
}));
}
/**
* 创建带模块名的子 logger
* 支持模块级别精细控制和日志采样
* @param {string} moduleName - 模块名称
* @returns {Object} 包含 debug/info/warn/error 方法的对象
*/
logger.module = (moduleName) => ({
debug: (msg, meta) => logger.debug(msg, { module: moduleName, ...meta }),
info: (msg, meta) => logger.info(msg, { module: moduleName, ...meta }),
warn: (msg, meta) => logger.warn(msg, { module: moduleName, ...meta }),
error: (msg, meta) => logger.error(msg, { module: moduleName, ...meta }),
});
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'),
};
};
module.exports = logger;
+145 -20
View File
@@ -1,9 +1,27 @@
/**
* 操作日志记录器
* 支持:
* - 关联请求追踪IDrequestId
* - 数据库写入失败时降级到文件日志
* - 统一错误处理(使用logger替代console.error
*/
const OperationLog = require('../models/OperationLog');
const logger = require('./logger');
const fs = require('fs');
const path = require('path');
const logModule = logger.module('OperationLogger');
const generateRecordId = () => {
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
};
/**
* 获取操作人信息
* @param {Object} req - Express请求对象
* @returns {Object} 操作人信息
*/
const getOperatorInfo = req => {
if (!req || !req.user) {
return {
@@ -19,9 +37,14 @@ const getOperatorInfo = req => {
};
};
/**
* 获取客户端信息
* @param {Object} req - Express请求对象
* @returns {Object} 客户端信息(含requestId
*/
const getClientInfo = req => {
if (!req) {
return { ipAddress: null, userAgent: null };
return { ipAddress: null, userAgent: null, requestId: null };
}
const ipAddress =
req.headers['x-forwarded-for'] ||
@@ -29,8 +52,11 @@ const getClientInfo = req => {
req.connection?.remoteAddress ||
req.ip ||
null;
const userAgent = req.headers['user-agent'] || null;
return { ipAddress, userAgent };
return {
ipAddress,
userAgent: req.headers['user-agent'] || null,
requestId: req.requestId || null,
};
};
const DEVICE_TYPE_MAP = {
@@ -43,6 +69,13 @@ const DEVICE_TYPE_MAP = {
other: '其他设备',
};
/**
* 生成设备操作描述
* @param {string} operation - 操作类型描述
* @param {Object} device - 设备信息
* @param {Object} options - 选项
* @returns {string} 操作描述
*/
const generateDeviceDescription = (operation, device, options = {}) => {
const {
includeRack = true,
@@ -82,6 +115,12 @@ const generateDeviceDescription = (operation, device, options = {}) => {
return `${operation}${parts.join('')}`;
};
/**
* 构建设备元数据
* @param {Object} device - 设备信息
* @param {Object} extra - 额外字段
* @returns {Object} 元数据
*/
const buildDeviceMetadata = (device, extra = {}) => {
return {
deviceId: device.deviceId || null,
@@ -99,6 +138,46 @@ const buildDeviceMetadata = (device, extra = {}) => {
};
};
/**
* 降级写入文件日志
* 当数据库不可用时,将操作日志写入文件作为降级方案
* @param {Object} logData - 日志数据
*/
const fallbackToFile = (logData) => {
try {
const fallbackDir = path.join(process.env.LOG_DIR || './logs', 'fallback');
if (!fs.existsSync(fallbackDir)) {
fs.mkdirSync(fallbackDir, { recursive: true });
}
const fallbackPath = path.join(fallbackDir, `operation-fallback-${new Date().toISOString().slice(0, 10)}.log`);
const fallbackEntry = {
timestamp: new Date().toISOString(),
level: 'warn',
module: 'OperationLogger',
message: '操作日志数据库写入失败(降级记录)',
data: logData,
};
fs.appendFileSync(fallbackPath, JSON.stringify(fallbackEntry) + '\n');
} catch {
// 文件写入也失败时静默忽略,避免级联错误
}
};
/**
* 记录操作日志(核心方法)
* @param {Object} params - 参数对象
* @param {string} params.module - 模块名称
* @param {string} params.operationType - 操作类型
* @param {string} params.operationDescription - 操作描述
* @param {string} params.targetId - 目标ID
* @param {string} params.targetName - 目标名称
* @param {Object} params.beforeState - 操作前状态
* @param {Object} params.afterState - 操作后状态
* @param {string} params.result - 操作结果
* @param {Object} params.req - 请求对象
* @param {Object} params.metadata - 附加元数据
* @returns {Promise<Object|null>} 创建的日志记录
*/
async function logOperation({
module,
operationType,
@@ -111,32 +190,64 @@ async function logOperation({
req,
metadata = {},
}) {
try {
const operatorInfo = getOperatorInfo(req);
const clientInfo = getClientInfo(req);
const operatorInfo = getOperatorInfo(req);
const clientInfo = getClientInfo(req);
await OperationLog.create({
const logData = {
module,
operationType,
operationDescription,
targetId: targetId || null,
targetName: targetName || null,
operatorId: operatorInfo.operatorId,
operatorName: operatorInfo.operatorName,
operatorRole: operatorInfo.operatorRole,
beforeState: beforeState || null,
afterState: afterState || null,
result,
ipAddress: clientInfo.ipAddress,
userAgent: clientInfo.userAgent,
requestId: clientInfo.requestId,
metadata,
};
try {
const operationLog = await OperationLog.create({
recordId: generateRecordId(),
...logData,
});
logModule.debug('操作日志记录成功', {
recordId: operationLog.recordId,
module,
operationType,
operationDescription,
targetId: targetId || null,
targetName: targetName || null,
operatorId: operatorInfo.operatorId,
operatorName: operatorInfo.operatorName,
operatorRole: operatorInfo.operatorRole,
beforeState: beforeState || null,
afterState: afterState || null,
result,
ipAddress: clientInfo.ipAddress,
userAgent: clientInfo.userAgent,
metadata,
targetId,
requestId: clientInfo.requestId,
});
return operationLog;
} catch (error) {
console.error('记录操作日志失败:', error);
logModule.error('记录操作日志失败', {
error: error.message,
module,
operationType,
targetId,
requestId: clientInfo.requestId,
});
fallbackToFile(logData);
return null;
}
}
/**
* 记录设备操作日志
* @param {string} operationType - 操作类型
* @param {string} operationDescription - 操作描述
* @param {Object} params - 参数
* @returns {Promise<Object|null>}
*/
async function logDeviceOperation(
operationType,
operationDescription,
@@ -156,6 +267,13 @@ async function logDeviceOperation(
});
}
/**
* 记录用户操作日志
* @param {string} operationType - 操作类型
* @param {string} operationDescription - 操作描述
* @param {Object} params - 参数
* @returns {Promise<Object|null>}
*/
async function logUserOperation(
operationType,
operationDescription,
@@ -175,6 +293,13 @@ async function logUserOperation(
});
}
/**
* 记录角色操作日志
* @param {string} operationType - 操作类型
* @param {string} operationDescription - 操作描述
* @param {Object} params - 参数
* @returns {Promise<Object|null>}
*/
async function logRoleOperation(
operationType,
operationDescription,