OpenTelemetry NestJS Instrumentation

Alexandr Bandurchin
April 22, 2026
11 min read

OpenTelemetry NestJS instrumentation is a set of OpenTelemetry packages that automatically patches HTTP, Express, and NestJS-core layers at startup to collect traces, metrics, and logs. You configure a NodeSDK once, load it before NestJS bootstraps via --require, and gain full visibility into request latency, controller execution, database queries, and downstream service calls — without touching your business logic.

Quick Start

StepActionDetail
1. InstallAdd OpenTelemetry packagesnpm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
2. ConfigureCreate src/otel.tsSee Usage section below
3. RunLoad with --require flagnode --require ./dist/otel.js dist/main.js
4. VerifyCheck traces in your backendSpans appear automatically

Minimal working example:

typescript
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'

const sdk = new NodeSDK({
  serviceName: 'my-nestjs-app',
  instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()

This configuration automatically instruments NestJS controllers, HTTP requests, and common Node.js libraries without modifying your application code.

If getNodeAutoInstrumentations() generates too much noise (filesystem events in particular), disable specific instrumentations selectively:

typescript
getNodeAutoInstrumentations({
  // fs instrumentation is very noisy — disable unless you need it
  '@opentelemetry/instrumentation-fs': { enabled: false },
  // dns instrumentation rarely adds value in most apps
  '@opentelemetry/instrumentation-dns': { enabled: false },
})

What is NestJS?

NestJS is a progressive Node.js framework for building efficient and scalable server-side applications. It is written in TypeScript and combines elements of Object-Oriented Programming, Functional Programming, and Functional Reactive Programming.

NestJS uses decorators and a module system inspired by Angular, making it a popular choice for teams building enterprise-grade REST APIs, GraphQL services, and microservices on Node.js.

What is OpenTelemetry?

OpenTelemetry is an open-source observability framework that standardizes the collection, processing, and export of telemetry data from applications. It supports distributed tracing, metrics, and logs across multiple languages and platforms.

OpenTelemetry enables developers to instrument their code and export telemetry to any OpenTelemetry backend for analysis and visualization. The OpenTelemetry architecture follows a modular design with SDKs, APIs, and exporters as its core components.

Quick Start: For fastest setup without code changes, see the Node.js zero-code instrumentation guide.

Prerequisites

Before instrumenting, make sure your environment meets these requirements:

RequirementVersion
Node.js18.19.0+ (Node.js 20+ required for NestJS 11+)
NestJS8+
TypeScript4.7+

Node.js 18 reached end-of-life in March 2025. For new projects, use Node.js 20 or 22.

Note: The examples in this guide use Uptrace as the OpenTelemetry backend. You can use any OTLP-compatible backend — replace OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS with your backend's endpoint and credentials.

Zero-code instrumentation

If you want traces without touching your application code, use environment variables with the --require flag:

shell
export OTEL_SERVICE_NAME=my-nestjs-app
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev # endpoint is embedded in DSN
export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=https://<secret>@api.uptrace.dev
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

node --require @opentelemetry/auto-instrumentations-node/register dist/main.js

Or via NODE_OPTIONS in your Docker or CI environment:

shell
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" node dist/main.js

This approach requires only one package:

shell
npm install @opentelemetry/auto-instrumentations-node

For production setups that need custom processors, sampling logic, or explicit instrumentation control, use the code-based approach below.

NestJS instrumentation

NestJS runs on top of Express (or Fastify), so you need to install instrumentations for both the framework layer and the HTTP transport layer. The @opentelemetry/instrumentation-nestjs-core package instruments NestJS-specific components: controllers, guards, interceptors, and pipes.

Install the required packages:

shell
# Using npm
npm install @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/instrumentation-nestjs-core \
  @opentelemetry/instrumentation-http \
  @opentelemetry/instrumentation-express

# Using yarn
yarn add @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/instrumentation-nestjs-core \
  @opentelemetry/instrumentation-http \
  @opentelemetry/instrumentation-express

If you prefer minimal explicit dependencies over the full auto-instrumentations-node bundle, you can install only what you need. The three packages above — instrumentation-nestjs-core, instrumentation-http, and instrumentation-express — cover the vast majority of production use cases.

Usage

After installing OpenTelemetry, configure the SDK to export data to an observability backend.

Create a dedicated file src/otel.ts:

typescript
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'

const dsn = process.env.UPTRACE_DSN
console.log('using dsn:', dsn)

const traceExporter = new OTLPTraceExporter({
  url: 'https://api.uptrace.dev/v1/traces',
  headers: { 'uptrace-dsn': dsn },
  compression: 'gzip' as any,
})

const sdk = new NodeSDK({
  spanProcessor: new BatchSpanProcessor(traceExporter, {
    maxExportBatchSize: 1000,
    maxQueueSize: 1000,
  }),
  resource: resourceFromAttributes({
    'service.name': 'my-nestjs-app',
    'service.version': '1.0.0',
    'deployment.environment': process.env.NODE_ENV || 'development',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'https://api.uptrace.dev/v1/metrics',
      headers: { 'uptrace-dsn': dsn },
    }),
    exportIntervalMillis: 15000,
  }),
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new NestInstrumentation(),
  ],
})

sdk.start()

process.on('SIGTERM', () => {
  sdk
    .shutdown()
    .then(() => console.log('OpenTelemetry SDK shut down'))
    .catch((err) => console.error('Error shutting down SDK', err))
    .finally(() => process.exit(0))
})

Note on Examples: This guide uses Uptrace as the OpenTelemetry backend in code examples. OpenTelemetry is vendor-neutral and works with any OTLP-compatible backend — Jaeger, Grafana Cloud, Honeycomb, and others. Replace OTEL_EXPORTER_OTLP_ENDPOINT and the uptrace-dsn header with your backend's endpoint and auth headers.

Critical: Initialization Order

The most common issue when setting up OpenTelemetry in NestJS is initialization order. The SDK must be started before NestJS imports any modules — otherwise, auto-instrumentation patches won't apply and you'll see incomplete or missing traces.

The correct approach is to load otel.ts via the --require flag before the application entry point:

shell
# Development (ts-node)
ts-node --require ./src/otel.ts src/main.ts

# Production (compiled)
node --require ./dist/otel.js dist/main.js

Update your package.json scripts:

json
{
  "scripts": {
    "start": "node --require ./dist/otel.js dist/main.js",
    "start:dev": "ts-node --require ./src/otel.ts src/main.ts",
    "start:debug": "node --inspect --require ts-node/register --require ./src/otel.ts src/main.ts"
  }
}

Do not import otel.ts at the top of main.ts. While it may appear to work in development, it causes race conditions during module resolution and breaks instrumentation for lazily-loaded NestJS modules. The --require flag guarantees the SDK initializes before any application code runs.

Instrumentation Options

The NestInstrumentation class supports configuration options for customizing behavior:

OptionDescription
enabledEnable or disable the instrumentation entirely

The HttpInstrumentation layer provides the main filtering options. Use ignoreIncomingRequestHook to exclude health check endpoints from traces:

typescript
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'

const httpInstrumentation = new HttpInstrumentation({
  ignoreIncomingRequestHook: (request) => {
    const ignorePatterns = ['/health', '/ready', '/metrics', '/favicon.ico']
    return ignorePatterns.some((pattern) => request.url?.includes(pattern))
  },
})

Semconv migration

Starting with @opentelemetry/instrumentation-nestjs-core v0.52.0, you can opt into the stable HTTP semantic conventions (v1.23.0) using an environment variable:

shell
# Use stable HTTP semconv (recommended for new projects)
export OTEL_SEMCONV_STABILITY_OPT_IN=http

# Use both old and new semconv (for gradual migration)
export OTEL_SEMCONV_STABILITY_OPT_IN=http/dup

Without this variable set, the instrumentation defaults to the older v1.7.0 conventions.

Custom Spans

In addition to automatic instrumentation, you can create custom spans to trace specific operations inside your services:

typescript
import { Injectable } from '@nestjs/common'
import { trace, SpanStatusCode } from '@opentelemetry/api'

const tracer = trace.getTracer('my-nestjs-app')

@Injectable()
export class OrderService {
  async processOrder(orderId: string): Promise<Order> {
    return tracer.startActiveSpan('process-order', async (span) => {
      try {
        span.setAttribute('order.id', orderId)

        const order = await this.orderRepository.findOne(orderId)
        span.setAttribute('order.status', order.status)

        const result = await this.fulfillmentService.fulfill(order)
        span.setAttribute('order.fulfilled', true)

        return result
      } catch (error) {
        span.recordException(error)
        span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
        throw error
      } finally {
        span.end()
      }
    })
  }
}

Use startActiveSpan instead of startSpan when you need child spans to be automatically associated with the parent — it sets the span as active in the current context, so any downstream instrumentation (database calls, HTTP requests) will attach to it automatically.

Database Instrumentation

OpenTelemetry provides span-level visibility into every database query through dedicated instrumentation packages.

TypeORM

getNodeAutoInstrumentations() does not currently include TypeORM instrumentation. It still captures lower-level database spans for drivers such as pg and mysql, but if you want ORM-level TypeORM spans you need to install the package explicitly:

shell
npm install @opentelemetry/instrumentation-typeorm
typescript
import { TypeormInstrumentation } from '@opentelemetry/instrumentation-typeorm'

const sdk = new NodeSDK({
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new NestInstrumentation(),
    new TypeormInstrumentation({
      enhancedDatabaseReporting: true, // include query params in span attributes
    }),
  ],
})

Each query produces a span with db.system.name, db.query.text, and db.operation.name attributes.

Prisma

Use the official @prisma/instrumentation package:

shell
npm install @prisma/instrumentation
typescript
import { PrismaInstrumentation } from '@prisma/instrumentation'

const sdk = new NodeSDK({
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new NestInstrumentation(),
    new PrismaInstrumentation(),
  ],
})

Distributed Tracing and Context Propagation

When NestJS is part of a microservices architecture, context propagation carries trace context across service boundaries automatically. HttpInstrumentation injects W3C traceparent headers into every outgoing HTTP request and extracts them from incoming requests — no manual code required.

For NestJS microservices using gRPC, Kafka, or RabbitMQ, add the corresponding instrumentation package:

shell
# gRPC
npm install @opentelemetry/instrumentation-grpc

# Kafka (kafkajs)
npm install @opentelemetry/instrumentation-kafkajs
typescript
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc'

const sdk = new NodeSDK({
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new NestInstrumentation(),
    new GrpcInstrumentation(),
  ],
})

With context propagation in place, a single trace links spans from the API gateway through NestJS controllers, downstream microservices, and database queries into one end-to-end view.

HTTP Metrics

OpenTelemetry automatically collects HTTP server metrics when metrics export is configured:

MetricDescription
http.server.request.durationDuration of HTTP server requests (histogram)
http.server.request.body.sizeSize of HTTP server request bodies
http.server.response.body.sizeSize of HTTP server response bodies
http.server.active_requestsNumber of concurrent active requests

These metrics are emitted by HttpInstrumentation and available in your backend as soon as the PeriodicExportingMetricReader is configured. In Uptrace, they appear in the built-in HTTP dashboard under the service name.

For custom business metrics (counters, gauges, histograms), see the OpenTelemetry Metrics guide.

Production Configuration

Sampling

At full request volume, exporting 100% of traces is expensive. Configure a ratio-based sampler via environment variables — no code changes required:

shell
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1

parentbased_traceidratio at 0.1 samples 10% of new root traces while respecting the sampling decision of upstream services. Adjust the ratio based on your request volume and cost. For a full explanation of sampler types, see the OpenTelemetry Sampling guide.

Or configure the sampler inline in otel.ts:

typescript
import { TraceIdRatioBasedSampler, ParentBasedSampler } from '@opentelemetry/sdk-trace-base'

const sdk = new NodeSDK({
  sampler: new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(
      process.env.NODE_ENV === 'production' ? 0.1 : 1.0
    ),
  }),
  // ... rest of config
})

Batch processor tuning

For high-throughput services, tune the BatchSpanProcessor to avoid queue overflow:

typescript
new BatchSpanProcessor(traceExporter, {
  maxExportBatchSize: 512,
  maxQueueSize: 2048,
  scheduledDelayMillis: 5000,
  exportTimeoutMillis: 30000,
})

Increase maxQueueSize if you see BatchSpanProcessor queue size limit reached in logs — this means spans are being dropped.

Deploying with Docker

Use the --require flag in your Dockerfile CMD to ensure the SDK initializes before NestJS:

dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "--require", "./dist/otel.js", "dist/main.js"]

Pass backend credentials at runtime so they stay out of the image:

yaml
# docker-compose.yml
services:
  api:
    build: .
    environment:
      - UPTRACE_DSN=https://<secret>@api.uptrace.dev
      - NODE_ENV=production
      - OTEL_TRACES_SAMPLER=parentbased_traceidratio
      - OTEL_TRACES_SAMPLER_ARG=0.1
    ports:
      - '3000:3000'

Troubleshooting

No traces appearing
Confirm you are using --require ./dist/otel.js and not importing otel.ts from inside main.ts. Add OTEL_LOG_LEVEL=debug to see SDK startup logs and verify the exporter endpoint is reachable.

Missing spans for controllers
@opentelemetry/instrumentation-nestjs-core must be listed in the instrumentations array. If you use getNodeAutoInstrumentations(), it is included automatically. Confirm the package version is ≥ 0.40.0.

Incomplete traces across services
If child spans from downstream services appear as separate traces, context propagation is broken. Verify both services have HttpInstrumentation configured and that the upstream service injects traceparent headers into outgoing requests.

High memory usage
Reduce maxQueueSize in BatchSpanProcessor or lower the sampling rate. The default queue holds up to 2048 spans in memory; at high request volumes this can accumulate if the exporter is slow.

Fastify adapter
@opentelemetry/instrumentation-fastify was removed from auto-instrumentations-node in March 2026. Use @fastify/otel — the official Fastify OpenTelemetry plugin — instead. @opentelemetry/instrumentation-nestjs-core works with both adapters.

What is Uptrace?

Uptrace is an OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues. For Node.js instrumentation, see the OpenTelemetry JavaScript guide.

Uptrace Overview

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.

FAQ

What does @opentelemetry/instrumentation-nestjs-core instrument?
It instruments NestJS-specific application layers: the NestFactory.create bootstrap call, controller method handlers, and the full request context lifecycle. For HTTP transport (request/response, status codes, headers), you still need @opentelemetry/instrumentation-http and @opentelemetry/instrumentation-express.

Why are my traces missing or incomplete?
The most likely cause is initialization order. OpenTelemetry must be loaded before NestJS bootstraps. Move your sdk.start() call to a separate otel.ts file and load it with --require ./otel.js instead of importing it at the top of main.ts. See the Critical: Initialization Order section for details.

Does this work with NestJS on Fastify?
Yes, but replace @opentelemetry/instrumentation-express with @fastify/otel — the official Fastify OpenTelemetry plugin. The @opentelemetry/instrumentation-fastify package was removed from auto-instrumentations-node in March 2026. The @opentelemetry/instrumentation-nestjs-core package works with both adapters.

Does OpenTelemetry work with NestJS microservices?
Yes. OpenTelemetry instruments the transport layer, so gRPC, Kafka, RabbitMQ, and Redis-based microservices are covered by their respective instrumentation packages (@opentelemetry/instrumentation-grpc, @opentelemetry/instrumentation-kafkajs, etc.). Context propagation across microservice boundaries works automatically as long as the receiving service also runs OTel instrumentation.

How do I trace TypeORM or Prisma database queries?
Use @opentelemetry/instrumentation-typeorm for TypeORM or @prisma/instrumentation for Prisma. Both can be added to the instrumentations array in your SDK configuration. getNodeAutoInstrumentations() does not currently include TypeORM, so install it explicitly if you want ORM-level spans.

What is the performance overhead of OpenTelemetry in production?
With BatchSpanProcessor and a reasonable sampling rate, the overhead is typically under 3-5% for most NestJS applications. The batch processor buffers spans in memory and exports them asynchronously, minimizing impact on request latency. For high-traffic services, configure head-based sampling using OTEL_TRACES_SAMPLER=parentbased_traceidratio and OTEL_TRACES_SAMPLER_ARG=0.1 to sample 10% of traces.

Can I use OpenTelemetry with NestJS and GraphQL?
Yes. Add @opentelemetry/instrumentation-graphql to your SDK configuration. It traces resolver execution, query parsing, and validation. Each GraphQL operation appears as a span with graphql.operation.type and graphql.operation.name attributes.

What's Next?

With NestJS instrumented, connect it to the rest of your stack for end-to-end visibility: