@beincom/nestjs-trace
v1.0.2
Published
OpenTelemetry tracing module for NestJS applications
Readme
@beincom/nestjs-trace
OpenTelemetry tracing module for NestJS applications. Provides automatic instrumentation, decorators for manual tracing, and flexible configuration options.
Installation
yarn add @beincom/nestjs-trace
# or
npm install @beincom/nestjs-traceNote: This package includes all required dependencies. Just install @beincom/nestjs-trace.
For better IDE support (auto-import suggestions): Add @opentelemetry/api to your devDependencies:
yarn add -D @opentelemetry/apiThis will help your IDE suggest imports for OpenTelemetry types like SpanKind, Span, etc.
Minimum Requirements
- Node.js: 18.x or higher
- NestJS: 9.x or higher (10.x recommended)
- TypeScript: 4.9.x or higher
This package is compiled with ES2022 target, which requires Node.js 18+ for optimal compatibility.
Quick Start
import { Module } from '@nestjs/common';
import { TraceModule } from '@beincom/nestjs-trace';
@Module({
imports: [
TraceModule.forRoot({
isEnabled: true, // Enable/disable tracing
serviceName: 'my-service', // Service name for traces
env: process.env.NODE_ENV || 'development', // Environment name
collectorUrl: 'http://localhost:4318/v1/traces', // Collector URL
// For OTLP/HTTP (default): use port 4318 (e.g., http://localhost:4318/v1/traces)
// For OTLP/GRPC: use port 4317 (e.g., http://localhost:4317)
samplerRatio: 1.0, // Sampling ratio (0.0 to 1.0)
traceDebug: false, // Enable OTel diagnostic logging (for debugging)
}),
],
})
export class AppModule {}Configuration Options
Basic Configuration
TraceModule.forRoot({
isEnabled: true, // Enable/disable tracing
serviceName: 'my-service', // Service name for traces
env: 'production', // Environment name
collectorUrl: 'http://...', // Collector URL (see below)
samplerRatio: 1.0, // Sampling ratio (0.0 to 1.0)
concurrencyLimit: 10, // OTLP exporter concurrency
timeout: 10000, // Export timeout in ms
enableAutoInstrumentations: true, // Enable auto-instrumentations (default: true)
traceDebug: false, // Enable diagnostic logging
});Protocol & Ports
OTLP/HTTP (Default):
- Port:
4318 - URL:
http://localhost:4318/v1/traces - Default exporter. Good for most cases.
- Configuration:
TraceModule.forRoot({ // ... collectorUrl: 'http://localhost:4318/v1/traces', traceExporter: 'otlp', // or 'otlp-http' (both are equivalent) });
- Port:
OTLP/gRPC:
- Port:
4317 - URL:
http://localhost:4317 - Better performance. Requires HTTP/2 support.
- Configuration:
TraceModule.forRoot({ // ... collectorUrl: 'http://localhost:4317', traceExporter: 'otlp-grpc', });
- Port:
Zipkin:
- Used automatically for
localordevelopmentenvironments (unless overridden) - Can be explicitly enabled with
traceExporter: 'zipkin'oruseLocalExporter: true - Configuration:
TraceModule.forRoot({ // ... collectorUrl: 'http://localhost:9411/api/v2/spans', traceExporter: 'zipkin', });
- Used automatically for
Disable Auto-Instrumentations
If you want to use only manual tracing (decorators) without auto-instrumentations:
TraceModule.forRoot({
// ... other config ...
enableAutoInstrumentations: false, // Disable all auto-instrumentations
});Advanced Configuration
import { Span } from '@opentelemetry/api';
import { ElasticsearchInstrumentation } from 'opentelemetry-instrumentation-elasticsearch';
TraceModule.forRoot({
// ... basic config ...
// Auto-instrumentation options
// Uses InstrumentationConfigMap type from @opentelemetry/auto-instrumentations-node
// Pass configuration directly to OpenTelemetry's getNodeAutoInstrumentations()
// Must use full package names (e.g., '@opentelemetry/instrumentation-http')
// Supported instrumentations include: http, fs, pino, express, fastify, mongodb, redis, pg, etc.
autoInstrumentations: {
'@opentelemetry/instrumentation-http': {
enabled: true,
requireParentforOutgoingSpans: true,
applyCustomAttributesOnSpan: (span: Span, request, response) => {
span.setAttribute('status_code', response.statusCode);
// Add custom logic here
},
ignoreIncomingPaths: ['/health/readyz', '/health/livez', '/metrics'],
},
'@opentelemetry/instrumentation-fs': {
enabled: true,
requireParentSpan: true,
},
'@opentelemetry/instrumentation-pino': {
enabled: true,
logHook: (span, record) => {
// Custom log hook
},
},
// Note: Only instrumentations from OpenTelemetry's InstrumentationMap are supported here
// For custom or third-party instrumentations, use additionalInstrumentations instead
},
// Additional instrumentation instances
// Use for instruments not included in getNodeAutoInstrumentations()
// (e.g., ElasticsearchInstrumentation, custom instruments)
additionalInstrumentations: [
new ElasticsearchInstrumentation({
moduleVersionAttributeName: 'elasticsearchClient.version',
}),
],
// Additional resource attributes
resourceAttributes: {
'service.version': '1.0.0',
'deployment.region': 'us-east-1',
},
// Custom trace exporter
// Options: 'otlp' | 'otlp-http' | 'otlp-grpc' | 'zipkin' | SpanExporter instance
// 'otlp' is an alias for 'otlp-http'
traceExporter: 'otlp', // or 'otlp-http', 'otlp-grpc', 'zipkin', or custom SpanExporter
// Use local exporter (Zipkin) instead of OTLP
// If true, uses Zipkin exporter regardless of traceExporter setting
// Default: false (auto-detect based on env: uses Zipkin for 'local' or 'development')
useLocalExporter: false,
// Filter patterns for span names to exclude
// Default: ['connect', 'create nest app', 'nestfactory.create', 'nestfactory', 'bootstrap', 'app.listen', 'application startup', 'nest application']
filterSpanPatterns: ['connect', 'bootstrap'],
// Custom span processor (wraps the exporter)
customSpanProcessor: myCustomProcessor,
// Custom resource detectors
customResourceDetectors: [myCustomDetector],
// Custom propagators
customPropagators: [myCustomPropagator],
});Auto Instrumentations Behavior
If autoInstrumentations is not provided:
- OpenTelemetry defaults will be used (all standard instrumentations enabled via
getNodeAutoInstrumentations())
If autoInstrumentations is provided:
- Your configuration uses
InstrumentationConfigMaptype from@opentelemetry/auto-instrumentations-node - Configuration is passed directly to
getNodeAutoInstrumentations()without any merging - You must provide complete configuration for each instrumentation you want to customize
- Must use full package names like
'@opentelemetry/instrumentation-http'(shorthand keys are not supported) - Only instrumentations included in OpenTelemetry's
InstrumentationMapare supported - Supported instrumentations include:
@opentelemetry/instrumentation-http,@opentelemetry/instrumentation-fs,@opentelemetry/instrumentation-pino,@opentelemetry/instrumentation-express,@opentelemetry/instrumentation-fastify,@opentelemetry/instrumentation-mongodb,@opentelemetry/instrumentation-redis,@opentelemetry/instrumentation-pg, and many more
Example:
TraceModule.forRoot({
// ... other config ...
autoInstrumentations: {
// Use full package name
'@opentelemetry/instrumentation-http': {
enabled: true,
requireParentforOutgoingSpans: true,
applyCustomAttributesOnSpan: (span, request, response) => {
span.setAttribute('status_code', response.statusCode);
},
ignoreIncomingPaths: ['/health', '/metrics'],
},
'@opentelemetry/instrumentation-fs': {
enabled: true,
requireParentSpan: true,
},
},
});Additional Instrumentations
For instruments that are not included in OpenTelemetry's InstrumentationMap (like Elasticsearch, custom instruments, or third-party instrumentations), use additionalInstrumentations:
Note: If an instrumentation is not part of getNodeAutoInstrumentations(), you cannot configure it via autoInstrumentations. Instead, instantiate it directly and add it to additionalInstrumentations.
import { ElasticsearchInstrumentation } from 'opentelemetry-instrumentation-elasticsearch';
TraceModule.forRoot({
// ... other config ...
additionalInstrumentations: [
new ElasticsearchInstrumentation({
moduleVersionAttributeName: 'elasticsearchClient.version',
}),
],
});Usage
Decorators
TraceClass - Automatically trace all methods
import { TraceClass } from '@beincom/nestjs-trace';
@TraceClass({ spanPrefix: 'UserService' })
export class UserService {
async createUser(data: CreateUserDto) {
// Automatically traced as 'UserService.createUser'
}
@NoTrace
async privateMethod() {
// This method won't be traced
}
}TraceMethod - Trace specific methods
import { TraceMethod } from '@beincom/nestjs-trace';
export class OrderService {
@TraceMethod('OrderService.processPayment', (args) => ({
orderId: args[0],
amount: args[1]?.amount,
}))
async processPayment(orderId: string, payment: PaymentInfo) {
// Traced with custom attributes
}
}TraceEventLog - Trace event handlers
import { TraceEventLog } from '@beincom/nestjs-trace';
export class UserListener {
@TraceEventLog({
eventNames: ['user.created', 'user.updated'],
traceOptions: { spanPrefix: 'UserListener' },
})
async handleUserEvent(event: UserEvent) {
// Automatically traced when events are emitted
}
}Manual Tracing
import { startTraceSpan, TraceService } from '@beincom/nestjs-trace';
// Start a span manually
const result = await startTraceSpan(
'MyOperation',
{ userId: '123' },
async (span) => {
span.setAttribute('custom.attr', 'value');
return await doSomething();
},
{
attributes: { layer: 'service' },
useLinks: true, // For async operations
}
);
// Inject trace context for propagation
// Note: injectToPropagation is an instance method, inject TraceService via DI
import { Injectable } from '@nestjs/common';
import { TraceService } from '@beincom/nestjs-trace';
@Injectable()
export class MyService {
constructor(private readonly traceService: TraceService) {}
async sendToAnotherService() {
const payload = {};
this.traceService.injectToPropagation(payload);
// Send payload to another service
}
}
// Or use static methods for tracer access
const tracer = TraceService.getTracer();
const activeSpan = TraceService.getActiveSpan();Attributes
Attributes are key-value metadata attached to spans. They help you:
- Add business context (userId, orderId, status)
- Search/filter traces in your tracing backend
- Debug and correlate application behavior
Static Attributes
@TraceMethod('UserService.createUser', { layer: 'service', action: 'createUser' })
async createUser(userId: string) {
// Every span will have layer and action attributes
}Dynamic Attributes
@TraceMethod('OrderService.payOrder', args => ({
orderId: args[0],
paymentType: args[1]?.type,
}))
payOrder(orderId: string, payment: PaymentInfo) {
// Attributes are set based on actual arguments
}Async Attributes
@TraceMethod('UserService.updateUser', async args => {
const meta = await getUserMeta(args[0]);
return { userId: args[0], ...meta };
})
async updateUser(userId: string) {
// Attributes can be fetched asynchronously
}Links vs Parent-Child
Parent-Child (Default)
Used for synchronous operations within the same request lifecycle:
- HTTP requests
- Internal events
- Synchronous operations
Links
Used for asynchronous operations where parent span may have already ended:
- Queue jobs
- Kafka consumers
- Scheduled tasks
- Messaging systems
@TraceClass({ useLinks: true })
export class QueueProcessor {
@Process('job.name')
async handleJob(job: Job) {
// This span will be a root span with link to producer span
}
}Important: Initialization Order
The trace module initializes immediately when imported to ensure instrumentation is active before other modules (like Redis) load. This is handled automatically by TraceModule.forRoot().
Method 1: Using TraceModule (Recommended)
import { TraceModule } from '@beincom/nestjs-trace';
@Module({
imports: [
TraceModule.forRoot({
isEnabled: true,
serviceName: 'my-service',
// ... other options
}),
],
})
export class AppModule {}Method 2: Manual Initialization
If you need manual control, you can use:
import { initializeTrace } from '@beincom/nestjs-trace';
// Initialize before other imports
const sdk = initializeTrace({
isEnabled: true,
serviceName: 'my-service',
// ... other options
});Method 3: Bootstrap in Separate File (For Services)
You can create a separate bootstrap file and import it in main.ts:
Create trace.bootstrap.ts:
import { initializeTrace } from '@beincom/nestjs-trace';
import { getTraceConfig } from './config'; // Your config helper
import config from './config'; // Your config object
export default initializeTrace(getTraceConfig(config().trace));In main.ts:
// Import trace bootstrap FIRST, before other imports
import './trace.bootstrap';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Note:
- The trace bootstrap file must be imported before any other imports that might use instrumented libraries (like Redis, HTTP clients, etc.) to ensure proper instrumentation.
- This method is useful when you want to centralize trace configuration across multiple services.
Graceful Shutdown
The TraceModule automatically handles graceful shutdown of the OpenTelemetry SDK when your NestJS application shuts down. This ensures that:
- All pending spans are exported before the application terminates
- The OpenTelemetry SDK is properly cleaned up
- No data loss occurs during application shutdown
Automatic Shutdown
Graceful shutdown is automatically handled in all cases as long as TraceModule is present in your application (even if you're using initializeTrace() directly).
The TraceModule implements NestJS's OnApplicationShutdown lifecycle hook and uses the same SDK instance created by initializeTrace(). The SDK will automatically shut down when:
- The application receives a termination signal (SIGTERM, SIGINT)
app.close()is called- The NestJS application lifecycle reaches the shutdown phase
This works automatically for:
- Method 1:
TraceModule.forRoot()- Automatic shutdown ✓ - Method 2:
initializeTrace()+TraceModulein app - Automatic shutdown ✓ - Method 3: Bootstrap file +
TraceModulein app - Automatic shutdown ✓
Example with TraceModule:
import { Module } from '@nestjs/common';
import { TraceModule } from '@beincom/nestjs-trace';
@Module({
imports: [
TraceModule.forRoot({
isEnabled: true,
serviceName: 'my-service',
// ... other options
}),
],
})
export class AppModule {}
// Graceful shutdown is handled automatically - no additional code needed!Example with Manual Initialization + TraceModule:
// main.ts - Initialize trace first
import './trace.bootstrap'; // Calls initializeTrace()
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// app.module.ts - Import TraceModule (without forRoot) for TraceService and automatic shutdown
import { Module } from '@nestjs/common';
import { TraceModule } from '@beincom/nestjs-trace';
@Module({
imports: [
TraceModule, // Module is present for TraceService and automatic shutdown handling
// TraceModule.onApplicationShutdown() will automatically call getTraceSDK().shutdown()
// ... other modules
],
})
export class AppModule {}
// Graceful shutdown still works automatically because TraceModule is in the app!Manual Shutdown (Only When TraceModule is NOT in App)
Only needed if: You're using initializeTrace() directly (Method 2 or Method 3) AND you don't have TraceModule anywhere in your application.
If you don't have TraceModule in your app, you need to manually handle shutdown:
Option 1: Using NestJS App Lifecycle
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import { getTraceSDK } from '@beincom/nestjs-trace';
@Injectable()
export class TraceShutdownService implements OnApplicationShutdown {
async onApplicationShutdown() {
const sdk = getTraceSDK();
if (sdk) {
await sdk.shutdown();
}
}
}Option 2: Using Process Signals
import { getTraceSDK } from '@beincom/nestjs-trace';
async function shutdown() {
const sdk = getTraceSDK();
if (sdk) {
await sdk.shutdown();
}
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);Shutdown Behavior
During shutdown:
- The SDK stops accepting new spans
- All pending spans in the batch processor are flushed and exported
- Exporters are given time to send remaining data (respects
timeoutconfiguration) - The SDK is fully cleaned up
Note: Ensure your application allows enough time for the shutdown process to complete. The shutdown timeout is controlled by your timeout configuration option (default: 10000ms).
API Reference
TraceModule
TraceModule.forRoot(options: TraceModuleOptions): Configure the trace module synchronouslyTraceModule.forRootAsync(options): Configure the trace module asynchronously with ConfigService
Decorators
@TraceClass(options?): Automatically trace all methods in a class@TraceMethod(spanNameOrOptions?, attributesOrOptions?): Trace a specific method@TraceEventLog({ eventNames, traceOptions?, onEventOptions? }): Trace event handlers@NoTrace: Exclude a method from tracing (used with TraceClass)
Utilities
startTraceSpan<T, R>(spanName, args, handler, options?): Manually start a trace spansetErrorAttributes(span, err): Set error attributes on a spanhasPropagationHeaders(obj): Check if object has trace propagation headersfindHeadersFromArgs(args): Find headers from function argumentscreateLinkFromHeaders(headers, linkAttributes?): Create span link from headers
TraceService
Static Methods:
TraceService.getTracer(): Get the OpenTelemetry tracerTraceService.getActiveSpan(): Get the currently active span
Instance Methods:
traceService.injectToPropagation(payload, span?): Inject trace context into payload (requires DI)
License
MIT
