Integrating OpenTelemetry with NestJS and Grafana Tempo
Introduction
Modern distributed applications can get complex very quickly. Tracking down performance bottlenecks or debugging request flows across microservices is challenging without proper observability. That’s where OpenTelemetry (OTel) comes in. Combined with Grafana Tempo and Grafana dashboards, you get a powerful stack to collect, store, and visualize distributed traces.
In this tutorial, we’ll walk through setting up a NestJS application with OTel, running an OTel Collector, Tempo, and Grafana via Docker, and finally seeing our traces in Grafana.
What is OpenTelemetry (OTel)?
OpenTelemetry (OTel) is an open-source observability framework that provides a set of APIs, SDKs, and tools for instrumenting, collecting, and exporting telemetry data (traces, metrics, logs).
Why use OTel?
- Vendor neutral – works with Grafana, Jaeger, Zipkin, Prometheus, etc.
- End-to-end tracing – track requests across services.
- Debugging & optimization – understand latency and bottlenecks.
- Cloud-native ready – integrates with modern tools and orchestrators.
What is Grafana?
Grafana is a visualization and analytics platform. With the Tempo plugin, you can visualize traces collected by OTel in a powerful, interactive dashboard.
- Tempo = distributed tracing backend from Grafana Labs.
- Grafana + Tempo = visualize, search, and correlate traces with metrics/logs.
Step 1: Create a NestJS Project
# Install NestJS CLI if not already installed
npm i -g @nestjs/cli
## Create new project
nest new otel-nestjs-demo
cd otel-nestjs-demo
Step 2: Add a Test Module with Controller & Service
nest generate module hello
nest generate controller hello
nest generate service hello
- hello.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HelloService } from './hello.service';
@Controller('hello')
export class HelloController {
constructor(private readonly helloService: HelloService) {}
@Get()
getHello(): string {
return this.helloService.getHello();
}
}
- hello.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class HelloService {
getHello(): string {
return 'Hello from NestJS with OpenTelemetry!';
}
}
Step 3: Add OpenTelemetry to NestJS
- Install dependencies:
npm install @opentelemetry/api @opentelemetry/sdk-trace-node \
@opentelemetry/auto-instrumentations-node @opentelemetry/resources
- Create src/tracer.ts:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
const traceExporter = new OTLPTraceExporter({
url: 'http://localhost:4317', // OTel Collector endpoint
});
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'nestjs-otel-demo',
}),
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start()
.then(() => console.log('OpenTelemetry initialized'))
.catch((error) => console.log('Error initializing OTel', error));
process.on('SIGTERM', () => {
sdk.shutdown().then(() => console.log('Tracing terminated'));
});
- Update main.ts to import tracer.ts before starting NestJS:
import './tracer'; // must be first
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Step 4: Configure OTel Collector
- Create otel-collector.yaml in project root:
receivers:
otlp:
protocols:
grpc:
http:
exporters:
tempo:
endpoint: "http://tempo:4317"
tls:
insecure: true
logging:
service:
pipelines:
traces:
receivers: [otlp]
exporters: [tempo, logging]
Step 5: Run Grafana, Tempo, and OTel Collector with Docker
Run Grafana
docker run -d --name=grafana -p 3001:3000 grafana/grafana
Run Tempo
- Create tempo.yaml:
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
http:
ingester:
traces:
max_block_duration: 5m
storage:
trace:
backend: local
local:
path: /tmp/tempo
- Run Tempo:
docker run -d --name=tempo -v $(pwd)/tempo.yaml:/etc/tempo.yaml \
-p 3200:3200 -p 4317:4317 grafana/tempo:latest -config.file=/etc/tempo.yaml
Run OTel Collector
docker run -d --name=otel-collector \
-v $(pwd)/otel-collector.yaml:/etc/otel-collector.yaml \
-p 4317:4317 -p 4318:4318 otel/opentelemetry-collector:latest \
--config=/etc/otel-collector.yaml
Step 6: Test NestJS and View Traces
- Start NestJS:
npm run start:dev
- Test the endpoint:
curl http://localhost:3000/hello
- Now go to Grafana (http://localhost:3001), configure Tempo as a data source (http://tempo:3200), and query traces. You should see your NestJS requests visualized!
Conclusion
We’ve successfully:
-
✅ Created a NestJS app with OTel tracing
-
✅ Configured OTel Collector
-
✅ Set up Grafana & Tempo via Docker
-
✅ Visualized NestJS traces in Grafana
-
This setup gives you full observability into your NestJS services, making debugging and monitoring much easier.