Skip to content

Logger

@voltage/logger replaces the default NestJS logger with a Winston-backed implementation. It assigns a correlation ID to every HTTP request and propagates it automatically through async context, so all logs within a request are traceable without any manual wiring.

Terminal window
yarn add @voltage/logger @voltage/async-context @voltage/event-manager @voltage/zod zod

Import LoggerModule once in your root application module. Because it is registered as global, you do not need to import it in feature modules.

import { LoggerModule } from '@voltage/logger';
@Module({
imports: [
LoggerModule.register({
level: 'info',
application: 'my-app',
}),
],
})
export class AppModule {}

Use a factory to pull configuration from an injected provider — the typical approach when using a configuration class:

LoggerModule.registerAsync({
inject: [AppConfiguration],
useFactory: (configuration: AppConfiguration) => ({
...configuration.logger,
application: 'my-app',
}),
})

Inject Logger into any service:

import { Injectable } from '@nestjs/common';
import { Logger } from '@voltage/logger';
@Injectable()
export class OrderService {
constructor(private readonly logger: Logger) {}
async create(order: Order) {
void this.logger.push('info', 'order.create', {
type: 'order.create',
order: { id: order.id },
});
}
}

In main.ts, connect LoggerFacade as the NestJS application logger so framework-internal logs (bootstrapping, route registration) also go through Winston:

import { LoggerFacade } from '@voltage/logger';
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(LoggerFacade));
app.flushLogs();

Levels in order from most to least verbose: verbose, debug, info, warn, error, fatal.

You can change the active level at runtime:

this.logger.setLevel('warn');

Pass a metadata object as the third argument to push(). Use type as a dot-namespaced identifier for the event, and group contextual data under a named key matching the domain:

void this.logger.push('debug', 'Processing export', {
type: 'exporter.write',
exporter: {
path,
writeCount: results.length,
},
});

Pass an error key to attach exception information. The logger normalizes the error to message and stack only:

void this.logger.push('error', 'Export failed', {
type: 'exporter.write',
error,
exporter: { path },
});

LoggerModule registers middleware on all routes automatically. For every request it:

  • Assigns a UUID v7 correlation ID and attaches it to req.correlationId
  • Emits an http.begin log with request metadata (method, URL, IP, headers)
  • Emits an http.end log on response with status code, matched route, and duration

You do not need to configure this — it is active as soon as you import the module.

[MM-DD-YYYY HH:mm:ss][CORRELATION_ID][LEVEL][+DELTA_MS] message

+DELTA_MS is the time elapsed since the previous log within the same request, useful for spotting slow steps without external tooling.

Pass any Winston-compatible transport via the transports option. Additional transports receive all metadata as JSON, independent of the logMetadata console setting:

import { transports } from 'winston';
LoggerModule.register({
level: 'info',
application: 'my-app',
transports: [
new transports.File({ filename: 'app.log' }),
],
})
interface LoggerConfiguration {
/** Minimum log level */
level: 'verbose' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
/** Application name included in every log entry */
application: string
/** Additional Winston transports appended to the default console transport */
transports?: Transport[]
/** Metadata inspection depth in console output. true for full depth, a number for a specific limit */
logMetadata?: boolean | number
}