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:
parent
fcd6fcf091
commit
e4b1f73a46
3 changed files with 158 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
40
server/middleware/requestLogger.js
Normal file
40
server/middleware/requestLogger.js
Normal 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
108
server/utils/logger.js
Normal 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 };
|
||||
Loading…
Add table
Reference in a new issue