All files / src/middleware requestLogger.js

100% Statements 34/34
91.66% Branches 22/24
100% Functions 4/4
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                  1x   1x                               18x 15x 15x 23x 8x   15x                   15x   15x 1x     14x     14x 14x     14x 14x   14x     14x 11x 11x 11x   11x 11x 10x   11x 11x 11x   11x                                             14x       1x  
/**
 * Request/response logging middleware with trace ID propagation.
 *
 * Usage:
 *   const logger = require('../utils/loggerInstance');
 *   const { createRequestLogger } = require('./requestLogger');
 *   app.use(createRequestLogger(logger));
 */
 
const crypto = require('node:crypto');
 
const SENSITIVE_BODY_KEYS = new Set([
  'password',
  'token',
  'idtoken',
  'accesstoken',
  'refreshtoken',
  'secret',
  'credential',
  'pin',
  'code',
]);
 
/**
 * Strip sensitive fields from request body (shallow clone, one level deep).
 */
function sanitizeBody(body) {
  if (!body || typeof body !== 'object') return body;
  const clean = {};
  for (const [key, value] of Object.entries(body)) {
    if (SENSITIVE_BODY_KEYS.has(key.toLowerCase())) continue;
    clean[key] = value;
  }
  return clean;
}
 
/**
 * Create Express middleware that logs every request/response.
 *
 * @param {object} logger - Logger instance with a `log(entry)` method.
 * @returns {Function} Express middleware
 */
function createRequestLogger(logger) {
  return function requestLoggerMiddleware(req, res, next) {
    // Skip logging for health checks — they add ~1,440 writes/day for no value
    if (req.path === '/api/health') {
      return next();
    }
 
    const startTime = Date.now();
 
    // Generate request trace ID
    const requestTraceId = crypto.randomBytes(16).toString('hex');
    const sessionTraceId = req.headers['x-session-trace-id'] || null;
 
    // Attach to request for downstream use
    req.requestTraceId = requestTraceId;
    req.sessionTraceId = sessionTraceId;
 
    res.setHeader('x-request-trace-id', requestTraceId);
 
    // Log after response completes
    res.on('finish', () => {
      try {
        const durationMs = Date.now() - startTime;
        const statusCode = res.statusCode;
 
        let level = 'INFO';
        if (statusCode >= 500) level = 'ERROR';
        else if (statusCode >= 400) level = 'WARN';
 
        const method = req.method;
        const path = req.originalUrl || req.url;
        const message = `${method} ${path} ${statusCode} ${durationMs}ms`;
 
        logger.log({
          level,
          source: 'http',
          message,
          requestTraceId,
          sessionTraceId,
          userId: req.auth?.uid || null,
          context: {
            method,
            path,
            statusCode,
            durationMs,
            requestBody:
              req.body !== null && req.body !== undefined ? sanitizeBody(req.body) : null,
            userAgent: req.headers['user-agent'] || null,
          },
        });
      } catch {
        // Intentionally swallowed — request logging must never throw to avoid disrupting HTTP responses
      }
    });
 
    // Never block the request
    next();
  };
}
 
module.exports = { createRequestLogger, sanitizeBody };