Distributed Tracing with OpenTelemetry and Jaeger

Khimananda Oli 6 min read Database
Distributed Tracing with OpenTelemetry and Jaeger

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.

App + OTel SDK(Trace Context)OTel Collector(Batch / Filter)Jaeger Backend(Query / UI)OTLP/gRPCOTLP/gRPC
Data flow for distributed tracing with OpenTelemetry and Jaeger: SDK exports via OTLP to Collector, then to Jaeger storage.

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.

OTLP ReceiverPrometheus RecvMemory LimiterBatch ProcessorOTLP → JaegerLogging ExporterPipeline: Receivers → Processors → Exporters
OTel Collector pipeline structure: memory limiter must precede batch to prevent OOM during traffic spikes.

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.

StrategyBest ForTrade-offConfig Location
Head-based (probabilistic)High-traffic services (>1k RPS)May miss rare errorsSDK or Collector
Tail-based (error-aware)Critical paths, SLA monitoringHigher memory/CPU on collectorCollector only
Parent-basedMicroservice chainsRespects upstream decisionSDK default
Always-onDev/staging, low-traffic prodUnscalable beyond ~100 RPSSDK

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.

  1. 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.
  2. Install OTel Collector as a Deployment (for centralized processing) or DaemonSet (for node-level metrics). Use ConfigMaps for the YAML shown above.
  3. Configure RBAC so pods can export traces. NetworkPolicies should allow egress to the collector on ports 4317/4318 only.
  4. Validate by generating test traffic and confirming traces appear in Jaeger UI within 30 seconds.
Kubernetes ClusterApp Pod + SidecarOTel Auto-InstrApp Pod + SidecarOTel Auto-InstrOTel Collector(Deployment)Jaeger + ES(StatefulSet)
Kubernetes topology: sidecar-injected app pods export to centralized OTel Collector, which forwards to Jaeger backend.

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.

Frequently Asked Questions

It is an observability pattern where OpenTelemetry instruments code to generate trace data, and Jaeger visualizes request flows across microservices. This combination helps developers identify latency bottlenecks and errors in complex distributed systems by correlating spans into complete end-to-end transaction paths.

Deploy the collector via Helm or Docker using the official 2026 chart. Configure the OTLP receiver on port 4317 and set the Jaeger exporter endpoint in your config.yaml. Ensure network policies allow traffic between application pods, the collector, and the Jaeger backend storage service.

Yes, modern Jaeger versions accept OTLP directly without translation.

Start with probabilistic sampling at 1% to manage storage costs while capturing sufficient error patterns. Use tail-based sampling in the OpenTelemetry Collector to retain 100% of error traces and slow requests exceeding defined latency thresholds, discarding only successful low-latency noise to balance observability with infrastructure budget constraints.

The SDK injects W3C Trace Context headers into outgoing requests and extracts them from incoming ones automatically. This links spans across service boundaries without manual coding. Ensure all middleware and gateways forward these headers correctly, as stripping them breaks the trace chain and creates fragmented visibility in Jaeger.

Exemplars link Prometheus metrics directly to specific Jaeger traces.

Missing spans usually result from dropped context headers during proxy forwarding or mismatched service names between instrumented libraries. Verify that load balancers preserve W3C Trace Context headers and that the OpenTelemetry SDK version matches your language runtime. Check collector logs for export failures or queue overflows causing silent data loss.

Elasticsearch or ClickHouse are preferred for production scale due to better query performance than Cassandra. ClickHouse offers superior compression and faster aggregation for trace analytics. Avoid in-memory storage outside local development. Ensure your chosen backend has adequate retention policies configured to prevent unbounded storage growth and excessive cloud costs.

Use the native C extension instead of pure PHP userland instrumentation to minimize CPU impact. Enable selective instrumentation for only critical frameworks like Eloquent and HTTP clients. Configure batch span processors with appropriate queue sizes to reduce synchronous blocking. Profile your application under load to verify tracing adds less than five percent latency overhead.

Both tools are open source with no licensing fees, but operational costs include compute, storage, and engineering time. Self-hosting requires provisioning Elasticsearch or object storage. Managed offerings charge per gigabyte ingested. Budget for infrastructure scaling as trace volume grows linearly with request traffic and retention duration requirements.

Configure OpenTelemetry attribute processors to redact PII before export using regex patterns. Never log request bodies or authentication tokens as span attributes. Enable TLS encryption between collectors and Jaeger backends. Implement RBAC in Jaeger UI to restrict access. Treat trace data with the same compliance controls as application logs and database records.

Jaeger offers richer UI features, gRPC support, and better Kubernetes integration compared to Zipkin. Zipkin has simpler deployment but lacks advanced querying and adaptive sampling capabilities. Both accept OTLP, but Jaeger aligns more closely with CNCF ecosystem standards in 2026. Choose based on your team's debugging complexity and existing infrastructure investments.

Enable debug logging and check the exporter_failed_spans metric. Validate endpoint connectivity using grpcurl against the Jaeger OTLP port. Inspect memory limits and batch timeout configurations that cause queue drops. Test with a file exporter temporarily to isolate whether failures stem from network issues, authentication errors, or malformed telemetry payloads.

Start with automatic instrumentation to gain immediate baseline visibility.

Retain full traces for seven days for incident response and aggregated metrics for ninety days for trend analysis. Full trace storage is expensive; use tiered storage policies moving older data to cold object storage. Align retention with compliance requirements and mean time to detection goals rather than keeping everything indefinitely.