Distributed Tracing
A comprehensive guide to distributed tracing in Kubernetes covering Jaeger, Zipkin, OpenTelemetry, trace collection, performance analysis, and practical implementation strategies for microservices observability.
Distributed tracing is a method of monitoring and profiling applications built on microservices architecture. It tracks requests as they traverse through different services, providing visibility into the entire request flow, including latency, errors, and dependencies.
Distributed tracing helps answer critical questions:
- Where is the latency? Which service is slowing down requests?
- What are the dependencies? Which services call which other services?
- Where are errors occurring? Which service is failing?
- What is the flow? How does a request move through the system?
- What is the impact? How does a change affect performance?
- Trace: The complete journey of a request through all services
- Span: A single unit of work within a trace (e.g., an API call or database query)
- Context Propagation: Passing trace context between services via HTTP headers
- Tags: Key-value pairs that add metadata to spans
- Trace ID: A unique identifier for a trace, passed between services
Jaeger
Zipkin
OpenTelemetry
OpenTelemetry Collector
# Install Jaeger Operator
kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/latest/download/jaeger-operator.yaml
# Verify installation
kubectl get pods -n observability
# Create Jaeger instance
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
namespace: observability
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch-master:9200
index-prefix: jaeger
sampling:
options:
default_strategy:
type: probabilistic
param: 0.1 # Sample 10% of traces
ingress:
enabled: true
hostname: jaeger.example.com
agent:
strategy: DaemonSet
# Apply Jaeger instance
kubectl apply -f jaeger-instance.yaml
# Verify Jaeger deployment
kubectl get pods -n observability -l app=jaeger
kubectl get svc -n observability -l app=jaeger
# Access Jaeger UI
kubectl port-forward -n observability svc/jaeger-query 16686:16686
# Install Jaeger with Helm (alternative)
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update
helm install jaeger jaegertracing/jaeger \
--namespace observability \
--create-namespace \
--set provisionDataStore.cassandra=false \
--set storage.type=elasticsearch \
--set storage.elasticsearch.host=elasticsearch-master \
--set storage.elasticsearch.port=9200 \
--set agent.enabled=true \
--set collector.enabled=true \
--set query.enabled=true
OpenTelemetry is the industry standard for instrumentation. It provides SDKs for multiple languages to generate and export traces.
# Java Application with OpenTelemetry
# Add dependencies to pom.xml
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.30.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.30.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-jaeger</artifactId>
<version>1.30.0</version>
</dependency>
# Java code instrumentation
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class OrderService {
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");
public void createOrder(OrderRequest request) {
Span span = tracer.spanBuilder("createOrder")
.setAttribute("order.id", request.getId())
.setAttribute("customer.id", request.getCustomerId())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Business logic
processPayment(request);
updateInventory(request);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
# Python Application with OpenTelemetry
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
# Setup tracer
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer("order-service")
# Create Jaeger exporter
jaeger_exporter = JaegerExporter(
agent_host_name="jaeger-agent.observability.svc.cluster.local",
agent_port=6831,
)
# Add span processor
span_processor = BatchSpanProcessor(jaeger_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Instrument a function
with tracer.start_as_current_span("create_order") as span:
span.set_attribute("order.id", order_id)
process_payment()
update_inventory()
# Node.js Application with OpenTelemetry
const { trace, context } = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const provider = new NodeTracerProvider();
const exporter = new JaegerExporter({
agentHost: 'jaeger-agent.observability.svc.cluster.local',
agentPort: 6832,
});
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
const tracer = trace.getTracer('order-service');
tracer.startActiveSpan('createOrder', (span) => {
span.setAttribute('order.id', orderId);
processPayment();
updateInventory();
span.end();
});
- Use OpenTelemetry SDKs for consistent instrumentation across languages
- Add meaningful span names and attributes
- Use context propagation to maintain trace continuity
- Set span status for errors and exceptions
- Use sampling to control trace volume
- Add baggage for cross-service context
The OpenTelemetry Collector provides a vendor-agnostic way to receive, process, and export telemetry data.
# OpenTelemetry Collector Configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: observability
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
attributes:
actions:
- key: environment
value: production
action: upsert
exporters:
jaeger:
endpoint: jaeger-collector.observability.svc.cluster.local:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp, jaeger]
processors: [memory_limiter, batch, attributes]
exporters: [jaeger]
# Deploy OpenTelemetry Collector
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: observability
spec:
replicas: 2
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: collector
image: otel/opentelemetry-collector-contrib:latest
args:
- --config=/etc/otel/config.yaml
ports:
- containerPort: 4317
name: otlp-grpc
- containerPort: 4318
name: otlp-http
- containerPort: 14268
name: jaeger-thrift
volumeMounts:
- name: config
mountPath: /etc/otel
volumes:
- name: config
configMap:
name: otel-collector-config
- Use the Collector as a central trace aggregation point
- Configure batching to reduce network overhead
- Use memory limiting to prevent OOM issues
- Add global attributes (environment, cluster) to all traces
- Use tail-based sampling for critical traces
Context propagation is the mechanism by which trace context is passed between services. It's essential for creating a complete distributed trace.
# HTTP Headers (W3C Trace Context)
# Standard headers for context propagation:
# - traceparent: version-traceid-parentspanid-flags
# - tracestate: vendor-specific trace state
# Example: traceparent
# 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# Format: version-trace_id-parent_id-flags
# HTTP Propagation Example (Client)
import requests
from opentelemetry import trace
from opentelemetry.propagate import inject
tracer = trace.get_tracer("client")
with tracer.start_as_current_span("make_request") as span:
headers = {}
inject(headers) # Injects trace context into headers
response = requests.get("http://backend-service", headers=headers)
# HTTP Propagation Example (Server)
from opentelemetry.propagate import extract
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
def handle_request(request):
# Extract trace context from headers
ctx = extract(request.headers)
with trace.start_as_current_span("handle_request", context=ctx) as span:
span.set_attribute("http.method", request.method)
# Process request
# gRPC Context Propagation
import grpc
from opentelemetry import trace
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
# Client-side
with tracer.start_as_current_span("grpc_call") as span:
# Metadata automatically carries trace context
response = stub.Call(request)
# Server-side
class ServiceServicer(ServiceServicer):
def Call(self, request, context):
# Trace context automatically extracted
with tracer.start_as_current_span("handle_call") as span:
return response
Jaeger provides a powerful UI for analyzing traces, identifying performance issues, and understanding service dependencies.
# Jaeger UI Features
# 1. Service Dependencies Graph
# Shows how services are connected and traffic flow
# Identifies bottleneck services and dependencies
# 2. Trace Search
# Search by service, operation, tags, and time range
# Find specific traces for analysis
jaeger_query:
search:
service: order-service
operation: createOrder
minDuration: 1s
maxDuration: 5s
# 3. Trace Flame Graph
# Visual representation of spans with timing
# Identify where latency is occurring
# Zoom into specific spans for details
# 4. Trace Detail
# View span details, tags, and logs
# Analyze performance per operation
# View trace context and propagation
# 5. Performance Analytics
# Span statistics per service
# Percentile latency tracking
# Error rate analysis
# Jaeger Query API
# Get traces for a service
curl "http://jaeger-query:16686/api/traces?service=order-service&limit=20"
# Get specific trace
curl "http://jaeger-query:16686/api/traces/{trace_id}"
# Get services
curl "http://jaeger-query:16686/api/services"
# Get operations
curl "http://jaeger-query:16686/api/services/{service_name}/operations
- Search for slow traces to identify latency issues
- Look for error traces to find failures
- Analyze service dependencies for optimization opportunities
- Use trace comparisons for performance tuning
- Set up automated analysis for common patterns
- Create custom dashboards for key metrics
Sampling controls how many traces are collected. It's essential for managing trace volume and storage costs.
Probabilistic Sampling
Rate-Limiting Sampling
Head-Based Sampling
Tail-Based Sampling
# Jaeger Sampling Configuration
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
spec:
sampling:
options:
default_strategy:
type: probabilistic
param: 0.1 # Sample 10%
service_strategies:
- service: order-service
type: rate_limiting
param: 5 # 5 traces per second
- service: payment-service
type: probabilistic
param: 0.5 # Sample 50%
# OpenTelemetry Collector Sampling
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100
expected_new_traces_per_sec: 10
policies:
- name: errors-policy
type: status_code
status_code: ERROR
- name: slow-policy
type: latency
latency_threshold: 5s
- name: random-policy
type: probabilistic
sampling_rate: 0.1
# Sampling in application
from opentelemetry.sdk.trace import sampling
class MySampler(sampling.Sampler):
def should_sample(self, parent_context, trace_id, name, kind, attributes, links):
# Sample based on custom logic
if name == "critical_operation":
return sampling.Decision.RECORD_AND_SAMPLE
return sampling.Decision.RECORD_ONLY
- Start with 10-20% probabilistic sampling for production
- Use tail-based sampling to capture slow and error traces
- Adjust sampling rates based on trace volume and storage capacity
- Use service-specific sampling strategies
- Monitor trace collection volume to prevent backpressure
| Feature | Jaeger | Zipkin | OpenTelemetry |
|---|---|---|---|
| Architecture | Distributed | Centralized | Unified standard |
| Storage | Elasticsearch, Cassandra | MySQL, Cassandra, Elasticsearch | Various |
| UI | Rich, modern | Simple, clean | Varies |
| Sampling | Adaptive | Static | Configurable |
| Service Dependencies | Yes | Limited | Yes |
| Performance | High | Good | Good |
| Complexity | Medium | Low | Medium |
| Best For | Production, large scale | Small to medium | Standard instrumentation |
Distributed tracing is essential for understanding complex microservices interactions. Start with Jaeger and OpenTelemetry, instrument your critical services, and use tracing data to optimize performance and troubleshoot issues.