Package Exports
- nestjs-context-winston
- nestjs-context-winston/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (nestjs-context-winston) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Nestjs Context Logger
Contextual logging library for NestJS applications based on AsyncLocalStorage with third party enricher support.
Features
- đ Native NestJS integration - Ready-to-use module
- đ Contextual logging - Automatic logs with transaction information using AsyncLocalStorage
- đ Third party enrichment support - Integrate with New Relic using the
@newrelic/log-enricher
package or others! - ⥠Performance - Efficient logs with per-request metadata accumulation
- đ Type-safe - Fully typed in TypeScript with standardized metadata
- đ¯ Standardized metadata - Full control over accepted metadata fields
Best Practices
Use addMeta/addMetas instead of multiple logs
// â
Correct - accumulate metadata and log once
this.logger.addMeta('userId', '123');
this.logger.addMeta('operation', 'login');
// â Avoid - multiple logs
this.logger.info('Starting login', { userId: '123' });
this.logger.info('Login performed', { operation: 'login' });
3. Automatic Per-Request Logging
By default, this library includes a RequestLoggerInterceptor
that automatically generates a single structured log for every request. This means you do not need to manually call a log method in your controllers or services for each request. Instead, you can simply use addMeta
, addMetas
, or incMeta
throughout your request handling to accumulate metadata, and the interceptor will log everything at the end of the request.
Disabling the Automatic Request Log
If you want to disable this automatic per-request log (for example, if you want to handle logging manually), you can do so in two ways:
- Via options:
Set
useLogInterceptor: false
in the options passed toContextLoggingModule.forRoot
.ContextLoggingModule.forRoot({ logClass: AppLogger, useLogInterceptor: false, })
- Via environment variable:
Set the environment variable
AUTO_REQUEST_LOG=false
(as a string) to disable the interceptor globally.
By default, the automatic request log is enabled.
Defining log level
You can de define log level in two ways:
- Via options:
Set
logLevel: debug | info | warn | error
in the options passed toContextLoggingModule.forRoot
.ContextLoggingModule.forRoot({ logClass: AppLogger, logLevel: LogLevel.warn, })
- Via environment variable:
Set the environment variable
LOG_LEVEL=warn
to disable the interceptor globally.
Installation
npm install nestjs-context-winston
Configuration
1. Define the metadata class
First, create an interface/class that defines the accepted metadata in the logs:
// src/logging/metadata.interface.ts
export interface AppLoggerMetadata {
userId?: string;
requestId?: string;
operation?: string;
duration?: number;
statusCode?: number;
error?: string;
// Add other fields as needed
}
2. Create your custom logger
Extend ContextLogger
with your metadata interface (for standardization):
// src/logging/app-logger.service.ts
import { BaseContextLogger } from 'nestjs-context-winston';
import { AppLoggerMetadata } from './metadata.interface';
export class AppLogger extends bASEContextLogger<AppLoggerMetadata> { }
3. Configure the application module
Set up the logger as a global provider:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
export loggingModule = ContextLoggingModule.forRoot({
logClass: AppLogger,
});
@Module({
imports: [loggingModule],
})
export class AppModule {}
// In your main.ts:
import { ContextNestLogger } from 'nestjs-context-winston';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
{
// Replace the global NestJS logger with your contextual logger
logger: loggingModule
}
);
await app.listen(3000);
}
bootstrap();
âšī¸ Automatic context: The module automatically registers a global guard to capture the context of all requests. Metadata accumulated with
addMeta()
andaddMetas()
is isolated per request - each request maintains its own independent context.
Basic Usage
Logger Injection
Your custom logger will be used as the injection symbol throughout the application:
import { Injectable } from '@nestjs/common';
import { AppLogger } from '../logging/app-logger.service';
@Injectable()
export class UserService {
constructor(private readonly logger: AppLogger) {}
async findUser(id: string) {
// Add individual metadata to the context (without logging yet)
this.logger.addMeta('userId', id);
this.logger.addMeta('operation', 'find_user');
try {
const user = await this.userRepository.findById(id);
// Increment a counter in the context
this.logger.incMeta('queries_executed');
// Add multiple metadata at once
this.logger.addMetas({
userName: user.name,
userType: user.type
});
return user;
} catch (error) {
// You can also pass metadata directly in the log
this.logger.addMeta('errorCode', error.code);
throw error;
}
}
}
Contextual Logging
How Context Works
The library uses AsyncLocalStorage to manage metadata context throughout the request.
What is AsyncLocalStorage? It's a native Node.js API that allows you to create a "repository" of contextual information that persists through an entire chain of asynchronous operations (Promises, callbacks, etc.). When instantiated at the start of a request, it serves as isolated storage that only exists for that specific request.
How it works in practice:
- The
ContextLoggingModule
automatically registers a global guard that starts the AsyncLocalStorage context at the beginning of each request - Throughout execution (controllers, services, etc.), you can accumulate metadata using
addMeta()
- Metadata is isolated per request - each request has its own independent context
- At the end, the log is generated with all accumulated metadata for that specific request
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
async getUser(@Param('id') id: string) {
// AsyncLocalStorage context is started automatically
// All metadata added during this request will be isolated
return this.userService.findUser(id);
}
}
Metadata Management
Methods for Accumulating Metadata
The library provides methods to add metadata to the context without generating logs immediately:
export class PaymentService {
constructor(private readonly logger: AppLogger) {}
async processPayment(paymentData: PaymentRequest) {
// Add individual metadata
this.logger.addMeta('operation', 'process_payment');
this.logger.addMeta('paymentMethod', paymentData.method);
// Add multiple metadata at once
this.logger.addMetas({
amount: paymentData.amount,
currency: paymentData.currency,
merchantId: paymentData.merchantId
});
// Simulate validation
await this.validatePayment(paymentData);
this.logger.incMeta('validations_completed'); // Increment counter
// Simulate processing
const result = await this.externalPaymentAPI.process(paymentData);
this.logger.addMeta('transactionId', result.id);
// No need to call logger.info here: the interceptor will log all accumulated metadata automatically
return result;
}
private async validatePayment(data: PaymentRequest) {
this.logger.incMeta('validation_steps'); // Increment on each validation
if (!data.amount || data.amount <= 0) {
this.logger.addMeta('validationError', 'invalid_amount');
throw new Error('Invalid value');
}
this.logger.incMeta('validation_steps');
// More validations...
}
}
Advantages of Metadata Accumulation
- Cost savings: One log per request instead of multiple logs
- Complete context: All request metadata in one place
- Performance: Reduces logging I/O
- Standardization: Consistent log structure
Example of Final Log
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"message": "Payment processed successfully",
"context": "PaymentController.processPayment",
"transactionId": "abc123",
"operation": "process_payment",
"paymentMethod": "credit_card",
"amount": 100.50,
"currency": "BRL",
"merchantId": "merchant-123",
"validations_completed": 1,
"validation_steps": 3,
"paymentTransactionId": "pay-xyz789"
}
New Relic Integration
Log Enrichment with Custom Formatters
By default, logs generated when running your application in vscode has a fine formatted log with metadata highlighted. When generating logs in a provisioned environment, they are generated in json format. If you need, though, you can enrich your logs by providing any custom Winston formatter to the logger. This allows you to add trace, context, or any other fields to your logs. For example, you can use enrichers for New Relic, OpenTelemetry, or your own custom logic.
Example: Using a Single Enricher (New Relic)
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
import { createEnricher } from '@newrelic/log-enricher';
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
logEnricher: createEnricher(), // Adds New Relic trace fields automatically
}),
],
})
export class AppModule {}
Example: Combining Multiple Enrichers
You can combine multiple formatters/enrichers using Winston's format.combine
. For example, to use both New Relic and a custom enricher:
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
import { createEnricher as createNewRelicEnricher } from '@newrelic/log-enricher';
import { format } from 'winston';
// Example custom enricher
const customEnricher = format((info) => {
info.customField = 'custom-value';
return info;
});
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
logEnricher: format.combine(
createNewRelicEnricher(),
customEnricher()
),
}),
],
})
export class AppModule {}
âšī¸ Tip: You can combine as many formatters/enrichers as you need using
format.combine
.
Manual Instrumentation for Uncovered Applications
For applications not covered by New Relic's automatic instrumentation (such as HTTP/2 servers, custom protocols, or non-standard HTTP implementations), you can use the newrelic-nestjs-instrumentation
library to generate the necessary instrumentation.
Installation
npm install @newrelic/log-enricher newrelic-nestjs-instrumentation
Example: Using Both Libraries Together
To get full New Relic trace enrichment and distributed tracing context, use both @newrelic/log-enricher
and newrelic-nestjs-instrumentation
together. The instrumentation module must be imported before the logger module.
import { Module } from '@nestjs/common';
import { NewRelicInstrumentationModule } from 'newrelic-nestjs-instrumentation';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
import { createEnricher } from '@newrelic/log-enricher';
@Module({
imports: [
// CRITICAL: Instrumentation module must come FIRST
NewRelicInstrumentationModule.forRoot(),
ContextLoggingModule.forRoot({
logClass: AppLogger,
logEnricher: createEnricher(),
}),
// ... other modules
],
})
export class AppModule {}
Common Scenarios for Manual Instrumentation
- HTTP/2 servers: The server itself (not client calls)
- Custom protocols: WebSocket, gRPC, etc.
- Non-standard HTTP implementations: Fastify, Koa, etc.
- Applications with custom transport layers
If your application uses standard HTTP/1.1 servers, New Relic's automatic instrumentation may already be sufficient for distributed tracing, but you can still use @newrelic/log-enricher
for log enrichment.
ContextLoggingModule Configuration: Options and Examples
As of the latest version, the forRoot
method of ContextLoggingModule
now receives an options object instead of the logger class directly. This allows for more flexible and powerful configuration.
Simple Example
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
}),
],
})
export class AppModule {}
Intermediate Example: Correlation ID and Custom Error Level
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
import { HttpStatus } from '@nestjs/common';
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
getCorrelationId: () => {
// Example: extract correlationId from request context
// (can use AsyncLocalStorage, headers, etc)
return 'my-correlation-id';
},
// Custom rule to define what log level default log interceptor will use
errorLevelCallback: (error) => {
// 4xx errors will generate warning level
if (error instanceof MyCustomError) return HttpStatus.BAD_REQUEST;
// 5xx errors will generate error level
return HttpStatus.INTERNAL_SERVER_ERROR;
},
}),
],
})
export class AppModule {}
Complete Example: Log Enrichment with New Relic
import { Module } from '@nestjs/common';
import { ContextLoggingModule } from 'nestjs-context-winston';
import { AppLogger } from './logging/app-logger.service';
import { createEnricher } from '@newrelic/log-enricher';
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
getCorrelationId: () => {
// Generate a unique correlation ID for each request
return crypto.randomUUID();
},
errorLevelCallback: (error) => {
// Custom logic for error level
return 500;
},
logEnricher: createEnricher(), // Adds New Relic trace fields automatically
}),
],
})
export class AppModule {}
Available properties in forRoot(options)
logClass
(required): Logger class to register (must extendBaseContextLogger
)getCorrelationId
(optional): Function to extract correlationId from the request contexterrorLevelCallback
(optional): Function to determine HTTP status/log level based on the errorlogEnricher
(optional): Winston formatter to enrich logs (e.g.,@newrelic/log-enricher
)
âšī¸ Tip: You can combine all options to get highly contextual, traceable logs integrated with APMs like New Relic.
API Reference
ContextLogger
Main contextual logger class with AsyncLocalStorage support.
Logging Methods
info(message: string, metadata?: T)
- Info logwarn(message: string, metadata?: T)
- Warning logerror(message: string, metadata?: T)
- Error logdebug(message: string, metadata?: T)
- Debug log
Metadata Management Methods
addMeta(key: keyof T, value: T[keyof T])
- Adds a specific metadata to the current contextaddMetas(metadata: Partial<T>)
- Adds multiple metadata to the current contextincMeta(key: keyof T, increment?: number)
- Increments a numeric value in the context (default: 1)
Properties
winstonLogger: winston.Logger
- Underlying Winston instance
ContextLoggingModule
NestJS module for logger configuration.
Methods
forRoot<T>(options: ContextLoggingOptions<T>)
- Module configuration with custom logger class and options
ContextLoggerContextGuard
Guard that automatically sets up AsyncLocalStorage context.
- Automatically captures
Controller.method
- Includes New Relic
transactionId
when available - Should be used as a global APP_GUARD
- Sets up AsyncLocalStorage for the entire request
Centralized Logging Strategy
đĄ Recommended approach: Use the default log interceptor as the single logging point of your application. Throughout the request execution, services and controllers accumulate metadata using addMeta()
and addMetas()
, but do not log individually. The interceptor automatically consolidates all accumulated metadata into a single structured log at the end of the request.
Advantages of this approach:
- â Resource savings: One log per request instead of dozens
- â Complete context: The entire request journey in one place
- â Better observability: Holistic view of each operation
- â Noise reduction: Cleaner, more organized logs
- â Optimized performance: Lower I/O overhead
Log Interceptor Features
The base interceptor automatically captures and logs:
- Request information: HTTP method, URL, relevant headers
- Response information: status code, response time
- Application context: client IP, user agent
- Correlation: correlation ID for cross-service tracing
- Performance: total request processing time
Example of Generated Log
{
"timestamp": "2025-06-22T16:34:23.000Z",
"level": "info",
"message": "GET /api/products?distributionCenterCode=1&businessModelCode=1... HTTP/1.1\" 200 8701.035309ms",
"routine": "ProductsController.getProducts",
"correlationId": "b2fc6867c551766b5197caa444d9e16d",
"filteredRequestPath": "businessModelCode=1&comStrCode=1&cycle=202506...",
"cached": 1,
"newTime": 285.2268260000019,
"requestPath": "/api/products?distributionCenterCode=1&businessModelCode=1...",
"responseStatusCode": 200,
"responseTime": 8701.035309
}
Service with Structured Logging
This is an example where we locally accumulate some meta to write it once into the context, minimizing, that way, context retrieving, ie, AsyncLocalStorage overhead
@Injectable()
export class OrderService {
constructor(private readonly logger: AppLogger) {}
async createOrder(orderData: CreateOrderDto) {
// Start the operation context using both forms
const meta: Partial<AppLoggerMetadata> = {
operation: 'create_order'
itemCount: orderData.items.length,
customerId: orderData.customerId
totalAmount: orderData.total
}
try {
// Validation
await this.validateOrder(orderData);
this.logger.incMeta('validation_passed');
// Stock reservation
await this.reserveStock(orderData.items);
this.logger.incMeta('stock_operations');
// Payment processing
const payment = await this.processPayment(orderData);
meta.paymentId = payment.id;
// Order creation
const order = await this.orderRepository.create(orderData);
meta.orderId = order.id;
meta.orderStatus = order.status;
return order;
} catch (error) {
// Metadata can be passed directly in the log
meta.errorStep = this.getCurrentStep();
throw error;
} finally {
this.logger.addMetas(meta);
}
}
private async validateOrder(data: CreateOrderDto) {
this.logger.incMeta('validation_steps');
// Validations...
}
private async reserveStock(items: OrderItem[]) {
for (const item of items) {
this.logger.incMeta('stock_checks');
// Reservation logic...
}
}
}
Context Filter
The contextFilter
option allows you to control which requests are logged by the RequestLoggerInterceptor
. This is useful when you want to exclude certain requests from logging, such as health checks, static asset requests, or any custom logic based on the execution context.
How It Works
When you provide a contextFilter
function in the options for ContextLoggingModule.forRoot
, the interceptor will call this function for every request. If the function returns false
, the request will not be logged.
Usage Example with Built-in Helpers
You can use the built-in contextFilters
helpers to easily exclude requests from specific controllers or routes. For example, to skip logging for all requests handled by HealthCheckController
:
import { ContextLoggingModule, contextFilters } from 'nestjs-context-winston';
import { HealthCheckController } from './health-check.controller';
@Module({
imports: [
ContextLoggingModule.forRoot({
logClass: AppLogger,
contextFilter: contextFilters.exclude(
contextFilters.matchController(HealthCheckController)
),
}),
],
})
export class AppModule {}
Notes
- The
contextFilter
function receives the NestJSExecutionContext
for each request. - Returning
true
means the request will be logged; returningfalse
skips logging for that request. - You can implement any custom logic or use the provided helpers to decide which requests should be logged.