# ADR-004: Standardized Application-Wide Structured Logging **Date**: 2025-12-12 **Status**: Accepted **Implemented**: 2026-01-07 ## Context Our application currently uses a `logger` service, but the implementation of logging varies significantly across different modules. The `errorHandler` middleware produces high-quality, structured JSON logs for errors, but logging within route handlers and service layers is often ad-hoc, using plain strings or inconsistent object structures. This inconsistency leads to several problems: **Difficult Debugging**: It is hard to trace a single user request through the system or correlate events related to a specific operation. **Ineffective Log Analysis**: Inconsistent log formats make it difficult to effectively query, filter, and create dashboards in a log management system (like Datadog, Splunk, or the ELK stack). **Security Risks**: There is no enforced standard for redacting sensitive information (like passwords or tokens) in logs outside of the `errorHandler`, increasing the risk of accidental data exposure. **Missing Context**: Logs often lack crucial context, such as a unique request ID, the authenticated user's ID, or the source IP address, making them less useful for diagnosing issues. ## Decision We will adopt a standardized, application-wide structured logging policy. All log entries MUST be in JSON format and adhere to a consistent schema. **Request-Scoped Logger with Context**: We will create a middleware that runs at the beginning of the request lifecycle. This middleware will: - Generate a unique `request_id` for each incoming request. - Create a request-scoped logger instance (a "child logger") that automatically includes the `request_id`, `user_id` (if authenticated), and `ip_address` in every log message it generates. - Attach this child logger to the `req` object (e.g., `req.log`). **Mandatory Use of Request-Scoped Logger**: All route handlers and any service functions called by them **MUST** use the request-scoped logger (`req.log`) instead of the global logger instance. This ensures all logs for a given request are automatically correlated. **Standardized Log Schema**: All log messages should follow a base schema. The logger configuration will be updated to enforce this. **Base Fields**: `level`, `timestamp`, `message`, `request_id`, `user_id`, `ip_address`. **Error Fields**: When logging an error, the log entry MUST include an `error` object with `name`, `message`, and `stack`. **Standardized Logging Practices**: **INFO**: Log key business events, such as `User logged in` or `Flyer processed`. **WARN**: Log recoverable errors or unusual situations that do not break the request, such as `Client Error: 404 on GET /api/non-existent-route` or `Retrying failed database connection`. **ERROR**: Log only unhandled or server-side errors that cause a request to fail (typically handled by the `errorHandler`). Avoid logging expected client errors (like 4xx) at this level. **DEBUG**: Log detailed diagnostic information useful during development, such as function entry/exit points or variable states. ### Example Usage ```typescript // In a new middleware file: logger.middleware.ts import { logger } from '../services/logger.server'; import { randomUUID } from 'crypto'; export const requestLogger = (req, res, next) => { const requestId = randomUUID(); // Create a child logger with context for this request req.log = logger.child({ request_id: requestId, user_id: req.user?.user_id, // Assumes user is attached by auth middleware ip_address: req.ip, }); next(); }; // In server/app setup: // app.use(requestLogger); // Add this early in the middleware chain // In a route handler: router.get('/:id', async (req, res, next) => { // Use the request-scoped logger req.log.info({ flyerId: req.params.id }, 'Fetching flyer by ID'); try { // ... business logic ... res.json(flyer); } catch (error) { // The error itself will be logged with full context by the errorHandler next(error); } }); ``` ## Consequences ### Positive **Enhanced Observability**: Every log line from a single request can be instantly grouped and analyzed, dramatically speeding up debugging. **Improved Security**: Centralizing the addition of context (like `user_id`) reduces the chance of developers manually logging sensitive data. **Scalable Log Management**: Consistent JSON logs are easily ingested and indexed by any modern log aggregation tool. **Clearer Code**: Removes the need to manually pass contextual information (like user ID) down to service functions just for logging purposes. ### Negative **Refactoring Effort**: Requires adding the `requestLogger` middleware and refactoring all routes and services to use `req.log` instead of the global `logger`. **Slight Performance Overhead**: Creating a child logger for every request adds a minor performance cost, though this is negligible for most modern logging libraries. ## Implementation Details ### Logger Configuration Located in `src/services/logger.server.ts`: ```typescript import pino from 'pino'; const isProduction = process.env.NODE_ENV === 'production'; const isTest = process.env.NODE_ENV === 'test'; export const logger = pino({ level: isProduction ? 'info' : 'debug', transport: isProduction || isTest ? undefined : { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname', }, }, redact: { paths: [ 'req.headers.authorization', 'req.headers.cookie', '*.body.password', '*.body.newPassword', '*.body.currentPassword', '*.body.confirmPassword', '*.body.refreshToken', '*.body.token', ], censor: '[REDACTED]', }, }); ``` ### Request Logger Middleware Located in `server.ts`: ```typescript const requestLogger = (req: Request, res: Response, next: NextFunction) => { const requestId = randomUUID(); const user = req.user as UserProfile | undefined; const start = process.hrtime(); // Create request-scoped logger req.log = logger.child({ request_id: requestId, user_id: user?.user.user_id, ip_address: req.ip, }); req.log.debug({ method: req.method, originalUrl: req.originalUrl }, 'INCOMING'); res.on('finish', () => { const duration = getDurationInMilliseconds(start); const { statusCode, statusMessage } = res; const logDetails = { user_id: (req.user as UserProfile | undefined)?.user.user_id, method: req.method, originalUrl: req.originalUrl, statusCode, statusMessage, duration: duration.toFixed(2), }; // Include request details for failed requests (for debugging) if (statusCode >= 400) { logDetails.req = { headers: req.headers, body: req.body }; } if (statusCode >= 500) req.log.error(logDetails, 'Request completed with server error'); else if (statusCode >= 400) req.log.warn(logDetails, 'Request completed with client error'); else req.log.info(logDetails, 'Request completed successfully'); }); next(); }; app.use(requestLogger); ``` ### TypeScript Support The `req.log` property is typed via declaration merging in `src/types/express.d.ts`: ```typescript import { Logger } from 'pino'; declare global { namespace Express { export interface Request { log: Logger; } } } ``` ### Automatic Sensitive Data Redaction The Pino logger automatically redacts sensitive fields: ```json // Before redaction { "body": { "email": "user@example.com", "password": "secret123", "newPassword": "newsecret456" } } // After redaction (in logs) { "body": { "email": "user@example.com", "password": "[REDACTED]", "newPassword": "[REDACTED]" } } ``` ### Log Levels by Scenario | Level | HTTP Status | Scenario | | ----- | ----------- | -------------------------------------------------- | | DEBUG | Any | Request incoming, internal state, development info | | INFO | 2xx | Successful requests, business events | | WARN | 4xx | Client errors, validation failures, not found | | ERROR | 5xx | Server errors, unhandled exceptions | ### Service Layer Logging Services accept the request-scoped logger as an optional parameter: ```typescript export async function registerUser(email: string, password: string, reqLog?: Logger) { const log = reqLog || logger; // Fall back to global logger log.info({ email }, 'Registering new user'); // ... implementation log.debug({ userId: user.user_id }, 'User created successfully'); return user; } // In route handler router.post('/register', async (req, res, next) => { await authService.registerUser(req.body.email, req.body.password, req.log); }); ``` ### Log Output Format **Development** (pino-pretty): ```text [2026-01-09 12:34:56.789] INFO (request_id=abc123): Request completed successfully method: "GET" originalUrl: "/api/flyers" statusCode: 200 duration: "45.23" ``` **Production** (JSON): ```json { "level": 30, "time": 1704812096789, "request_id": "abc123", "user_id": "user_456", "ip_address": "192.168.1.1", "method": "GET", "originalUrl": "/api/flyers", "statusCode": 200, "duration": "45.23", "msg": "Request completed successfully" } ``` ### Routes Using `req.log` All route files have been migrated to use the request-scoped logger: - `src/routes/auth.routes.ts` - `src/routes/user.routes.ts` - `src/routes/flyer.routes.ts` - `src/routes/ai.routes.ts` - `src/routes/admin.routes.ts` - `src/routes/budget.routes.ts` - `src/routes/recipe.routes.ts` - `src/routes/gamification.routes.ts` - `src/routes/personalization.routes.ts` - `src/routes/stats.routes.ts` - `src/routes/health.routes.ts` - `src/routes/system.routes.ts` ## Key Files - `src/services/logger.server.ts` - Pino logger configuration - `src/services/logger.client.ts` - Client-side logger (for frontend) - `src/types/express.d.ts` - TypeScript declaration for `req.log` - `server.ts` - Request logger middleware ## Related ADRs - [ADR-001](./0001-standardized-error-handling.md) - Error handler uses `req.log` for error logging - [ADR-026](./0026-standardized-client-side-structured-logging.md) - Client-side logging strategy