Spaces:
Running
Running
// logger.js - 统一的日志系统模块 | |
const fs = require('fs'); | |
const path = require('path'); | |
// 避免循环依赖 | |
let config = null; | |
// 延迟加载配置 | |
function getConfig() { | |
if (!config) { | |
try { | |
config = require('../config/config'); | |
} catch (err) { | |
console.error('加载配置文件失败:', err.message); | |
config = { log: { level: 'INFO', format: 'colored' } }; | |
} | |
} | |
return config; | |
} | |
const LOG_LEVELS = { | |
ERROR: 0, | |
WARN: 1, | |
INFO: 2, | |
DEBUG: 3, | |
TRACE: 4, | |
HTTP: 2 // HTTP日志级别与INFO相同 | |
}; | |
// 默认日志级别 | |
let currentLogLevel = LOG_LEVELS.INFO; | |
// 日志格式 | |
let logFormat = 'colored'; // colored, json, text | |
// 带颜色的控制台输出 | |
const COLORS = { | |
RESET: '\x1b[0m', | |
RED: '\x1b[31m', | |
YELLOW: '\x1b[33m', | |
GREEN: '\x1b[32m', | |
BLUE: '\x1b[34m', | |
CYAN: '\x1b[36m' | |
}; | |
// 日志文件配置 | |
const LOG_DIR = path.join(__dirname, '../../logs'); | |
const LOG_FILE = path.join(LOG_DIR, 'app.log'); | |
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB | |
let logToFile = false; | |
// 内存中存储的日志(用于网页显示) | |
const memoryLogs = []; | |
const MAX_MEMORY_LOGS = 1000; // 内存中最多保存的日志条数 | |
// 确保日志目录存在 | |
function ensureLogDirExists() { | |
try { | |
if (!fs.existsSync(LOG_DIR)) { | |
fs.mkdirSync(LOG_DIR, { recursive: true }); | |
} | |
return true; | |
} catch (err) { | |
console.error(`创建日志目录失败: ${err.message}`); | |
return false; | |
} | |
} | |
// 初始化文件日志 | |
function initFileLogging() { | |
const conf = getConfig(); | |
if (process.env.LOG_TO_FILE === 'true' || (conf.log && conf.log.toFile)) { | |
if (ensureLogDirExists()) { | |
logToFile = true; | |
// 检查日志文件大小,如果超过最大值则进行轮转 | |
if (fs.existsSync(LOG_FILE)) { | |
const stats = fs.statSync(LOG_FILE); | |
if (stats.size > MAX_LOG_SIZE) { | |
rotateLogFile(); | |
} | |
} | |
return true; | |
} | |
} | |
return false; | |
} | |
// 日志文件轮转 | |
function rotateLogFile() { | |
try { | |
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
const newLogFile = path.join(LOG_DIR, `app-${timestamp}.log`); | |
if (fs.existsSync(LOG_FILE)) { | |
fs.renameSync(LOG_FILE, newLogFile); | |
} | |
// 清理旧日志文件,保留最近10个 | |
const logFiles = fs.readdirSync(LOG_DIR) | |
.filter(file => file.startsWith('app-') && file.endsWith('.log')) | |
.sort() | |
.reverse(); | |
if (logFiles.length > 10) { | |
logFiles.slice(10).forEach(file => { | |
try { | |
fs.unlinkSync(path.join(LOG_DIR, file)); | |
} catch (err) { | |
console.error(`删除旧日志文件失败: ${err.message}`); | |
} | |
}); | |
} | |
} catch (err) { | |
console.error(`日志文件轮转失败: ${err.message}`); | |
logToFile = false; | |
} | |
} | |
// 添加日志到内存 | |
function addLogToMemory(level, timestamp, ...args) { | |
// 将日志对象添加到内存数组 | |
const logEntry = { | |
level, | |
timestamp, | |
message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ') | |
}; | |
memoryLogs.unshift(logEntry); // 新日志添加到数组开头 | |
// 保持数组在最大长度以内 | |
if (memoryLogs.length > MAX_MEMORY_LOGS) { | |
memoryLogs.pop(); // 移除最旧的日志 | |
} | |
} | |
// 将日志写入文件 | |
function writeLogToFile(level, timestamp, ...args) { | |
if (!logToFile) return; | |
try { | |
let logEntry; | |
if (logFormat === 'json') { | |
// JSON格式 | |
const data = args.map(arg => typeof arg === 'object' ? arg : String(arg)); | |
const logObject = { | |
level, | |
timestamp, | |
message: data.length === 1 ? data[0] : data | |
}; | |
logEntry = JSON.stringify(logObject) + '\n'; | |
} else { | |
// 文本格式 | |
logEntry = `[${level}] ${timestamp} ${args.map(arg => | |
typeof arg === 'object' ? JSON.stringify(arg) : arg | |
).join(' ')}\n`; | |
} | |
fs.appendFileSync(LOG_FILE, logEntry); | |
// 检查文件大小,必要时进行轮转 | |
const stats = fs.statSync(LOG_FILE); | |
if (stats.size > MAX_LOG_SIZE) { | |
rotateLogFile(); | |
} | |
} catch (err) { | |
console.error(`写入日志文件失败: ${err.message}`); | |
logToFile = false; | |
} | |
} | |
// 获取时间戳 | |
function getTimestamp() { | |
return new Date().toISOString(); | |
} | |
// 设置日志级别 | |
function setLogLevel(level) { | |
if (typeof level === 'string') { | |
level = level.toUpperCase(); | |
if (LOG_LEVELS[level] !== undefined) { | |
currentLogLevel = LOG_LEVELS[level]; | |
} else { | |
error(`无效的日志级别: ${level}`); | |
} | |
} else if (typeof level === 'number' && level >= 0 && level <= 4) { | |
currentLogLevel = level; | |
} else { | |
error(`无效的日志级别: ${level}`); | |
} | |
} | |
// 设置日志格式 | |
function setLogFormat(format) { | |
const validFormats = ['colored', 'json', 'text']; | |
if (validFormats.includes(format)) { | |
logFormat = format; | |
return true; | |
} else { | |
error(`无效的日志格式: ${format}`); | |
return false; | |
} | |
} | |
// 格式化控制台日志 | |
function formatConsoleLog(level, timestamp, color, ...args) { | |
if (logFormat === 'json') { | |
// JSON格式 | |
const data = args.map(arg => typeof arg === 'object' ? arg : String(arg)); | |
return JSON.stringify({ | |
level, | |
timestamp, | |
message: data.length === 1 ? data[0] : data | |
}); | |
} else if (logFormat === 'text') { | |
// 纯文本格式(无颜色) | |
return `[${level}] ${timestamp} ${args.join(' ')}`; | |
} else { | |
// 默认:带颜色格式 | |
return `${color}[${level}] ${timestamp}${COLORS.RESET} ${args.join(' ')}`; | |
} | |
} | |
// 错误日志 | |
function error(...args) { | |
if (currentLogLevel >= LOG_LEVELS.ERROR) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('ERROR', timestamp, COLORS.RED, ...args); | |
console.error(formattedLog); | |
writeLogToFile('ERROR', timestamp, ...args); | |
addLogToMemory('ERROR', timestamp, ...args); | |
} | |
} | |
// 警告日志 | |
function warn(...args) { | |
if (currentLogLevel >= LOG_LEVELS.WARN) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('WARN', timestamp, COLORS.YELLOW, ...args); | |
console.warn(formattedLog); | |
writeLogToFile('WARN', timestamp, ...args); | |
addLogToMemory('WARN', timestamp, ...args); | |
} | |
} | |
// 信息日志 | |
function info(...args) { | |
if (currentLogLevel >= LOG_LEVELS.INFO) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('INFO', timestamp, COLORS.GREEN, ...args); | |
console.log(formattedLog); | |
writeLogToFile('INFO', timestamp, ...args); | |
addLogToMemory('INFO', timestamp, ...args); | |
} | |
} | |
// 调试日志 | |
function debug(...args) { | |
if (currentLogLevel >= LOG_LEVELS.DEBUG) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('DEBUG', timestamp, COLORS.BLUE, ...args); | |
console.log(formattedLog); | |
writeLogToFile('DEBUG', timestamp, ...args); | |
addLogToMemory('DEBUG', timestamp, ...args); | |
} | |
} | |
// 跟踪日志 | |
function trace(...args) { | |
if (currentLogLevel >= LOG_LEVELS.TRACE) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('TRACE', timestamp, COLORS.CYAN, ...args); | |
console.log(formattedLog); | |
writeLogToFile('TRACE', timestamp, ...args); | |
addLogToMemory('TRACE', timestamp, ...args); | |
} | |
} | |
// HTTP请求日志 (特殊处理,方便筛选) | |
function http(...args) { | |
if (currentLogLevel >= LOG_LEVELS.INFO) { | |
const timestamp = getTimestamp(); | |
const formattedLog = formatConsoleLog('HTTP', timestamp, COLORS.CYAN, ...args); | |
console.log(formattedLog); | |
writeLogToFile('HTTP', timestamp, ...args); | |
addLogToMemory('HTTP', timestamp, ...args); | |
} | |
} | |
// 获取内存中的日志 | |
function getLogs(filter = {}) { | |
let filteredLogs = [...memoryLogs]; | |
// 按日志级别筛选 | |
if (filter.level) { | |
filteredLogs = filteredLogs.filter(log => log.level === filter.level); | |
} | |
// 按时间范围筛选 | |
if (filter.startTime) { | |
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= new Date(filter.startTime)); | |
} | |
if (filter.endTime) { | |
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= new Date(filter.endTime)); | |
} | |
// 按关键词搜索 | |
if (filter.search) { | |
const searchTerm = filter.search.toLowerCase(); | |
filteredLogs = filteredLogs.filter(log => | |
log.message.toLowerCase().includes(searchTerm) || | |
log.level.toLowerCase().includes(searchTerm) | |
); | |
} | |
// 分页 | |
const page = filter.page || 1; | |
const pageSize = filter.pageSize || 100; | |
const start = (page - 1) * pageSize; | |
const end = start + pageSize; | |
return { | |
logs: filteredLogs.slice(start, end), | |
total: filteredLogs.length, | |
page, | |
pageSize | |
}; | |
} | |
// 清除内存日志 | |
function clearMemoryLogs() { | |
memoryLogs.length = 0; | |
info('内存日志已清除'); | |
} | |
// 初始化配置 | |
function initialize() { | |
try { | |
const conf = getConfig(); | |
// 初始化日志级别 | |
const envLevel = process.env.LOG_LEVEL; | |
if (envLevel) { | |
setLogLevel(envLevel); | |
} else if (conf && conf.log && conf.log.level) { | |
setLogLevel(conf.log.level); | |
} | |
// 初始化日志格式 | |
const envFormat = process.env.LOG_FORMAT; | |
if (envFormat) { | |
setLogFormat(envFormat); | |
} else if (conf && conf.log && conf.log.format) { | |
setLogFormat(conf.log.format); | |
} | |
// 初始化文件日志 | |
initFileLogging(); | |
} catch (err) { | |
console.error(`初始化日志系统出错: ${err.message}`); | |
} | |
} | |
// 初始化 | |
initialize(); | |
module.exports = { | |
LOG_LEVELS, | |
setLogLevel, | |
setLogFormat, | |
error, | |
warn, | |
info, | |
debug, | |
trace, | |
http, | |
// 暴露文件日志相关方法 | |
enableFileLogging: () => { | |
if (ensureLogDirExists()) { | |
logToFile = true; | |
info('文件日志已启用'); | |
return true; | |
} | |
return false; | |
}, | |
disableFileLogging: () => { | |
logToFile = false; | |
info('文件日志已禁用'); | |
}, | |
rotateLogFile, | |
// 添加内存日志相关方法 | |
getLogs, | |
clearMemoryLogs | |
}; |