Skip to main content

Logging

Zelt provides a built-in Logger module with structured logging, configurable transports, and context propagation.

Basic Usage​

Inject the Logger into your services or controllers:

import { Injectable, inject, Logger } from '@zeltjs/core';

@Injectable()
export class OrderService {
  constructor(private logger = inject(Logger)) {}

  processOrder(orderId: string) {
    this.logger.info(`Processing order: ${orderId}`);

    try {
      // ... process order
      this.logger.debug('Order validation passed');
    } catch (error) {
      this.logger.error(`Failed to process order: ${orderId}`);
      throw error;
    }
  }
}

Log Levels​

The Logger supports four log levels in order of severity:

LevelMethodDescription
debuglogger.debug()Detailed debugging information
infologger.info()General informational messages
warnlogger.warn()Warning messages
errorlogger.error()Error messages

Messages are only output if their level is equal to or higher than the configured level. For example, with level: 'info', debug() messages are suppressed.

Structured Logging​

Pass context as the second argument to include structured data:

logger.info('Order processed', { orderId, userId, duration: 150 });
// Output: 13:45:23 INFO  Order processed {"orderId":"123","userId":"456","duration":150}

Child Loggers​

Create child loggers with bound context that persists across all log calls:

@Injectable()
export class OrderService {
  private logger: Logger;

  constructor(baseLogger = inject(Logger)) {
    this.logger = baseLogger.child({ service: 'OrderService' });
  }

  processOrder(orderId: string) {
    const orderLogger = this.logger.child({ orderId });
    orderLogger.info('Processing started');
    // Output includes: {"service":"OrderService","orderId":"123"}
  }
}

Global Context with withLogContext​

Use withLogContext to propagate context across async boundaries using AsyncLocalStorage:

This requires asynchronous context mode. In synchronous mode, the context is available only during the callback's synchronous execution. Use logger.child(bindings) to retain log bindings across awaits in either mode.

const logger = inject(Logger);

withLogContext({ requestId: 'abc-123' }, () => {
  logger.info('Request received');
  // Context is automatically included in all logs within this scope
  someService.process();
});

Configuration​

Basic Configuration​

Configure the Logger using LoggerConfig:

@Config
export class AppLoggerConfig extends LoggerConfig {
  constructor(
    private env = inject(Env),
    private consoleTransport = inject(ConsoleTransport),
    private jsonlFormatter = inject(JsonlFormatter),
  ) {
    super();
  }

  override get level(): LogLevel {
    return (this.env.getString('LOG_LEVEL') as LogLevel) ?? 'info';
  }

  override get transports(): readonly TransportBinding[] {
    return [{ transport: this.consoleTransport, formatter: this.jsonlFormatter }];
  }
}

Using PrettyFormatter​

For human-readable output in development, use PrettyFormatter:

@Config
export class DevLoggerConfig extends LoggerConfig {
  constructor(
    private consoleTransport = inject(ConsoleTransport),
    private prettyFormatter = inject(PrettyFormatter),
  ) {
    super();
  }

  override get level() {
    return 'debug' as const;
  }

  override get transports(): readonly TransportBinding[] {
    return [{ transport: this.consoleTransport, formatter: this.prettyFormatter }];
  }
}

PrettyFormatter outputs colored logs in TTY environments:

13:45:23 INFO  Order processed {"orderId":"123"}
13:45:23 ERROR Failed to process {"error":"timeout"}

Register the config when creating the app:

const app = createApp([http({
    controllers: [AppController],
  })], { configs: [AppLoggerConfig] });

Transports and Formatters​

The Logger uses a pluggable transport/formatter architecture:

ComponentDescription
ConsoleTransportWrites to stdout/stderr
JsonlFormatterJSON Lines format (one JSON object per line)
PrettyFormatterHuman-readable format with optional colors

Custom Transport​

Implement LoggerTransport for custom output destinations:

import type { LoggerTransport } from '@zeltjs/core';

export class FileTransport implements LoggerTransport {
  write(message: string): void {
    // Write to file
  }
}

Custom Formatter​

Implement LoggerFormatter for custom output formats:

import type { LoggerFormatter, LogEntry } from '@zeltjs/core';

export class CustomFormatter implements LoggerFormatter {
  format(entry: LogEntry): string {
    return `[${entry.level}] ${entry.message}`;
  }
}

Default Behavior​

Without custom configuration:

  • Level: 'info' (debug messages are suppressed)
  • Transport: ConsoleTransport
  • Formatter: JsonlFormatter