# OpenTelemetry NestJS Instrumentation

> Instrument a NestJS application with OpenTelemetry — auto-instrumentation, custom spans, distributed tracing, and exporting traces and metrics to Uptrace.

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

<table>
<thead>
  <tr>
    <th>
      Step
    </th>
    
    <th>
      Action
    </th>
    
    <th>
      Detail
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      1. Install
    </td>
    
    <td>
      Add OpenTelemetry packages
    </td>
    
    <td>
      <code>
        npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      2. Configure
    </td>
    
    <td>
      Create <code>
        src/otel.ts
      </code>
    </td>
    
    <td>
      See <a href="#usage">
        Usage
      </a>
      
       section below
    </td>
  </tr>
  
  <tr>
    <td>
      3. Run
    </td>
    
    <td>
      Load with <code>
        --require
      </code>
      
       flag
    </td>
    
    <td>
      <code>
        node --require ./dist/otel.js dist/main.js
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      4. Verify
    </td>
    
    <td>
      Check traces in your backend
    </td>
    
    <td>
      Spans appear automatically
    </td>
  </tr>
</tbody>
</table>

**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](/opentelemetry/distributed-tracing), [metrics](/opentelemetry/metrics), and [logs](/opentelemetry/logs) across multiple languages and platforms.

OpenTelemetry enables developers to instrument their code and export telemetry to any [OpenTelemetry backend](/blog/opentelemetry-backend) for analysis and visualization. The [OpenTelemetry architecture](/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](/get/opentelemetry-js/zero-code).

## Prerequisites

Before instrumenting, make sure your environment meets these requirements:

<table>
<thead>
  <tr>
    <th>
      Requirement
    </th>
    
    <th>
      Version
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Node.js
    </td>
    
    <td>
      18.19.0+ (Node.js 20+ required for NestJS 11+)
    </td>
  </tr>
  
  <tr>
    <td>
      NestJS
    </td>
    
    <td>
      8+
    </td>
  </tr>
  
  <tr>
    <td>
      TypeScript
    </td>
    
    <td>
      4.7+
    </td>
  </tr>
</tbody>
</table>

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:

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        enabled
      </code>
    </td>
    
    <td>
      Enable or disable the instrumentation entirely
    </td>
  </tr>
</tbody>
</table>

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](/opentelemetry/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:

<table>
<thead>
  <tr>
    <th>
      Metric
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        http.server.request.duration
      </code>
    </td>
    
    <td>
      Duration of HTTP server requests (histogram)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        http.server.request.body.size
      </code>
    </td>
    
    <td>
      Size of HTTP server request bodies
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        http.server.response.body.size
      </code>
    </td>
    
    <td>
      Size of HTTP server response bodies
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        http.server.active_requests
      </code>
    </td>
    
    <td>
      Number of concurrent active requests
    </td>
  </tr>
</tbody>
</table>

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](/opentelemetry/metrics).

## 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](/opentelemetry/sampling).

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**<br />


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**<br />

`@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**<br />


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**<br />


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**<br />

`@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](/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](/get/opentelemetry-js).

![Uptrace Overview](/home/screenshots/apm.png)

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](https://play.uptrace.dev/) (no login required) or running it locally with [Docker](/get/hosted/docker). The source code is available on [GitHub](https://github.com/uptrace/uptrace).

## FAQ

**What does @opentelemetry/instrumentation-nestjs-core instrument?**<br />


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?**<br />


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](#critical-initialization-order) section for details.

**Does this work with NestJS on Fastify?**<br />


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?**<br />


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?**<br />


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?**<br />


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?**<br />


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:

- **Express services**: [OpenTelemetry Express guide](/guides/opentelemetry-express) for other Node.js services in the same stack
- **Frontend**: [OpenTelemetry JavaScript](/get/opentelemetry-js) browser instrumentation to track sessions end-to-end
- **Sampling**: [OpenTelemetry Sampling](/opentelemetry/sampling) for production cost control
- **Context propagation**: [OpenTelemetry context propagation](/opentelemetry/context-propagation) for microservice architectures
