Skip to content

Logger

Architecture

The logger uses a transport-pool pattern. LoggerService dispatches log entries to all registered transports. Each transport filters by channel and log level, so different transports can handle different types of logs independently.

LoggerService.info('request processed', { statusCode: 200 })

  ├── ConsoleTransport (channels: ['default'], minLevel: 'info')  ✓ matches
  ├── FileTransport    (channels: ['audit'],   minLevel: 'debug') ✗ wrong channel
  └── FileTransport    (channels: '*',         minLevel: 'error') ✗ level too low

Log Levels

LevelSeverityDescription
debug0Diagnostic detail
info1Normal operations
warn2Potential issues
error3Failures

Transports only receive entries at or above their minLevel.

Setup

typescript
import { LoggerModule } from '@modularityjs/logger';
import { LoggerConsoleModule } from '@modularityjs/logger-console';
import { LoggerFileModule } from '@modularityjs/logger-file';

const modules = [
  LoggerModule,
  LoggerConsoleModule,
  // Add file transports as needed:
  LoggerFileModule.forRoot({
    path: './logs/app.log',
    channels: ['default'],
    minLevel: 'info',
  }),
];

Usage

Scoped Loggers

Use LoggerFactory.create(scope) for a logger with a scope label on the default channel:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { LoggerFactory, LoggerService } from '@modularityjs/logger';

@Injectable()
class OrderService {
  private readonly logger: LoggerService;

  constructor(@Inject(LoggerFactory) factory: LoggerFactory) {
    this.logger = factory.create('order-service');
  }

  placeOrder(orderId: string) {
    this.logger.info('Order placed', { orderId });
    // Output: 2026-04-11T12:00:00.000Z [INFO] [order-service] Order placed {"orderId":"ORD-1"}
  }
}

Named Channels

Use LoggerFactory.createChannel(channel) to route logs to specific transports:

typescript
@Injectable()
class PaymentService {
  private readonly logger: LoggerService;

  constructor(@Inject(LoggerFactory) factory: LoggerFactory) {
    this.logger = factory.createChannel('payment', 'payment-service');
  }

  charge(amount: number) {
    this.logger.info('Charge processed', { amount });
    // Only transports with channels: ['payment'] or channels: '*' receive this
  }
}

Logging Methods

typescript
logger.debug(message, context?)   // Diagnostic detail
logger.info(message, context?)    // Normal operations
logger.warn(message, context?)    // Potential issues
logger.error(message, context?)   // Failures

context is an optional Record<string, unknown> included as structured data.

Transports

Console (@modularityjs/logger-console)

Writes to stdout (debug/info/warn) and stderr (error).

typescript
import { LoggerConsoleModule } from '@modularityjs/logger-console';

const modules = [
  LoggerModule,
  LoggerConsoleModule, // channels: ['default'], minLevel: 'debug'
];

Default configuration: logs the default channel at debug level and above.

File (@modularityjs/logger-file)

Appends to files on disk. Creates parent directories on first write.

typescript
import { LoggerFileModule } from '@modularityjs/logger-file';

Three registration methods:

typescript
// Log specific channels to a file
LoggerFileModule.forRoot({
  path: './logs/app.log',
  channels: ['default', 'http'],
  minLevel: 'info',
});

// Log a single channel to its own file
LoggerFileModule.forChannel('audit', {
  path: './logs/audit.log',
  minLevel: 'debug',
});

// Log all channels to one file (wildcard)
LoggerFileModule.forAll({
  path: './logs/all.log',
  minLevel: 'warn',
});

OpenTelemetry (@modularityjs/logger-telemetry)

Registers OtelLogTransport, which attaches each log entry as an addEvent on the active OTEL span (no-op when no span is active). Wildcard channel, debug minLevel — runs in parallel with console/file transports.

Custom Transports

Extend LogTransport and register via the LogTransportPool:

typescript
import { Injectable } from '@modularityjs/di';
import { LogTransport, LogTransportPool } from '@modularityjs/logger';
import type { LogEntry, LogLevel } from '@modularityjs/logger';
import { Module } from '@modularityjs/modularity';

@Injectable()
class SlackTransport extends LogTransport {
  readonly channels = '*' as const;
  readonly minLevel: LogLevel = 'error';

  log(entry: LogEntry) {
    // Send to Slack webhook...
  }
}

@Module({
  name: 'logger-slack',
  imports: [LoggerModule],
  providers: [SlackTransport],
  pools: [{ pool: LogTransportPool, key: 'slack', useClass: SlackTransport }],
})
class LoggerSlackModule {}

Log Entry Structure

Every log entry has the following shape:

typescript
interface LogEntry {
  readonly level: LogLevel;
  readonly message: string;
  readonly context?: Record<string, unknown>;
  readonly channel: string;
  readonly scope?: string;
  readonly timestamp: Date;
}