/** * Logging Middleware for API Routes * Agent 4: Production Deployment * * Provides request/response logging middleware for Next.js API routes. */ import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next'; import { createLogger, Logger } from './logger'; interface RequestLogContext { requestId: string; method: string; path: string; query?: Record; userAgent?: string; ip?: string; } interface ResponseLogContext extends RequestLogContext { statusCode: number; duration: number; } /** * Generate a unique request ID */ function generateRequestId(): string { return `req_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`; } /** * Get client IP from request headers */ function getClientIp(req: NextApiRequest): string { const forwarded = req.headers['x-forwarded-for']; if (typeof forwarded === 'string') { return forwarded.split(',')[0].trim(); } if (Array.isArray(forwarded)) { return forwarded[0]; } return req.socket?.remoteAddress || 'unknown'; } /** * Sanitize headers for logging (remove sensitive data) */ function sanitizeHeaders(headers: Record): Record { const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; const sanitized: Record = {}; for (const [key, value] of Object.entries(headers)) { if (sensitiveHeaders.includes(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'string') { sanitized[key] = value; } else if (Array.isArray(value)) { sanitized[key] = value.join(', '); } } return sanitized; } /** * Request logging middleware */ export function withLogging(handler: NextApiHandler): NextApiHandler { return async (req: NextApiRequest, res: NextApiResponse) => { const startTime = Date.now(); const requestId = generateRequestId(); const logger = createLogger({ requestId }); // Extract request information const requestContext: RequestLogContext = { requestId, method: req.method || 'UNKNOWN', path: req.url || '/', query: req.query as Record, userAgent: req.headers['user-agent'], ip: getClientIp(req), }; // Log incoming request logger.info('Incoming request', { ...requestContext, headers: sanitizeHeaders(req.headers as Record), }); // Add request ID to response headers res.setHeader('X-Request-Id', requestId); // Capture the original end method const originalEnd = res.end; let responseLogged = false; // Override end to log response res.end = function (this: NextApiResponse, ...args: Parameters) { if (!responseLogged) { responseLogged = true; const duration = Date.now() - startTime; const responseContext: ResponseLogContext = { ...requestContext, statusCode: res.statusCode, duration, }; // Log based on status code if (res.statusCode >= 500) { logger.error('Request completed with server error', responseContext); } else if (res.statusCode >= 400) { logger.warn('Request completed with client error', responseContext); } else { logger.info('Request completed', responseContext); } } return originalEnd.apply(this, args); } as typeof originalEnd; try { // Execute the handler await handler(req, res); } catch (error) { const duration = Date.now() - startTime; // Log error logger.error( 'Request failed with exception', error instanceof Error ? error : new Error(String(error)), { ...requestContext, duration, } ); // Re-throw to let Next.js handle the error throw error; } }; } /** * Create a logger with request context for use within API handlers */ export function getRequestLogger(req: NextApiRequest): Logger { const requestId = (req.headers['x-request-id'] as string) || generateRequestId(); return createLogger({ requestId, method: req.method, path: req.url, }); } // Export types export type { RequestLogContext, ResponseLogContext };