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>
137 lines
3.4 KiB
TypeScript
137 lines
3.4 KiB
TypeScript
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;
|