NestJS OpenTelemetry instrumentation
This guide instruments a NestJS service with the OpenTelemetry Node.js SDK. NestJS runs on Express or Fastify under the hood, so the auto-instrumentations capture controller routes and outbound calls with no changes to your handlers. The OTLP exporters send traces, metrics, and logs to Osuite.
Prerequisites
Section titled “Prerequisites”- Node.js 18 or newer.
- A NestJS project that compiles to
dist/. - An Osuite ingest token from Settings → API Keys.
Instrument the service
Section titled “Instrument the service”-
Install the OpenTelemetry packages
Terminal window npm install @opentelemetry/sdk-node \@opentelemetry/auto-instrumentations-node \@opentelemetry/exporter-trace-otlp-proto \@opentelemetry/exporter-metrics-otlp-proto \@opentelemetry/exporter-logs-otlp-proto -
Create the tracing module
Create
src/tracing.ts. It starts the SDK on import and is compiled todist/tracing.jsalongside the rest of your build.src/tracing.ts import { NodeSDK } from '@opentelemetry/sdk-node';import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';const sdk = new NodeSDK({traceExporter: new OTLPTraceExporter(),metricReader: new PeriodicExportingMetricReader({exporter: new OTLPMetricExporter(),}),logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],instrumentations: [getNodeAutoInstrumentations()],});sdk.start();process.on('SIGTERM', () => {sdk.shutdown().finally(() => process.exit(0));}); -
Load it before the application boots
The SDK must start before
@nestjs/coreand its HTTP platform are loaded. Add this as the very first line ofsrc/main.ts, above every other import:src/main.ts import './tracing';import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() {const app = await NestFactory.create(AppModule);await app.listen(3000);}bootstrap();
Configure and run
Section titled “Configure and run”The exporters and resource are configured entirely through environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT— your Osuite ingest endpoint.OTEL_EXPORTER_OTLP_HEADERS— thex-osuite-ingest-tokenheader that authenticates ingestion.OTEL_SERVICE_NAME— the service name that identifies this service in Osuite.OTEL_RESOURCE_ATTRIBUTES— additional resource attributes such asservice.environment.
Build the project (npm run build), then run the compiled output. Choose your deployment target:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443"export OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>"export OTEL_SERVICE_NAME="billing-service"export OTEL_RESOURCE_ATTRIBUTES="service.environment=production"
node dist/main.jsBecause main.ts imports ./tracing first, no --require flag is needed. If you prefer not to touch main.ts, drop that import and start with node --require ./dist/tracing.js dist/main.js instead.
# DockerfileFROM node:22-slimWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run buildCMD ["node", "dist/main.js"]Pass the configuration at run time so the ingest token never lands in the image:
docker run \ -e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443" \ -e OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>" \ -e OTEL_SERVICE_NAME="billing-service" \ -e OTEL_RESOURCE_ATTRIBUTES="service.environment=production" \ billing-serviceStore the ingest token in a Secret:
kubectl create secret generic osuite-ingest \ --from-literal=otlp-headers="x-osuite-ingest-token=<your-ingest-token>"Reference it from the Deployment:
apiVersion: apps/v1kind: Deploymentmetadata: name: billing-servicespec: replicas: 1 selector: matchLabels: app: billing-service template: metadata: labels: app: billing-service spec: containers: - name: billing-service image: billing-service:latest command: ["node", "dist/main.js"] env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "https://ingest.<region>.osuite.io:443" - name: OTEL_EXPORTER_OTLP_HEADERS valueFrom: secretKeyRef: name: osuite-ingest key: otlp-headers - name: OTEL_SERVICE_NAME value: "billing-service" - name: OTEL_RESOURCE_ATTRIBUTES value: "service.environment=production"Framework notes
Section titled “Framework notes”Verify in Osuite
Section titled “Verify in Osuite”What you should see
Send a request to a controller route, then open APM. The service appears within a minute under the service.name you set, emitting request-rate and latency metrics. Opening a trace shows the inbound route span with child spans for downstream providers and outbound calls.
Troubleshooting
Section titled “Troubleshooting”No data in Osuite after a few minutes? Work through these checks.
- Endpoint — confirm the exporter targets exactly
ingest.<region>.osuite.io:443for your region, over TLS. - Token — confirm the ingest header carries a valid
<your-ingest-token>and has not been rotated. - Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the
otlp/osuiteexporter attached. - Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
- Network — confirm the host has outbound access to the ingest endpoint on port
443. - Timing — allow up to a minute for the first data to appear before assuming a failure.
Still stuck? Ask the Investigation Agent or contact support.
For Node-specific issues — ESM vs CommonJS loading, the --require flag, or missing spans — see Node.js troubleshooting.
Next steps
Section titled “Next steps”- Add custom spans and metrics for your business logic.
- Correlate application logs with traces.