Debugging latency issues in microservices
Scope: OpenTelemetry, spans, trace context, sampling, Jaeger, Zipkin, trace propagation
Lines: 381
Last Updated: 2025-10-18
Use this skill when:
Don't use for:
Trace: Complete journey of a request through the system
a1b2c3d4e5f6g7h8)Span: Single operation within a trace
GET /api/users)http.method, db.statement)Context: Propagated metadata
Trace: User purchases product
├─ Span 1: API Gateway (100ms)
│ ├─ Span 2: Auth Service (20ms)
│ │ └─ Span 3: Database query (5ms)
│ ├─ Span 4: Product Service (50ms)
│ │ ├─ Span 5: Database query (10ms)
│ │ └─ Span 6: Cache lookup (5ms)
│ └─ Span 7: Payment Service (30ms)
│ └─ Span 8: External API call (25ms)
Why: Reduce data volume and cost (storing 100% traces is expensive)
Strategies:
Why OpenTelemetry: Vendor-neutral, unified API for traces, metrics, logs
Components:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from fastapi import FastAPI
# Initialize tracer provider
provider = TracerProvider()
trace.set_tracer_provider(provider)
# Configure exporter (send to OTel Collector or Jaeger)
otlp_exporter = OTLPSpanExporter(
endpoint="http://localhost:4317", # gRPC endpoint
insecure=True
)
# Add span processor
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
# Get tracer
tracer = trace.get_tracer(__name__)
# Auto-instrument FastAPI
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
# Auto-instrument HTTP requests
RequestsInstrumentor().instrument()
# Auto-instrument SQLAlchemy
SQLAlchemyInstrumentor().instrument()
# Manual span creation
@app.get("/api/users/{user_id}")
async def get_user(user_id: str):
# Parent span created automatically by FastAPI instrumentation
# Create child span for business logic
with tracer.start_as_current_span("get_user_logic") as span:
span.set_attribute("user_id", user_id)
span.set_attribute("service", "user-service")
try:
# Database call (auto-instrumented)
user = await db.get_user(user_id)
# Add event
span.add_event("user_fetched", {"user_id": user_id})
# Create nested span for external API
with tracer.start_as_current_span("fetch_user_preferences"):
prefs = await external_api.get_preferences(user_id)
span.set_status(trace.Status(trace.StatusCode.OK))
return {"user": user, "preferences": prefs}
except Exception as e:
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
package main
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
func initTracer() (*sdktrace.TracerProvider, error) {
// Create OTLP exporter
exporter, err := otlptracegrpc.New(
context.Background(),
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
// Create resource (service metadata)
res, err := resource.New(
context.Background(),
resource.WithAttributes(
semconv.ServiceNameKey.String("api-service"),
semconv.ServiceVersionKey.String("1.0.0"),
semconv.DeploymentEnvironmentKey.String("production"),
),
)
if err != nil {
return nil, err
}
// Create tracer provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
return tp, nil
}
func main() {
tp, err := initTracer()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
}()
// Get tracer
tracer := otel.Tracer("api-service")
// Create root span
ctx, span := tracer.Start(context.Background(), "handle_request")
defer span.End()
span.SetAttributes(
attribute.String("http.method", "GET"),
attribute.String("http.route", "/api/users/:id"),
)
// Create child span
processUser(ctx, tracer, "user_123")
span.SetStatus(trace.StatusCode(200), "OK")
}
func processUser(ctx context.Context, tracer trace.Tracer, userID string) {
ctx, span := tracer.Start(ctx, "process_user")
defer span.End()
span.SetAttributes(attribute.String("user_id", userID))
// Simulate work
time.Sleep(50 * time.Millisecond)
// Nested span
fetchFromDB(ctx, tracer, userID)
}
func fetchFromDB(ctx context.Context, tracer trace.Tracer, userID string) {
ctx, span := tracer.Start(ctx, "database_query")
defer span.End()
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.statement", "SELECT * FROM users WHERE id = ?"),
)
// Simulate query
time.Sleep(10 * time.Millisecond)
}
from opentelemetry import trace
from opentelemetry.propagate import inject, extract
import requests
tracer = trace.get_tracer(__name__)
# Service A: Outgoing request
def call_service_b():
with tracer.start_as_current_span("call_service_b") as span:
headers = {}
# Inject trace context into HTTP headers
inject(headers)
# Headers now contain:
# traceparent: 00-<trace_id>-<span_id>-01
# tracestate: ...
response = requests.get(
"http://service-b/api/data",
headers=headers
)
return response.json()
# Service B: Incoming request
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/api/data")
async def get_data(request: Request):
# Extract trace context from incoming headers
ctx = extract(request.headers)
# Continue trace from Service A
with tracer.start_as_current_span("get_data", context=ctx) as span:
span.set_attribute("service", "service-b")
data = await fetch_data()
return {"data": data}
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
tracer = trace.get_tracer(__name__)
@app.post("/api/orders")
async def create_order(order: Order):
with tracer.start_as_current_span("create_order") as span:
# Standard semantic conventions
span.set_attribute("http.method", "POST")
span.set_attribute("http.route", "/api/orders")
span.set_attribute("http.status_code", 201)
# Custom business attributes
span.set_attribute("order.id", order.id)
span.set_attribute("order.total", order.total)
span.set_attribute("order.items_count", len(order.items))
span.set_attribute("customer.id", order.customer_id)
span.set_attribute("payment.method", order.payment_method)
# Add events (logs within span)
span.add_event("order_validated", {
"validation_duration_ms": 10
})
try:
result = await process_order(order)
span.add_event("payment_processed", {
"transaction_id": result.transaction_id
})
span.set_status(Status(StatusCode.OK))
return result
except PaymentError as e:
span.set_status(Status(StatusCode.ERROR, "Payment failed"))
span.record_exception(e)
# Add error attributes
span.set_attribute("error", True)
span.set_attribute("error.type", "PaymentError")
span.set_attribute("error.message", str(e))
raise
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import (
TraceIdRatioBased,
ParentBased,
ALWAYS_ON,
ALWAYS_OFF
)
# 1. Probabilistic sampling (1% of traces)
sampler = TraceIdRatioBased(0.01)
# 2. Parent-based sampling (respect parent's decision)
sampler = ParentBased(root=TraceIdRatioBased(0.1))
# 3. Always sample (development)
sampler = ALWAYS_ON
# 4. Never sample (disabled)
sampler = ALWAYS_OFF
# 5. Custom sampling (sample errors and slow requests)
class CustomSampler:
def should_sample(self, context, trace_id, name, attributes):
# Always sample if error
if attributes.get("http.status_code", 0) >= 500:
return ALWAYS_ON.should_sample(context, trace_id, name, attributes)
# Always sample if slow
if attributes.get("duration_ms", 0) > 1000:
return ALWAYS_ON.should_sample(context, trace_id, name, attributes)
# Otherwise 1% sampling
return TraceIdRatioBased(0.01).should_sample(
context, trace_id, name, attributes
)
provider = TracerProvider(sampler=CustomSampler())
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from sqlalchemy import create_engine
# Auto-instrument SQLAlchemy
engine = create_engine("postgresql://localhost/mydb")
SQLAlchemyInstrumentor().instrument(engine=engine)
# Manual span for complex queries
with tracer.start_as_current_span("complex_query") as span:
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.name", "mydb")
span.set_attribute("db.operation", "SELECT")
span.set_attribute("db.statement", "SELECT * FROM users WHERE status = ?")
results = session.execute(query)
span.set_attribute("db.rows_returned", len(results))
# Python
pip install opentelemetry-api opentelemetry-sdk
pip install opentelemetry-exporter-otlp
pip install opentelemetry-instrumentation-fastapi
pip install opentelemetry-instrumentation-requests
pip install opentelemetry-instrumentation-sqlalchemy
# Go
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
# Node.js
npm install @opentelemetry/api @opentelemetry/sdk-node
npm install @opentelemetry/exporter-trace-otlp-grpc
npm install @opentelemetry/auto-instrumentations-node
| Backend | Description | Use Case | |---------|-------------|----------| | Jaeger | CNCF project, full-featured | Self-hosted, open source | | Zipkin | Twitter origin, simple | Lightweight, easy setup | | Tempo | Grafana Labs, S3-compatible | Cost-effective, high volume | | Datadog APM | Commercial SaaS | Enterprise, full stack | | New Relic | Commercial SaaS | Enterprise, full stack |
# HTTP
span.set_attribute("http.method", "GET")
span.set_attribute("http.url", "https://api.example.com/users")
span.set_attribute("http.status_code", 200)
span.set_attribute("http.route", "/users/:id")
# Database
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.name", "mydb")
span.set_attribute("db.statement", "SELECT * FROM users")
span.set_attribute("db.operation", "SELECT")
# RPC
span.set_attribute("rpc.system", "grpc")
span.set_attribute("rpc.service", "UserService")
span.set_attribute("rpc.method", "GetUser")
# Messaging
span.set_attribute("messaging.system", "rabbitmq")
span.set_attribute("messaging.destination", "orders_queue")
span.set_attribute("messaging.operation", "publish")
# W3C Trace Context (standard)
traceparent: 00-<trace_id>-<span_id>-<flags>
tracestate: vendor1=value1,vendor2=value2
# Example
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# WRONG: Span for every function call
with tracer.start_as_current_span("add_numbers"): # Too granular!
result = a + b
# CORRECT: Span for meaningful operations
with tracer.start_as_current_span("calculate_order_total"):
total = sum(item.price for item in order.items)
total += calculate_tax(total)
total += calculate_shipping(order)
# WRONG: No trace context in HTTP call
requests.get("http://service-b/api/data") # Breaks trace!
# CORRECT: Inject trace context
headers = {}
inject(headers)
requests.get("http://service-b/api/data", headers=headers)
# WRONG: Span never closed
span = tracer.start_span("operation")
do_work()
# span.end() missing!
# CORRECT: Use context manager
with tracer.start_as_current_span("operation"):
do_work()
# WRONG: User ID in span name (high cardinality)
span = tracer.start_as_current_span(f"get_user_{user_id}")
# CORRECT: User ID as attribute
span = tracer.start_as_current_span("get_user")
span.set_attribute("user_id", user_id)
# WRONG: Sample before seeing attributes
sampler = TraceIdRatioBased(0.01) # Can't see if request is slow/error
# CORRECT: Use tail-based sampling or custom sampler
class TailBasedSampler:
def should_sample(self, context, trace_id, name, attributes):
if attributes.get("http.status_code", 0) >= 500:
return ALWAYS_ON
return TraceIdRatioBased(0.01).should_sample(...)
# WRONG: Synchronous sleep in async span
with tracer.start_as_current_span("wait"):
time.sleep(10) # Blocks event loop!
# CORRECT: Async sleep
with tracer.start_as_current_span("wait"):
await asyncio.sleep(10)
Location: skills/observability/distributed-tracing/resources/
REFERENCE.md (800+ lines): Deep-dive reference covering:
scripts/analyze_traces.py
scripts/visualize_trace.py
scripts/test_propagation.sh
examples/python/otel_fastapi_tracing.py
examples/python/otel_manual_spans.py
examples/typescript/otel_express_tracing.ts
examples/go/otel_http_tracing.go
examples/docker/jaeger-compose.yml
examples/docker/otel-collector-config.yaml
# Analyze traces for slow operations
./scripts/analyze_traces.py --file traces.json --slow-threshold 500
# Generate interactive trace visualization
./scripts/visualize_trace.py --file traces.json --interactive --output trace.html
# Test trace propagation
./scripts/test_propagation.sh --url http://localhost:8000 --format w3c
# Run example services
python examples/python/otel_fastapi_tracing.py
ts-node examples/typescript/otel_express_tracing.ts
go run examples/go/otel_http_tracing.go
# Start tracing infrastructure
docker-compose -f examples/docker/jaeger-compose.yml up -d
# Access Jaeger UI: http://localhost:16686
Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)