Skip to content

OpenTelemetry Integration

Cello provides native OpenTelemetry support for distributed tracing, metrics, and context propagation across microservices.


Overview

OpenTelemetry (OTel) is the industry standard for observability. Cello's integration is implemented in Rust for minimal overhead and supports:

  • Distributed tracing with automatic span creation for each request
  • OTLP export to any compatible collector (Jaeger, Zipkin, Grafana Tempo, Datadog)
  • Trace context propagation via W3C traceparent headers
  • Configurable sampling to control trace volume

Configuration

from cello import App, OpenTelemetryConfig

app = App()

app.enable_telemetry(OpenTelemetryConfig(
    service_name="my-service",
    otlp_endpoint="http://collector:4317",
    sampling_rate=0.1,
))

OpenTelemetryConfig

Field Type Default Description
service_name str Required Name identifying this service in traces
otlp_endpoint str "http://localhost:4317" OTLP gRPC endpoint for the collector
sampling_rate float 1.0 Fraction of requests to trace (0.0 to 1.0)
propagation str "w3c" Context propagation format ("w3c", "b3", "jaeger")
export_timeout_ms int 10000 Export timeout in milliseconds
batch_size int 512 Maximum batch size for span export

Automatic Instrumentation

Once enabled, every HTTP request automatically creates a span with the following attributes:

Attribute Example
http.method GET
http.url /api/users/42
http.status_code 200
http.route /api/users/{id}
http.request.duration_ms 12.5
service.name my-service

Trace Context Propagation

Cello automatically reads and writes the W3C traceparent header. When service A calls service B, the trace context is propagated so both requests appear in the same trace.

Service A                          Service B
  |                                  |
  |-- POST /orders ----------------->|
  |   traceparent: 00-abc...         |
  |                                  |-- GET /users/1 ---------> Service C
  |                                  |   traceparent: 00-abc...
  |<----- 201 Created --------------|

Sampling

Control trace volume with the sampling_rate parameter:

Rate Behavior
1.0 Trace every request (development)
0.1 Trace 10% of requests (staging)
0.01 Trace 1% of requests (high-traffic production)
# Production: trace 5% of requests
app.enable_telemetry(OpenTelemetryConfig(
    service_name="api-gateway",
    otlp_endpoint="http://collector:4317",
    sampling_rate=0.05,
))

Collector Setup

Docker Compose with Jaeger

services:
  collector:
    image: otel/opentelemetry-collector:latest
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    volumes:
      - ./otel-config.yaml:/etc/otel/config.yaml

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"  # Jaeger UI

otel-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [jaeger]

Custom Spans

Add custom spans in your handler code for fine-grained tracing:

@app.get("/orders/{id}")
async def get_order(request):
    # The framework creates the parent span automatically
    order = await db.fetch_order(request.params["id"])
    items = await db.fetch_order_items(order["id"])
    return {"order": order, "items": items}

Each database call creates a child span if your database driver supports OpenTelemetry.


Combining with Prometheus

OpenTelemetry and Prometheus metrics can run simultaneously. Use OTel for distributed tracing and Prometheus for metrics dashboards.

app.enable_telemetry(OpenTelemetryConfig(
    service_name="my-service",
    otlp_endpoint="http://collector:4317",
))
app.enable_prometheus(endpoint="/metrics")

Next Steps