OpenTelemetry module for Nest.
This is a improvement on top of a fork of NestJs Otel
- Create tracing file
tracer.ts
inside your appsrc
folder:
import {
createTracingSdk,
hookSdkShutdown,
} from '@observability/observability';
const otelSDK = createTracingSdk({
useNodeAutoInstrumentations: true,
// metricEndpointPort: Defaults to 8081;
// metricInterval: Defaults to 6000;
});
hookSdkShutdown(otelSDK);
export { otelSDK };
- Import the metric file and start otel node SDK in your
src/main.ts
file:
import otelSDK from './tracing';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino';
async function bootstrap() {
// Start SDK before nestjs factory create
await otelSDK.start();
const app = await NestFactory.create(AppModule);
// Enables NestJs Shutdown Hooks to Graceful shutdown the SDK
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
- Configure the Module and import inside your AppModule:
const OtelModuleConfig = OpenTelemetryModule.forRoot({
metrics: {
hostMetrics: true, // Includes Host Metrics
defaultMetrics: true, // Includes Default Metrics
apiMetrics: {
enable: true, // Includes api metrics
timeBuckets: [], // You can change the default time buckets
defaultLabels: { // You can set default labels for api metrics
custom: 'label'
},
ignoreRoutes: ['/favicon.ico'], // You can ignore specific routes (See https://docs.nestjs.com/middleware#excluding-routes for options)
ignoreUndefinedRoutes: false, //Records metrics for all URLs, even undefined ones
},
},
});
@Module({
imports: [
OtelModuleConfig,
],
controllers: [AmsBackofficeApiController],
providers: [
AmsBackofficeApiService,
{
provide: APP_INTERCEPTOR,
useClass: ErrorsInterceptor,
},
],
})
export class AmsBackofficeApiModule {}
If you need, you can define a custom Tracing Span for a method. It works async or sync. Span takes its name from the parameter; but by default, it is the same as the method's name
import { Span } from 'nestjs-otel';
@Span('CRITICAL_SECTION')
async getBooks() {
return [`Harry Potter and the Philosopher's Stone`];
}
In case you need to access native span methods for special logics in the method block:
import { TraceService } from 'nestjs-otel';
@Injectable()
export class BookService {
constructor(private readonly traceService: TraceService) {}
@Span()
async getBooks() {
const currentSpan = this.traceService.getSpan(); // --> retrives current span, comes from http or @Span
await this.doSomething();
currentSpan.addEvent('event 1');
currentSpan.end(); // current span end
const span = this.traceService.startSpan('sub_span'); // start new span
span.setAttributes({ userId: 1 });
await this.doSomethingElse();
span.end(); // new span ends
return [`Harry Potter and the Philosopher's Stone`];
}
}
OpenTelemetry Metrics allow a user to collect data and export it to metrics backend like Prometheus.
import { MetricService } from 'nestjs-otel';
import { Counter } from '@opentelemetry/api-metrics';
@Injectable()
export class BookService {
private customMetricCounter: Counter;
constructor(private readonly metricService: MetricService) {
this.customMetricCounter = this.metricService.getCounter('custom_counter', {
description: 'Description for counter',
});
}
async getBooks() {
this.customMetricCounter.add(1);
return [`Harry Potter and the Philosopher's Stone`];
}
}
If you want to count how many instance of a specific class has been created:
@OtelInstanceCounter() // It will generate a counter called: app_MyClass_instances_total.
export class MyClass {
}
If you want to increment a counter on each call of a specific method:
@Injectable()
export class MyService {
@OtelMethodCounter()
doSomething() {
}
}
@Controller()
export class AppController {
@Get()
@OtelMethodCounter() // It will generate `app_AppController_doSomething_calls_total` counter.
doSomething() {
// do your stuff
}
}
You have the following decorators:
@OtelCounter()
@OtelUpDownCounter()
@OtelHistogram()
@OtelObservableGauge()
@OtelObservableCounter()
@OtelObservableUpDownCounter()
Example of usage:
import { OtelCounter } from 'nestjs-otel';
import { Counter } from '@opentelemetry/api-metrics';
@Controller()
export class AppController {
@Get('/home')
home(
@OtelCounter('app_counter_1_inc', { description: 'counter 1 description' }) counter1: Counter,
) {
counter1.add(1);
}
}
Impl | Metric | Description | Labels | Metric Type |
---|---|---|---|---|
✅ | http_request_total | Total number of HTTP requests. | method, path | Counter |
✅ | http_response_total | Total number of HTTP responses. | method, status, path | Counter |
✅ | http_response_success_total | Total number of all successful responses. | - | Counter |
✅ | http_response_error_total | Total number of all response errors. | - | Counter |
✅ | http_request_duration_seconds | HTTP latency value recorder in seconds. | method, status, path | Histogram |
✅ | http_client_error_total | Total number of client error requests. | - | Counter |
✅ | http_server_error_total | Total number of server error requests. | - | Counter |
✅ | http_server_aborts_total | Total number of data transfers aborted. | - | Counter |
✅ | http_request_size_bytes | Current total of incoming bytes. | - | Histogram |
✅ | http_response_size_bytes | Current total of outgoing bytes. | - | Histogram |
When metricExporter
is defined in otel SDK with a PrometheusExporter
it will start a new process on port 8081
(default port) and metrics will be available at http://localhost:8081/metrics
.
This approach uses otel instrumentation to automatically inject spanId and traceId.
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
const otelSDK = new NodeSDK({
instrumentations: [new PinoInstrumentation()],
});
This approach uses the global trace context for injecting SpanId and traceId as a property of your structured log.
import Pino, { Logger } from 'pino';
import { LoggerOptions } from 'pino';
import { trace, context } from '@opentelemetry/api';
export const loggerOptions: LoggerOptions = {
formatters: {
log(object) {
const span = trace.getSpan(context.active());
if (!span) return { ...object };
const { spanId, traceId } = trace.getSpan(context.active())?.spanContext();
return { ...object, spanId, traceId };
},
},
};
export const logger: Logger = Pino(loggerOptions);