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 lowLog Levels
| Level | Severity | Description |
|---|---|---|
debug | 0 | Diagnostic detail |
info | 1 | Normal operations |
warn | 2 | Potential issues |
error | 3 | Failures |
Transports only receive entries at or above their minLevel.
Setup
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:
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:
@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
logger.debug(message, context?) // Diagnostic detail
logger.info(message, context?) // Normal operations
logger.warn(message, context?) // Potential issues
logger.error(message, context?) // Failurescontext is an optional Record<string, unknown> included as structured data.
Transports
Console (@modularityjs/logger-console)
Writes to stdout (debug/info/warn) and stderr (error).
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.
import { LoggerFileModule } from '@modularityjs/logger-file';Three registration methods:
// 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:
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:
interface LogEntry {
readonly level: LogLevel;
readonly message: string;
readonly context?: Record<string, unknown>;
readonly channel: string;
readonly scope?: string;
readonly timestamp: Date;
}