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 dotenv from 'dotenv';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
|
import logger, { loggers } from './utils/logger.js';
|
||||||
|
import { requestLogger } from './middleware/requestLogger.js';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
@ -49,6 +51,9 @@ app.use(cors({
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Request logging
|
||||||
|
app.use(requestLogger);
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
|
||||||
|
|
@ -105,9 +110,11 @@ app.use((err, req, res, next) => {
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`NaviDocs API listening on port ${PORT}`);
|
logger.info(`NaviDocs API server started`, {
|
||||||
console.log(`Environment: ${NODE_ENV}`);
|
port: PORT,
|
||||||
console.log(`Health check: http://localhost:${PORT}/health`);
|
environment: NODE_ENV,
|
||||||
|
healthCheck: `http://localhost:${PORT}/health`
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
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