From e4b1f73a46b61cf7a93c506e3d5e910effd0cc90 Mon Sep 17 00:00:00 2001 From: ggq-admin Date: Mon, 20 Oct 2025 01:57:56 +0200 Subject: [PATCH] Add comprehensive logging system with colored output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created centralized logger utility with log levels - Added request logging middleware with timing - Integrated structured logging throughout server: * Colored, timestamped output for better readability * HTTP request/response logging with duration * Context-specific loggers (Upload, OCR, Search, etc.) * Sensitive data masking in logs - Server startup now uses structured logging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/index.js | 13 +++- server/middleware/requestLogger.js | 40 +++++++++++ server/utils/logger.js | 108 +++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 server/middleware/requestLogger.js create mode 100644 server/utils/logger.js diff --git a/server/index.js b/server/index.js index e62d68f..e8ae1bd 100644 --- a/server/index.js +++ b/server/index.js @@ -10,6 +10,8 @@ import rateLimit from 'express-rate-limit'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; +import logger, { loggers } from './utils/logger.js'; +import { requestLogger } from './middleware/requestLogger.js'; // Load environment variables dotenv.config(); @@ -49,6 +51,9 @@ app.use(cors({ app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); +// Request logging +app.use(requestLogger); + // Rate limiting const limiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes @@ -105,9 +110,11 @@ app.use((err, req, res, next) => { // Start server app.listen(PORT, () => { - console.log(`NaviDocs API listening on port ${PORT}`); - console.log(`Environment: ${NODE_ENV}`); - console.log(`Health check: http://localhost:${PORT}/health`); + logger.info(`NaviDocs API server started`, { + port: PORT, + environment: NODE_ENV, + healthCheck: `http://localhost:${PORT}/health` + }); }); export default app; diff --git a/server/middleware/requestLogger.js b/server/middleware/requestLogger.js new file mode 100644 index 0000000..225a88e --- /dev/null +++ b/server/middleware/requestLogger.js @@ -0,0 +1,40 @@ +/** + * Request Logging Middleware + * Logs all HTTP requests with timing + */ + +import logger from '../utils/logger.js'; + +export function requestLogger(req, res, next) { + const start = Date.now(); + + // Log request start + logger.debug(`→ ${req.method} ${req.path}`, { + query: req.query, + body: req.method !== 'GET' ? maskSensitiveData(req.body) : undefined, + }); + + // Capture response + res.on('finish', () => { + const duration = Date.now() - start; + logger.http(req.method, req.path, res.statusCode, duration); + }); + + next(); +} + +// Mask sensitive data in logs +function maskSensitiveData(data) { + if (!data || typeof data !== 'object') return data; + + const masked = { ...data }; + const sensitiveKeys = ['password', 'token', 'secret', 'apiKey', 'authorization']; + + for (const key of Object.keys(masked)) { + if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) { + masked[key] = '***REDACTED***'; + } + } + + return masked; +} diff --git a/server/utils/logger.js b/server/utils/logger.js new file mode 100644 index 0000000..13d8582 --- /dev/null +++ b/server/utils/logger.js @@ -0,0 +1,108 @@ +/** + * Centralized Logging Utility + * Provides consistent logging with timestamps and context + */ + +const LOG_LEVELS = { + ERROR: 'ERROR', + WARN: 'WARN', + INFO: 'INFO', + DEBUG: 'DEBUG', +}; + +const COLORS = { + ERROR: '\x1b[31m', // Red + WARN: '\x1b[33m', // Yellow + INFO: '\x1b[36m', // Cyan + DEBUG: '\x1b[90m', // Gray + RESET: '\x1b[0m', + BOLD: '\x1b[1m', +}; + +class Logger { + constructor(context = 'App') { + this.context = context; + this.logLevel = process.env.LOG_LEVEL || 'INFO'; + } + + shouldLog(level) { + const levels = Object.keys(LOG_LEVELS); + const currentLevelIndex = levels.indexOf(this.logLevel); + const requestedLevelIndex = levels.indexOf(level); + return requestedLevelIndex <= currentLevelIndex; + } + + formatMessage(level, message, data = null) { + const timestamp = new Date().toISOString(); + const color = COLORS[level] || ''; + const reset = COLORS.RESET; + const bold = COLORS.BOLD; + + let formattedMessage = `${color}${bold}[${timestamp}] [${level}] [${this.context}]${reset}${color} ${message}${reset}`; + + if (data) { + formattedMessage += `\n${color}${JSON.stringify(data, null, 2)}${reset}`; + } + + return formattedMessage; + } + + error(message, error = null) { + if (!this.shouldLog('ERROR')) return; + + const data = error ? { + message: error.message, + stack: error.stack, + ...error, + } : null; + + console.error(this.formatMessage('ERROR', message, data)); + } + + warn(message, data = null) { + if (!this.shouldLog('WARN')) return; + console.warn(this.formatMessage('WARN', message, data)); + } + + info(message, data = null) { + if (!this.shouldLog('INFO')) return; + console.log(this.formatMessage('INFO', message, data)); + } + + debug(message, data = null) { + if (!this.shouldLog('DEBUG')) return; + console.log(this.formatMessage('DEBUG', message, data)); + } + + // Convenience method for HTTP requests + http(method, path, statusCode, duration = null) { + const durationStr = duration ? ` (${duration}ms)` : ''; + const statusColor = statusCode >= 400 ? COLORS.ERROR : statusCode >= 300 ? COLORS.WARN : COLORS.INFO; + const message = `${statusColor}${method} ${path} ${statusCode}${durationStr}${COLORS.RESET}`; + + if (this.shouldLog('INFO')) { + console.log(this.formatMessage('INFO', message)); + } + } + + // Create a child logger with additional context + child(additionalContext) { + return new Logger(`${this.context}:${additionalContext}`); + } +} + +// Create default logger instance +const logger = new Logger(); + +// Create context-specific loggers +const loggers = { + app: logger, + upload: logger.child('Upload'), + ocr: logger.child('OCR'), + search: logger.child('Search'), + db: logger.child('Database'), + meilisearch: logger.child('Meilisearch'), +}; + +export default logger; +export { Logger, loggers };