All files / src/services logger.server.ts

78.48% Statements 135/172
68.08% Branches 32/47
100% Functions 8/8
78.98% Lines 109/138

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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 1391x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x         12x 2x 10x 1x   2x 2x 9x 2x 1x 1x 12x 12x 2x   2x 10x 10x 10x 6x   4x 2x   6x 6x   2x 1x 1x 12x 1x 1x 1x 1x   1x 2x     2x 2x 2x 2x 2x 12x 12x 2x 2x 12x 2x 2x   2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 10x 4x 1x 1x 4x 1x 2x 2x 2x 2x               4x     2x 2x 2x 2x 2x 2x 2x 6x   2x 2x 2x 1x 12x 1x 14x 1x 12x   5x   5x          
// src/services/logger.server.ts
/**
 * SERVER-SIDE LOGGER
 * This file configures and exports a singleton `pino` logger instance for
 * server-side use, adhering to ADR-004 for structured JSON logging.
 *
 * In production/test environments, logs are written to:
 * - stdout (for PM2 capture and real-time viewing)
 * - File: logs/app.log (for Logstash aggregation)
 *
 * Log files are stored in the application's logs/ directory:
 * - Production: /var/www/flyer-crawler.projectium.com/logs/
 * - Test: /var/www/flyer-crawler-test.projectium.com/logs/
 * - Dev container: /app/logs/
 */
import pino from 'pino';
import fs from 'fs';
import path from 'path';
 
const isProduction = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';
const isStaging = process.env.NODE_ENV === 'staging';
const isDevelopment = !isProduction && !isTest && !isStaging;

// Determine log directory based on environment
// In production/test, use the application directory's logs folder
// In development, use process.cwd()/logs
const getLogDirectory = (): string => {
  // Allow override via environment variable
  if (process.env.LOG_DIR) {
    return process.env.LOG_DIR;
  }
 
  // Default to logs/ in current working directory
  return path.join(process.cwd(), 'logs');
};
 
// Ensure log directory exists (only in production/test where we write files)
const ensureLogDirectory = (): string | null => {
  if (isDevelopment) {
    return null; // Don't create log files in development
  }
 
  const logDir = getLogDirectory();
  try {
    if (!fs.existsSync(logDir)) {
      fs.mkdirSync(logDir, { recursive: true });
    }
    return logDir;
  } catch (error) {
    // If we can't create the directory, fall back to stdout only
    console.error(`Failed to create log directory ${logDir}:`, error);
    return null;
  }
};
 
// Common redaction configuration
const redactConfig = {
  paths: [
    'req.headers.authorization',
    'req.headers.cookie',
    '*.body.password',
    '*.body.newPassword',
    '*.body.currentPassword',
    '*.body.confirmPassword',
    '*.body.refreshToken',
    '*.body.token',
  ],
  censor: '[REDACTED]',
};
 
// Create the logger based on environment
const createLogger = (): pino.Logger => {
  const logDir = ensureLogDirectory();
 
  // Development: Use pino-pretty for human-readable output
  if (isDevelopment) {
    return pino({
      level: 'debug',
      transport: {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'SYS:standard',
          ignore: 'pid,hostname',
        },
      },
      redact: redactConfig,
    });
  }
 
  // Production/Test: Write to both stdout and file
  if (logDir) {
    const logFilePath = path.join(logDir, 'app.log');
 
    // Create a multi-stream destination
    const streams: pino.StreamEntry[] = [
      // Stream to stdout (for PM2 and real-time viewing)
      { stream: process.stdout },
      // Stream to file (for Logstash aggregation)
      {
        stream: pino.destination({
          dest: logFilePath,
          sync: false, // Async for better performance
          mkdir: true, // Create directory if needed
        }),
      },
    ];

    return pino(
      {
        level: isProduction ? 'info' : 'debug',
        redact: redactConfig,
      },
      pino.multistream(streams),
    );
  }
 
  // Fallback: stdout only (if log directory creation failed)
  return pino({
    level: isProduction ? 'info' : 'debug',
    redact: redactConfig,
  });
};
 
export const logger = createLogger();
 
const debugModules = (process.env.DEBUG_MODULES || '').split(',').map((s) => s.trim());
 
export const createScopedLogger = (moduleName: string) => {
  // If DEBUG_MODULES contains "ai-service" or "*", force level to 'debug'
  const isDebugEnabled = debugModules.includes('*') || debugModules.includes(moduleName);

  return logger.child({
    module: moduleName,
    level: isDebugEnabled ? 'debug' : logger.level,
  });
};