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' }),
],
})

Set the elastic option to ship logs to Elasticsearch without wiring the transport yourself. You pass the connection details; the logger constructs the winston-elasticsearch transport with dataStream enabled and appends it to the console transport.

LoggerModule.register({
level: 'info',
application: 'my-app',
elastic: {
node: 'https://elastic.internal:9200',
index: 'volta-logs-production-my-app',
apiKey: process.env.ELASTIC_API_KEY,
},
})

The index is the full index name - you own its naming, including any environment prefix. apiKey is optional; omit it when the client authenticates another way.

TLS certificate validation is disabled by default to support self-signed internal clusters. Override any client setting through clientOpts, which is shallow-merged over the defaults - a tls, auth, or node key you pass replaces the default wholesale:

elastic: {
node: 'https://elastic.internal:9200',
index: 'volta-logs-production-my-app',
clientOpts: {
auth: { username: 'logger', password: process.env.ELASTIC_PASSWORD },
tls: { rejectUnauthorized: true },
},
}
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
/** Ship logs to Elasticsearch through a managed winston-elasticsearch transport */
elastic?: {
/** Elasticsearch node URL */
node: string
/** Full index name, including any environment prefix */
index: string
/** API key authentication. Omit when authenticating through clientOpts */
apiKey?: string
/** Elasticsearch client options, shallow-merged over the defaults */
clientOpts?: ClientOptions
}
}