Add comprehensive logging system with colored output

- 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 <noreply@anthropic.com>
This commit is contained in:
ggq-admin 2025-10-20 01:57:56 +02:00
parent fcd6fcf091
commit e4b1f73a46
3 changed files with 158 additions and 3 deletions

View file

@ -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;

View file

@ -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;
}

108
server/utils/logger.js Normal file
View file

@ -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 };