@pawells/nestjs-open-telemetry
v3.0.5
Published
NestJS OpenTelemetry integration with tracing, metrics, and logger adapter
Downloads
1,580
Readme
NestJS OpenTelemetry Module
OpenTelemetry integration for NestJS applications. Provides distributed tracing with the @Traced decorator, manual span creation helpers, HTTP metrics recording, and a NestJS logger adapter that automatically injects trace context.
Installation
yarn add @pawells/nestjs-open-telemetryRequirements
- Node.js: >= 22.0.0
- NestJS: >= 10.0.0
- @opentelemetry/api: >= 1.0.0
- @pawells/nestjs-shared: peer dependency (provides
InstrumentationRegistry)
Quick Start
Module Setup
Import OpenTelemetryModule in your root application module. The module must be imported after CommonModule from @pawells/nestjs-shared (which provides the InstrumentationRegistry).
import { Module } from '@nestjs/common';
import { CommonModule } from '@pawells/nestjs-shared';
import { OpenTelemetryModule } from '@pawells/nestjs-open-telemetry';
@Module({
imports: [
CommonModule, // Provides InstrumentationRegistry
OpenTelemetryModule.forRoot(), // Registers OpenTelemetry exporter
],
})
export class AppModule {}Using @Traced Decorator
Automatically wrap any method in a distributed tracing span with the @Traced() decorator:
import { Injectable } from '@nestjs/common';
import { Traced, SpanKind } from '@pawells/nestjs-open-telemetry';
@Injectable()
export class UserService {
// Basic usage — spans are INTERNAL by default
@Traced()
async getUserById(userId: string) {
return await this.db.findUser(userId);
}
// Custom span name and attributes
@Traced({
name: 'UserService.fetchFromAPI',
attributes: { 'service.layer': 'business-logic' },
captureReturn: true, // Include return value in span
})
async fetchUserFromAPI(userId: string) {
return await this.httpClient.get(`/api/users/${userId}`);
}
// CLIENT span for external HTTP calls
@Traced({
name: 'getUserDataFromExternalAPI',
kind: SpanKind.CLIENT,
attributes: { 'http.method': 'GET' },
})
async getDataFromExternal(userId: string) {
return await fetch(`https://api.example.com/users/${userId}`);
}
// SERVER span for request handlers
@Traced({
kind: SpanKind.SERVER,
captureArgs: true,
captureReturn: true,
})
async createUser(dto: CreateUserDto) {
return await this.db.createUser(dto);
}
}@Traced Decorator Options
interface TracedOptions {
/**
* Custom span name. Defaults to "ClassName.methodName".
*/
name?: string;
/**
* Span kind. Defaults to SpanKind.INTERNAL.
* Common values:
* - SpanKind.INTERNAL: Business logic (default)
* - SpanKind.CLIENT: External API calls
* - SpanKind.SERVER: Request handlers
* - SpanKind.PRODUCER: Message producers
* - SpanKind.CONSUMER: Message consumers
*/
kind?: SpanKind;
/**
* Additional span attributes to always set.
*/
attributes?: Record<string, string | number | boolean>;
/**
* Capture method arguments as span attributes. Defaults to true.
* Arguments > 100 chars or complex objects are omitted for security.
* PII (email, phone, SSN, credit cards) is automatically redacted.
*/
captureArgs?: boolean;
/**
* Capture method return value as span attribute. Defaults to false.
* Return values > 100 chars or complex objects are omitted for security.
*/
captureReturn?: boolean;
}SpanKind Values
Exported from @pawells/nestjs-open-telemetry:
- INTERNAL (default) — Internal business logic, synchronous operations
- SERVER — Request handlers, server-side operations
- CLIENT — External API calls, outbound requests
- PRODUCER — Message producers
- CONSUMER — Message consumers
Manual Span Creation
For more control, use the tracing helpers exported from the package:
GetTracer(name, version?)
Get or create a tracer instance with namespace conventions.
import { GetTracer } from '@pawells/nestjs-open-telemetry';
const tracer = GetTracer('user-service', '1.2.0');
// Actual tracer name: 'pawells.user-service'CreateSpan(tracer, name, options?, makeActive?)
Create a span and optionally set it as active in context.
import { GetTracer, CreateSpan } from '@pawells/nestjs-open-telemetry';
const tracer = GetTracer('user-service');
const { span, ctx } = CreateSpan(tracer, 'getUserById', {
attributes: { 'user.id': '123' },
});
try {
// Do work within context
context.with(ctx, () => {
// Span is active here
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
} finally {
span.end();
}WithSpan(tracer, name, fn, options?)
Execute a function within a span, automatically handling success/error status and cleanup.
import { GetTracer, WithSpan } from '@pawells/nestjs-open-telemetry';
const tracer = GetTracer('user-service');
// Works with async or sync functions
const user = await WithSpan(tracer, 'getUserById', async () => {
return await db.findUser(userId);
}, {
attributes: { 'user.id': userId },
});AddAttributes(attributes, ctx?)
Add attributes to the currently active span. Silently no-ops if no span is active.
import { AddAttributes } from '@pawells/nestjs-open-telemetry';
AddAttributes({
'user.id': userId,
'user.role': 'admin',
'request.method': 'POST',
});HTTP Metrics
Record HTTP request metrics following OpenTelemetry semantic conventions:
recordHttpMetrics(method, route, statusCode, duration, requestSize?, responseSize?)
Record all HTTP request metrics at once.
import { recordHttpMetrics } from '@pawells/nestjs-open-telemetry';
// In a middleware or HTTP interceptor
recordHttpMetrics(
'GET', // HTTP method
'/users/:id', // Route pattern (normalized)
200, // Status code
45.2, // Duration in milliseconds
0, // Optional: request body size in bytes
1024, // Optional: response body size in bytes
);Metrics recorded (following OpenTelemetry semantic conventions):
http.server.request.count— Total HTTP requestshttp.server.request.duration— Request duration histogramhttp.server.request.size— Request body size histogramhttp.server.response.size— Response body size histogram
trackActiveRequests(delta, attributes?)
Track the number of active HTTP requests. Call with +1 when a request starts, -1 when it completes.
import { trackActiveRequests } from '@pawells/nestjs-open-telemetry';
// Request started
trackActiveRequests(1, { method: 'GET' });
// Later, request completed
trackActiveRequests(-1, { method: 'GET' });Metric recorded:
http.server.active_requests— UpDownCounter tracking active requests
OpenTelemetryLogger Adapter
Use OpenTelemetryLogger as your NestJS logger to automatically inject trace context (trace_id, span_id) into all logs.
The class implements the NestJS LoggerService interface. Following the project's PascalCase convention, the primary methods are Log, Error, Warn, Debug, Verbose, and Fatal. Lowercase aliases log, error, and warn are provided for direct LoggerService interface compatibility.
import { NestFactory } from '@nestjs/core';
import { OpenTelemetryLogger } from '@pawells/nestjs-open-telemetry';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new OpenTelemetryLogger({
service: 'my-app', // Optional: service name
level: 'info', // Optional: log level
format: 'json', // Optional: log format
}),
});
await app.listen(3000);
}
bootstrap();The logger automatically adds OpenTelemetry context to each log:
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"message": "Request completed",
"trace_id": "a1b2c3d4e5f6g7h8...",
"span_id": "x1y2z3a4b5c6d7e8",
"trace_flags": "01",
"service": "my-app"
}OpenTelemetryExporter
The OpenTelemetryExporter is automatically registered by the module and implements the IMetricsExporter interface from @pawells/nestjs-shared.
It converts metrics recorded through the InstrumentationRegistry to OpenTelemetry instruments:
- counter → OpenTelemetry
Counter - histogram → OpenTelemetry
Histogram - gauge → OpenTelemetry
UpDownCounter(push-based gauge semantics — OTel'sObservableGaugerequires a pull-based callback incompatible with the push model, soUpDownCounteris used instead with delta computation) - updown_counter → OpenTelemetry
UpDownCounter
Typically you don't interact with the exporter directly; the module handles registration automatically.
Public methods (follows PascalCase convention):
OnDescriptorRegistered(descriptor)— Pre-creates the OTel instrument for a registered metricOnMetricRecorded(value)— Pushes a recorded metric value to the appropriate instrumentShutdown()— Clears the instrument cache on application shutdown
Architecture
The module integrates with NestJS and OpenTelemetry as follows:
- @Traced Decorator — Wraps methods in spans with automatic error handling and PII redaction
- Tracing Helpers — Provide low-level span creation and context management
- HTTP Metrics — Record request latency, status, and size metrics
- OpenTelemetryExporter — Converts application metrics to OpenTelemetry instruments
- OpenTelemetryLogger — Injects trace context into all logs
- InstrumentationRegistry Integration — Works with
@pawells/nestjs-sharedto export metrics
Configuration Reference
OpenTelemetryModule.forRoot()
The module is initialized with no required configuration:
@Module({
imports: [
OpenTelemetryModule.forRoot(),
],
})
export class AppModule {}Testing Utilities
The package exports helper functions for test isolation and cleanup:
SetTracerNamespace(namespace)
Overrides the tracer namespace prefix used for all GetTracer calls. Useful in tests where a custom namespace is required.
import { SetTracerNamespace } from '@pawells/nestjs-open-telemetry';
SetTracerNamespace('my-custom-namespace');ResetTracerNamespace()
Resets the tracer namespace to its default value ('pawells'). Used in test teardown to ensure namespace isolation between tests.
import { ResetTracerNamespace } from '@pawells/nestjs-open-telemetry';
afterEach(() => {
ResetTracerNamespace(); // Clean up for next test
});resetHttpMetrics()
Resets the cached HTTP metrics to null, forcing re-initialization on next access. Used in test teardown to ensure metric state isolation between tests.
import { resetHttpMetrics } from '@pawells/nestjs-open-telemetry';
afterEach(() => {
resetHttpMetrics(); // Clean up for next test
});Re-exports
The package re-exports useful types from OpenTelemetry for convenience:
import {
Span,
SpanContext,
Attributes,
SpanKind,
} from '@pawells/nestjs-open-telemetry';
// Also available from @opentelemetry/apiLogger configuration types:
import {
ILoggerConfig,
LogLevel,
} from '@pawells/nestjs-open-telemetry';
// Also available from @pawells/loggerSecurity Features
PII Redaction
The @Traced decorator automatically detects and redacts Personally Identifiable Information (PII):
- Email addresses →
[REDACTED_EMAIL] - Phone numbers →
[REDACTED_PHONE] - Social Security Numbers (SSN) →
[REDACTED_SSN] - Credit card numbers (Luhn-validated) →
[REDACTED_CREDIT_CARD]
Argument Sanitization
- Arguments longer than 100 characters are truncated
- Complex objects are summarized (e.g.,
Object(5 keys)instead of stringified) - Arrays > 5 items are summarized
- Null/undefined are converted to strings
Return Value Redaction
Return values are only captured if explicitly enabled with captureReturn: true, and the same sanitization rules apply.
Integration with Other Packages
- @pawells/nestjs-shared — Provides
InstrumentationRegistryand HTTP metrics interceptor - @pawells/nestjs-auth — Trace authentication flows with
@Traced - @pawells/nestjs-pyroscope — Profiling integration
Examples
Complete Service Example
import { Injectable } from '@nestjs/common';
import { Traced, SpanKind, GetTracer, WithSpan, AddAttributes } from '@pawells/nestjs-open-telemetry';
@Injectable()
export class OrderService {
constructor(private db: Database, private httpClient: HttpClient) {}
// Automatic tracing with decorator
@Traced({
name: 'OrderService.getOrder',
kind: SpanKind.INTERNAL,
})
async getOrder(orderId: string) {
return await this.db.orders.findById(orderId);
}
// Manual span management with helpers
async processOrder(orderId: string) {
const tracer = GetTracer('order-service');
return WithSpan(tracer, 'processOrder', async () => {
AddAttributes({
'order.id': orderId,
'operation': 'process',
});
const order = await this.getOrder(orderId);
await this.validateOrder(order);
await this.chargePayment(order);
await this.shipOrder(order);
return order;
}, {
attributes: { 'order.id': orderId },
});
}
// CLIENT span for external calls
@Traced({
kind: SpanKind.CLIENT,
name: 'ExternalPaymentAPI.charge',
})
private async chargePayment(order: Order) {
return await this.httpClient.post('https://payment.example.com/charge', {
orderId: order.id,
amount: order.total,
});
}
@Traced()
private async validateOrder(order: Order) {
AddAttributes({ 'validation.status': 'passed' });
return true;
}
@Traced()
private async shipOrder(order: Order) {
AddAttributes({ 'shipping.method': 'standard' });
}
}HTTP Middleware Example
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { recordHttpMetrics, trackActiveRequests } from '@pawells/nestjs-open-telemetry';
@Injectable()
export class HttpMetricsMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
trackActiveRequests(1, { method: req.method });
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
recordHttpMetrics(
req.method,
req.route?.path || req.path,
res.statusCode,
duration,
req.get('content-length') ? parseInt(req.get('content-length')!, 10) : 0,
res.get('content-length') ? parseInt(res.get('content-length')!, 10) : 0,
);
trackActiveRequests(-1, { method: req.method });
});
next();
}
}Troubleshooting
Spans not appearing in traces
Ensure:
- OpenTelemetry SDK is initialized before your NestJS app
OpenTelemetryModuleis imported afterCommonModule- A span exporter is configured in your OpenTelemetry SDK
PII redaction not working
PII redaction only applies to method arguments when captureArgs: true (default). If you're seeing PII in custom attributes, redact them manually:
@Traced({
attributes: {
'user.email': sanitizeEmail(email), // Redact manually
},
})No trace context in logs
Ensure OpenTelemetryLogger is set as the NestJS logger during app creation:
const app = await NestFactory.create(AppModule, {
logger: new OpenTelemetryLogger(),
});License
MIT
