
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Debugging latency across microservices requires visibility that logs alone cannot provide. Distributed tracing with OpenTelemetry and Jaeger gives you end-to-end request visualization, letting you pinpoint exactly which service or database call causes bottlenecks. This guide covers the practical implementation steps for instrumenting applications, configuring the OpenTelemetry Collector, and deploying Jaeger as your analysis backend.
How does distributed tracing with OpenTelemetry and Jaeger work?
Understanding the data flow prevents misconfiguration later. The architecture relies on three distinct components: the instrumentation SDK embedded in your application, a collector that processes telemetry signals, and the Jaeger backend for storage and query. When a user request hits your system, the SDK creates a trace ID and propagates it through HTTP headers or message queues. Each service adds child spans representing local operations like database queries or external API calls.
The OpenTelemetry Collector sits between your apps and Jaeger for good reason. Direct exports from every pod to Jaeger create connection churn and backpressure risks during traffic spikes. The collector buffers, batches, and can filter sensitive data before it reaches storage. For teams managing infrastructure as code, defining this pipeline declaratively is essential; see my guide on infrastructure as code with Terraform for patterns on provisioning these observability components reproducibly.
How do you instrument an application for OpenTelemetry tracing?
Instrumentation strategy depends on your language runtime and framework. Auto-instrumentation libraries handle common frameworks (Express, Spring Boot, Django) without code changes, while manual instrumentation captures business-specific logic. In production, I recommend starting with auto-instrumentation to get baseline coverage, then adding manual spans only where latency attribution is unclear.
Auto-instrumentation example (Node.js)
For Node.js services, the @opentelemetry/auto-instrumentations-node package covers HTTP, Express, PostgreSQL, Redis, and gRPC out of the box. Initialize it before any other imports:
<!-- tracing.js -->
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start(); Manual span creation for business logic
Auto-instrumentation misses internal processing steps. Wrap critical sections manually to capture timing and attributes:
const { trace } = require('@opentelemetry/api');
async function processOrder(orderId) {
const tracer = trace.getTracer('order-service');
return tracer.startActiveSpan('process-order', async (span) => {
span.setAttribute('order.id', orderId);
try {
await validateInventory(orderId);
await chargePayment(orderId);
span.setStatus({ code: 1 }); // OK
} catch (err) {
span.recordException(err);
span.setStatus({ code: 2, message: err.message });
throw err;
} finally {
span.end();
}
});
} A common mistake is forgetting context propagation across async boundaries or message queues. If traces break at Kafka consumers or background workers, verify that your messaging library has an active instrumentation plugin. Broken propagation is the #1 cause of incomplete traces in my audit experience.
How do you configure the OpenTelemetry Collector for Jaeger?
The collector configuration defines receivers, processors, and exporters. A minimal production-ready config for Jaeger uses the OTLP receiver, batch processor, and OTLP exporter targeting Jaeger's gRPC endpoint. Avoid the deprecated Jaeger exporter; Jaeger natively accepts OTLP since v1.35+.
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_size: 1024
timeout: 5s
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/jaeger] The memory_limiter processor is non-negotiable in production. Without it, a sudden burst of telemetry can OOM-kill the collector, causing total observability loss during incidents—exactly when you need it most. Place it first in the processor chain to drop data early rather than buffering until failure.
What sampling strategy should you use for production tracing?
Tracing every request is cost-prohibitive at scale. Sampling decisions directly impact your ability to diagnose issues versus your storage bill. Choose based on traffic volume and compliance requirements.
| Strategy | Best For | Trade-off | Config Location |
|---|---|---|---|
| Head-based (probabilistic) | High-traffic services (>1k RPS) | May miss rare errors | SDK or Collector |
| Tail-based (error-aware) | Critical paths, SLA monitoring | Higher memory/CPU on collector | Collector only |
| Parent-based | Microservice chains | Respects upstream decision | SDK default |
| Always-on | Dev/staging, low-traffic prod | Unscalable beyond ~100 RPS | SDK |
For SOC 2 or ISO 27001 audited environments, tail-based sampling is often worth the overhead. You retain 100% of error traces and slow requests while dropping successful fast ones. Configure this in the collector's tail_sampling processor with policies for latency thresholds and status codes. Never sample away evidence required for compliance audits.
How do you deploy Jaeger and the collector on Kubernetes?
Kubernetes deployments benefit from the OpenTelemetry Operator, which auto-injects sidecars and manages collector lifecycle. However, for teams already comfortable with Helm, a direct deployment is simpler to reason about. If you're new to container orchestration, review Kubernetes basics before attempting production tracing setups.
- Deploy Jaeger using the official Helm chart with Elasticsearch or Cassandra as backend. In-memory storage is fine for dev but loses data on restart.
- Install OTel Collector as a Deployment (for centralized processing) or DaemonSet (for node-level metrics). Use ConfigMaps for the YAML shown above.
- Configure RBAC so pods can export traces. NetworkPolicies should allow egress to the collector on ports 4317/4318 only.
- Validate by generating test traffic and confirming traces appear in Jaeger UI within 30 seconds.
Resource limits matter more than most tutorials mention. Set collector memory requests to at least 512Mi and limits to 1Gi for moderate traffic. Undersized collectors silently drop spans under load, creating gaps precisely when debugging is hardest. Monitor collector metrics (otelcol_exporter_send_failed_spans) via Prometheus and Grafana to catch this before users complain.
Implementing distributed tracing with OpenTelemetry and Jaeger effectively
Successful adoption of distributed tracing with OpenTelemetry and Jaeger hinges on treating observability as a first-class engineering concern, not an afterthought. Start with auto-instrumentation to establish baseline visibility, layer in manual spans for business-critical paths, and enforce sampling policies that balance diagnostic fidelity against operational cost. Validate your pipeline end-to-end before relying on it during incidents. If your team needs help designing a compliant, scalable tracing architecture, reach out to discuss your specific requirements.