All files / src/app/core/utils/ssr-logging ssr-logging.service.ts

94.87% Statements 37/39
85% Branches 17/20
100% Functions 10/10
94.87% Lines 37/39

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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 18918x 18x 18x                         18x                                                     18x 18x                   36x 36x                   18x               18x                     18x 18x 18x   18x           18x   17x     17x   102x       17x 17x 17x 17x     17x         1x     6x                           18x   18x 1x 1x                         17x                         19x 18x 18x 18x   19x                                   18x 19x    
import ecsFormat from '@elastic/ecs-pino-format';
import pino, { Logger, LoggerOptions } from 'pino';
import pinoPretty from 'pino-pretty';
 
/**
 * SSR Logging Service
 *
 * Provides structured logging capabilities for the SSR (Server-Side Rendering) environment
 * using Pino logger with support for ECS (Elastic Common Schema) formatting.
 *
 */
 
/**
 * Supported log levels, matches Pino's standard log levels.
 */
const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const;
 
type LogLevel = (typeof LOG_LEVELS)[number];
 
/**
 * Supported log output formats.
 * - `json`: ECS-compliant JSON format for production/log aggregation
 * - `text`: Pretty-printed text format for development
 */
type LogFormat = 'json' | 'text';
 
interface LoggerConfig {
  // Log level threshold, messages below this level are not logged
  level?: LogLevel;
  // Output format for log messages
  format?: LogFormat;
  // Logger name, included in log output for identification
  name?: string;
}
 
/**
 * Retrieves the log level from the `LOGLEVEL` environment variable.
 * Falls back to 'error' if not set or invalid.
 *
 * @returns The configured log level or 'error' as default
 */
function getLogLevel(): LogLevel {
  const level = process.env.LOGLEVEL?.toLowerCase() as LogLevel;
  return LOG_LEVELS.includes(level) ? level : 'error';
}
 
/**
 * Retrieves the log format from the `LOGFORMAT` environment variable.
 * Falls back to 'json' if not set or invalid.
 *
 * @returns The configured log format or 'json' as default
 */
function getLogFormat(): LogFormat {
  const format = process.env.LOGFORMAT?.toLowerCase() as LogFormat;
  return format === 'text' ? 'text' : 'json';
}
 
/**
 * Builds PM2 process manager context if running under PM2.
 * Includes process ID and name for identifying log sources in clustered environments.
 *
 * @returns PM2 context object with id and name, or undefined if not running under PM2
 */
function getPM2Context(): Record<string, unknown> | undefined {
  Iif (process.env.pm_id && process.env.name) {
    return {
      pm2: {
        id: process.env.pm_id,
        name: process.env.name,
      },
    };
  }
  return;
}
 
/**
 * Creates Pino logger options based on the provided configuration.
 * Applies ECS formatting for JSON output and handles PM2 context injection.
 *
 * @param config - Logger configuration options
 * @returns Pino LoggerOptions configured for the specified format and level
 */
function createLoggerOptions(config: LoggerConfig): LoggerOptions {
  const level = config.level || getLogLevel();
  const format = config.format || getLogFormat();
  const pm2Context = getPM2Context();
 
  const baseOptions: LoggerOptions = {
    level,
    name: config.name,
    base: pm2Context ? { ...pm2Context } : undefined,
  };
 
  if (format === 'json') {
    // Disable Elastic APM agent integration to avoid unnecessary lookups when APM is not in use
    const ecsOptions = ecsFormat({ apmIntegration: false });
 
    // Uppercase the log level using ECS field name
    ecsOptions.formatters = {
      ...ecsOptions.formatters,
      level: (label: string) => ({ 'log.level': label.toUpperCase() }),
    };
 
    // Fix for webpack-bundled environment where bindings may be undefined
    const originalBindingsFormatter = ecsOptions.formatters?.bindings;
    if (originalBindingsFormatter) {
      ecsOptions.formatters.bindings = (bindings: Record<string, unknown> | undefined) =>
        originalBindingsFormatter(bindings || {});
    }
 
    return {
      ...baseOptions,
      ...ecsOptions,
    };
  } else {
    return {
      ...baseOptions,
      formatters: {
        level: (label: string) => ({ level: label.toUpperCase() }),
      },
    };
  }
}
 
/**
 * Creates the Pino destination stream for log output.
 * Outputs to stdout (fd 1) for Docker/PM2 compatibility.
 * Uses pino-pretty for text format with colorized, single-line output.
 *
 * @returns Pino destination stream configured for the current log format
 */
function createLoggerDestination(): pino.DestinationStream {
  const format = getLogFormat();
 
  if (format === 'text') {
    try {
      return pinoPretty({
        colorize: true,
        translateTime: 'SYS:standard',
        singleLine: true,
        errorLikeObjectKeys: [], // Disable special error formatting with new lines to keep single line
        destination: 1, // stdout
        sync: true, // synchronous for SSR compatibility
      });
    } catch {
      // If pino-pretty is not available, fall back to stdout
      return pino.destination(1);
    }
  }
  return pino.destination(1);
}
 
// Singleton logger instance, lazily initialized on first use.
let loggerInstance: Logger | undefined;
 
/**
 * Gets or creates the singleton Pino logger instance.
 * Creates the logger on first call and returns the cached instance on subsequent calls.
 *
 * @returns The singleton Pino Logger instance
 */
function getOrCreateLogger(): Logger {
  if (!loggerInstance) {
    const options = createLoggerOptions({});
    const destination = createLoggerDestination();
    loggerInstance = pino(options, destination);
  }
  return loggerInstance;
}
 
/**
 * Creates a child logger for the given context.
 * The context is included in log output under the `log.logger` field,
 * making it easy to filter logs by context.
 *
 * @param context - The context requesting the logger
 * @returns A Pino child logger instance with the context bound
 *
 * @example
 * ```typescript
 * const logger = getLogger('SSRErrorHandler');
 * logger.error({ ... }, 'HTTP ERROR');
 * // Output includes: { "log.logger": "SSRErrorHandler", "log.level": "ERROR", "message": "HTTP ERROR" }
 * ```
 */
export function getLogger(context: string): Logger {
  return getOrCreateLogger().child({ 'log.logger': context });
}