Files
claude-relay-service/src/utils/logger.js

374 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
const config = require('../../config/config')
const path = require('path')
const fs = require('fs')
const os = require('os')
// 安全的 JSON 序列化函数,处理循环引用
const safeStringify = (obj, maxDepth = 3, fullDepth = false) => {
const seen = new WeakSet()
// 如果是fullDepth模式增加深度限制
const actualMaxDepth = fullDepth ? 10 : maxDepth
const replacer = (key, value, depth = 0) => {
if (depth > actualMaxDepth) {
return '[Max Depth Reached]'
}
if (value !== null && typeof value === 'object') {
if (seen.has(value)) {
return '[Circular Reference]'
}
seen.add(value)
// 过滤掉常见的循环引用对象
if (value.constructor) {
const constructorName = value.constructor.name
if (
['Socket', 'TLSSocket', 'HTTPParser', 'IncomingMessage', 'ServerResponse'].includes(
constructorName
)
) {
return `[${constructorName} Object]`
}
}
// 递归处理对象属性
if (Array.isArray(value)) {
return value.map((item, index) => replacer(index, item, depth + 1))
} else {
const result = {}
for (const [k, v] of Object.entries(value)) {
result[k] = replacer(k, v, depth + 1)
}
return result
}
}
return value
}
try {
return JSON.stringify(replacer('', obj))
} catch (error) {
return JSON.stringify({ error: 'Failed to serialize object', message: error.message })
}
}
// 📝 增强的日志格式
const createLogFormat = (colorize = false) => {
const formats = [
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true })
// 移除 winston.format.metadata() 来避免自动包装
]
if (colorize) {
formats.push(winston.format.colorize())
}
formats.push(
winston.format.printf(({ level, message, timestamp, stack, ...rest }) => {
const emoji = {
error: '❌',
warn: '⚠️ ',
info: ' ',
debug: '🐛',
verbose: '📝'
}
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`
// 直接处理额外数据不需要metadata包装
const additionalData = { ...rest }
delete additionalData.level
delete additionalData.message
delete additionalData.timestamp
delete additionalData.stack
if (Object.keys(additionalData).length > 0) {
logMessage += ` | ${safeStringify(additionalData)}`
}
return stack ? `${logMessage}\n${stack}` : logMessage
})
)
return winston.format.combine(...formats)
}
const logFormat = createLogFormat(false)
const consoleFormat = createLogFormat(true)
// 📁 确保日志目录存在并设置权限
if (!fs.existsSync(config.logging.dirname)) {
fs.mkdirSync(config.logging.dirname, { recursive: true, mode: 0o755 })
}
// 🔄 增强的日志轮转配置
const createRotateTransport = (filename, level = null) => {
const transport = new DailyRotateFile({
filename: path.join(config.logging.dirname, filename),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles,
auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
format: logFormat
})
if (level) {
transport.level = level
}
// 监听轮转事件
transport.on('rotate', (oldFilename, newFilename) => {
console.log(`📦 Log rotated: ${oldFilename} -> ${newFilename}`)
})
transport.on('new', (newFilename) => {
console.log(`📄 New log file created: ${newFilename}`)
})
transport.on('archive', (zipFilename) => {
console.log(`🗜️ Log archived: ${zipFilename}`)
})
return transport
}
const dailyRotateFileTransport = createRotateTransport('claude-relay-%DATE%.log')
const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log', 'error')
// 🔒 创建专门的安全日志记录器
const securityLogger = winston.createLogger({
level: 'warn',
format: logFormat,
transports: [createRotateTransport('claude-relay-security-%DATE%.log', 'warn')],
silent: false
})
// 🔐 创建专门的认证详细日志记录器(记录完整的认证响应)
const authDetailLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp, data }) => {
// 使用更深的深度和格式化的JSON输出
const jsonData = data ? JSON.stringify(data, null, 2) : '{}'
return `[${timestamp}] ${level.toUpperCase()}: ${message}\n${jsonData}\n${'='.repeat(80)}`
})
),
transports: [createRotateTransport('claude-relay-auth-detail-%DATE%.log', 'info')],
silent: false
})
// 🌟 增强的 Winston logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || config.logging.level,
format: logFormat,
transports: [
// 📄 文件输出
dailyRotateFileTransport,
errorFileTransport,
// 🖥️ 控制台输出
new winston.transports.Console({
format: consoleFormat,
handleExceptions: false,
handleRejections: false
})
],
// 🚨 异常处理
exceptionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'exceptions.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 🔄 未捕获异常处理
rejectionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'rejections.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 防止进程退出
exitOnError: false
})
// 🎯 增强的自定义方法
logger.success = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'success', ...metadata })
}
logger.start = (message, metadata = {}) => {
logger.info(`🚀 ${message}`, { type: 'startup', ...metadata })
}
logger.request = (method, url, status, duration, metadata = {}) => {
const emoji = status >= 400 ? '🔴' : status >= 300 ? '🟡' : '🟢'
const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info'
logger[level](`${emoji} ${method} ${url} - ${status} (${duration}ms)`, {
type: 'request',
method,
url,
status,
duration,
...metadata
})
}
logger.api = (message, metadata = {}) => {
logger.info(`🔗 ${message}`, { type: 'api', ...metadata })
}
logger.security = (message, metadata = {}) => {
const securityData = {
type: 'security',
timestamp: new Date().toISOString(),
pid: process.pid,
hostname: os.hostname(),
...metadata
}
// 记录到主日志
logger.warn(`🔒 ${message}`, securityData)
// 记录到专门的安全日志文件
try {
securityLogger.warn(`🔒 ${message}`, securityData)
} catch (error) {
// 如果安全日志文件不可用,只记录到主日志
console.warn('Security logger not available:', error.message)
}
}
logger.database = (message, metadata = {}) => {
logger.debug(`💾 ${message}`, { type: 'database', ...metadata })
}
logger.performance = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'performance', ...metadata })
}
logger.audit = (message, metadata = {}) => {
logger.info(`📋 ${message}`, {
type: 'audit',
timestamp: new Date().toISOString(),
pid: process.pid,
...metadata
})
}
// 🔧 性能监控方法
logger.timer = (label) => {
const start = Date.now()
return {
end: (message = '', metadata = {}) => {
const duration = Date.now() - start
logger.performance(`${label} ${message}`, { duration, ...metadata })
return duration
}
}
}
// 📊 日志统计
logger.stats = {
requests: 0,
errors: 0,
warnings: 0
}
// 重写原始方法以统计
const originalError = logger.error
const originalWarn = logger.warn
const originalInfo = logger.info
logger.error = function (message, ...args) {
logger.stats.errors++
return originalError.call(this, message, ...args)
}
logger.warn = function (message, ...args) {
logger.stats.warnings++
return originalWarn.call(this, message, ...args)
}
logger.info = function (message, ...args) {
// 检查是否是请求类型的日志
if (args.length > 0 && typeof args[0] === 'object' && args[0].type === 'request') {
logger.stats.requests++
}
return originalInfo.call(this, message, ...args)
}
// 📈 获取日志统计
logger.getStats = () => ({ ...logger.stats })
// 🧹 清理统计
logger.resetStats = () => {
logger.stats.requests = 0
logger.stats.errors = 0
logger.stats.warnings = 0
}
// 📡 健康检查
logger.healthCheck = () => {
try {
const testMessage = 'Logger health check'
logger.debug(testMessage)
return { healthy: true, timestamp: new Date().toISOString() }
} catch (error) {
return { healthy: false, error: error.message, timestamp: new Date().toISOString() }
}
}
// 🔐 记录认证详细信息的方法
logger.authDetail = (message, data = {}) => {
try {
// 记录到主日志(简化版)
logger.info(`🔐 ${message}`, {
type: 'auth-detail',
summary: {
hasAccessToken: !!data.access_token,
hasRefreshToken: !!data.refresh_token,
scopes: data.scope || data.scopes,
organization: data.organization?.name,
account: data.account?.email_address
}
})
// 记录到专门的认证详细日志文件(完整数据)
authDetailLogger.info(message, { data })
} catch (error) {
logger.error('Failed to log auth detail:', error)
}
}
// 🎬 启动日志记录系统
logger.start('Logger initialized', {
level: process.env.LOG_LEVEL || config.logging.level,
directory: config.logging.dirname,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles,
envOverride: process.env.LOG_LEVEL ? true : false
})
module.exports = logger