Getting Started
Stand up Jaeger and the OpenTelemetry Collector with Docker, trace a Node.js app end-to-end
Getting Started
This page boots Jaeger (the trace backend) and the OTel Collector (the data pipeline), then traces a Node.js app. Same SDK works with any backend.
Stand Up the Stack
# docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:1.55
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
otel-collector:
image: otel/opentelemetry-collector-contrib:0.110.0
command: ["--config=/etc/otelcol-contrib/config.yaml"]
volumes:
- ./otel-config.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4319:4318" # apps push here; collector forwards to Jaeger
depends_on: [jaeger]# otel-config.yaml
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
send_batch_size: 1024
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger, debug]docker compose up -d
open http://localhost:16686 # Jaeger UIThe collector sits between your apps and the backend. You can change backends by editing the collector config — your apps never know.
Why a Collector?
Apps could send directly to Jaeger, but the collector adds:
- Batching / buffering so apps don't block on backend issues
- Filtering / sampling — drop noisy spans before they hit storage
- Multi-backend fan-out — send the same trace to Jaeger and an APM SaaS
- Enrichment — add cluster / region tags automatically
- Protocol translation — apps speak OTLP; backends might want their own format
Use the collector. It's a small ops cost for big flexibility.
Instrument a Node.js App
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions// tracing.js — load FIRST, before anything else
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'demo-api',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'dev',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4319/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());// app.js — load tracing.js FIRST
require('./tracing');
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.get('/checkout', async (req, res) => {
const user = await fetch('http://localhost:3001/user').then(r => r.json());
const inventory = await fetch('http://localhost:3001/inventory').then(r => r.json());
res.json({ user, inventory });
});
app.listen(3000);Run it, hit the endpoint, open Jaeger UI — you see the trace with Express, HTTP client, and the downstream service all linked.
getNodeAutoInstrumentations() patches Express, Fastify, http/https, pg, mysql, redis, kafkajs, ioredis, grpc, AWS SDK, MongoDB, GraphQL, and more. You get free tracing for ~30+ popular libraries.
Same Pattern, Other Languages
| Language | SDK install |
|---|---|
| Python | pip install opentelemetry-distro opentelemetry-exporter-otlp then opentelemetry-bootstrap -a install |
| Java | Java agent JAR + -javaagent:opentelemetry-javaagent.jar |
| Go | go get go.opentelemetry.io/otel — manual SDK setup; auto-instr via libraries |
| Ruby | gem install opentelemetry-sdk + per-library packages |
| .NET | dotnet add package OpenTelemetry.Instrumentation.* |
| Rust | tracing crate + tracing-opentelemetry |
| PHP | OTel PHP SDK; per-framework integrations |
OTel SDKs exist for every popular language. Use them; don't roll your own.
Add Manual Spans
Auto-instrumentation gets you 80% of the value. For business-logic boundaries you want manual spans:
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('checkout-service');
app.post('/checkout', async (req, res) => {
const span = tracer.startSpan('process-checkout');
try {
span.setAttribute('user.id', req.user.id);
span.setAttribute('cart.size', req.body.items.length);
span.setAttribute('cart.total_cents', req.body.total);
const result = await processOrder(req.body);
span.setStatus({ code: 1 }); // OK
res.json(result);
} catch (err) {
span.recordException(err);
span.setStatus({ code: 2, message: err.message });
res.status(500).json({ error: err.message });
} finally {
span.end();
}
});Attributes are key/value pairs on the span — visible in the trace UI, searchable, filterable. Use them generously for business context (user ID, account tier, request ID, A/B variant).
Context Propagation
When you call another service, the OTel HTTP instrumentation automatically injects the traceparent header. On the receiving side, the auto-instrumentation extracts it and links the new span to the parent.
Service A Service B
fetch(B) → HTTP Express middleware
traceparent: 00-abc123-001 extracts traceparent
creates span linked to 001For message queues, you propagate manually:
// Producer
const ctx = context.active();
const headers = {};
propagation.inject(ctx, headers); // populates traceparent into headers
producer.publish(message, { headers });
// Consumer
const parentCtx = propagation.extract(context.active(), msg.headers);
const span = tracer.startSpan('process-message', undefined, parentCtx);Tear Down
docker compose down -vWhat's Next
You have a working trace pipeline. Next:
- Instrumentation — auto vs manual, attributes, errors, propagation across protocols
- Best Practices — sampling, retention, correlating with logs, cost, pitfalls