feat: implement comprehensive logging infrastructure (Phases 1-3)
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m50s
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m50s
Added production-ready logging using Pino with structured JSON output, pretty printing in development, and automatic sensitive data redaction. ## Phase 1: Core Logger Setup - Installed pino, pino-http, and pino-pretty dependencies - Created logger utility (lib/utils/logger.ts): - Environment-based log levels (debug in dev, info in prod) - Pretty printing with colors in development - JSON structured logs in production - Sensitive data redaction (passwords, tokens, auth headers) - Custom serializers for errors and requests - Helper functions for child loggers and timing - Added LOG_LEVEL environment variable to .env.example - Configured Next.js for Turbopack with external pino packages ## Phase 2: API Request Logging - Created API logger wrapper (lib/utils/api-logger.ts): - withLogging() HOF for wrapping API route handlers - Automatic request/response logging with timing - Correlation ID generation (X-Request-ID header) - Error catching and structured error responses - logPerformance() helper for timing operations - createApiLogger() for manual logging in routes ## Phase 3: Supervisor Client Logging - Updated lib/supervisor/client.ts: - Added logger instance to SupervisorClient class - Comprehensive XML-RPC call logging (method, params, duration) - Error logging with full context and stack traces - Success logging with result size tracking - DEBUG level logs for all XML-RPC operations - Constructor logging for client initialization ## Configuration Changes - Updated next.config.ts for Turbopack compatibility - Added serverComponentsExternalPackages for pino - Removed null-loader workaround (not needed) ## Features Implemented ✅ Request correlation IDs for tracing ✅ Performance timing for all operations ✅ Sensitive data redaction (passwords, auth) ✅ Environment-based log formatting ✅ Structured JSON logs for production ✅ Pretty colored logs for development ✅ Error serialization with stack traces ✅ Ready for log aggregation (stdout/JSON) ## Next Steps (Phases 4-7) - Update ~30 API routes with logging - Add React Query/hooks error logging - Implement client-side error boundary - Add documentation and testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
136
lib/utils/logger.ts
Normal file
136
lib/utils/logger.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import pino from 'pino';
|
||||
|
||||
// Determine environment
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const logLevel = (process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info')) as pino.Level;
|
||||
|
||||
// Custom serializers for better log output
|
||||
const serializers = {
|
||||
error: pino.stdSerializers.err,
|
||||
req: (req: any) => ({
|
||||
id: req.id,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
remoteAddress: req.headers?.['x-forwarded-for'] || req.socket?.remoteAddress,
|
||||
userAgent: req.headers?.['user-agent'],
|
||||
}),
|
||||
res: (res: any) => ({
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
};
|
||||
|
||||
// Sensitive fields to redact from logs
|
||||
const redactPaths = [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'apiKey',
|
||||
'authorization',
|
||||
'*.password',
|
||||
'*.token',
|
||||
'*.secret',
|
||||
'*.apiKey',
|
||||
'*.authorization',
|
||||
'req.headers.authorization',
|
||||
'req.headers.cookie',
|
||||
'SUPERVISOR_PASSWORD',
|
||||
'SUPERVISOR_USERNAME',
|
||||
];
|
||||
|
||||
// Create base logger
|
||||
export const logger = pino({
|
||||
level: logLevel,
|
||||
// Pretty printing in development, JSON in production
|
||||
transport: isDevelopment
|
||||
? {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'HH:MM:ss',
|
||||
ignore: 'pid,hostname',
|
||||
singleLine: false,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
// Redact sensitive information
|
||||
redact: {
|
||||
paths: redactPaths,
|
||||
censor: '[REDACTED]',
|
||||
},
|
||||
// Custom serializers
|
||||
serializers,
|
||||
// Add timestamp in production
|
||||
timestamp: isDevelopment ? false : pino.stdTimeFunctions.isoTime,
|
||||
// Base context
|
||||
base: isDevelopment
|
||||
? undefined
|
||||
: {
|
||||
pid: process.pid,
|
||||
hostname: process.env.HOSTNAME || 'unknown',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a child logger with additional context
|
||||
* @param context - Additional context to add to all log messages
|
||||
* @returns Child logger instance
|
||||
*/
|
||||
export function createLogger(context: Record<string, any>) {
|
||||
return logger.child(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique request ID
|
||||
* @returns Unique request identifier
|
||||
*/
|
||||
export function generateRequestId(): string {
|
||||
return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger for API operations
|
||||
* @param operation - The operation name (e.g., 'startProcess', 'stopProcess')
|
||||
* @param metadata - Additional metadata
|
||||
* @returns Child logger with operation context
|
||||
*/
|
||||
export function createOperationLogger(operation: string, metadata: Record<string, any> = {}) {
|
||||
return createLogger({
|
||||
operation,
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log timing information for an operation
|
||||
* @param logger - Logger instance
|
||||
* @param operation - Operation name
|
||||
* @param startTime - Start time from performance.now() or Date.now()
|
||||
*/
|
||||
export function logTiming(logger: pino.Logger, operation: string, startTime: number) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info({ operation, duration }, `${operation} completed in ${duration}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error for logging
|
||||
* @param error - Error object or message
|
||||
* @returns Formatted error object
|
||||
*/
|
||||
export function formatError(error: unknown): { message: string; stack?: string; code?: string } {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: (error as any).code,
|
||||
};
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return { message: error };
|
||||
}
|
||||
return { message: 'Unknown error', stack: JSON.stringify(error) };
|
||||
}
|
||||
|
||||
// Export default logger
|
||||
export default logger;
|
||||
Reference in New Issue
Block a user