| const winston = require('winston') |
| const DailyRotateFile = require('winston-daily-rotate-file') |
| const config = require('../../config/config') |
| const { formatDateWithTimezone } = require('../utils/dateHelper') |
| const path = require('path') |
| const fs = require('fs') |
| const os = require('os') |
|
|
| |
| const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { |
| const seen = new WeakSet() |
| |
| const actualMaxDepth = fullDepth ? 10 : maxDepth |
|
|
| const replacer = (key, value, depth = 0) => { |
| if (depth > actualMaxDepth) { |
| return '[Max Depth Reached]' |
| } |
|
|
| |
| if (typeof value === 'string') { |
| try { |
| |
| let cleanValue = value |
| |
| .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') |
| .replace(/[\uD800-\uDFFF]/g, '') |
| |
| .replace(/\u0000/g, '') |
|
|
| |
| if (cleanValue.length > 1000) { |
| cleanValue = `${cleanValue.substring(0, 997)}...` |
| } |
|
|
| return cleanValue |
| } catch (error) { |
| return '[Invalid String Data]' |
| } |
| } |
|
|
| 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)) { |
| |
| |
| const safeKey = typeof k === 'string' ? k.replace(/[\u0000-\u001F\u007F]/g, '') : k |
| result[safeKey] = replacer(safeKey, v, depth + 1) |
| } |
| return result |
| } |
| } |
|
|
| return value |
| } |
|
|
| try { |
| const processed = replacer('', obj) |
| return JSON.stringify(processed) |
| } catch (error) { |
| |
| try { |
| return JSON.stringify({ |
| error: 'Failed to serialize object', |
| message: error.message, |
| type: typeof obj, |
| keys: obj && typeof obj === 'object' ? Object.keys(obj) : undefined |
| }) |
| } catch (finalError) { |
| return '{"error":"Critical serialization failure","message":"Unable to serialize any data"}' |
| } |
| } |
| } |
|
|
| |
| const createLogFormat = (colorize = false) => { |
| const formats = [ |
| winston.format.timestamp({ format: () => formatDateWithTimezone(new Date(), false) }), |
| winston.format.errors({ stack: true }) |
| |
| ] |
|
|
| 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}` |
|
|
| |
| 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: () => formatDateWithTimezone(new Date(), false) }), |
| winston.format.printf(({ level, message, timestamp, data }) => { |
| |
| 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 |
| }) |
|
|
| |
| 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, |
| 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, |
| 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 |
|
|