Distributed Tracing¶
Distributed tracing follows a request as it flows through multiple services. Cello creates spans automatically for each HTTP request and propagates trace context to downstream services.
How It Works¶
- A request arrives at the first service. Cello creates a root span.
- The span ID is attached to the response and propagated to any outgoing HTTP calls via the
traceparentheader. - Downstream services read the
traceparentheader and create child spans under the same trace. - All spans are exported to a collector and visualized in a tracing backend.
Client --> [API Gateway] --> [User Service] --> [Database]
span A span B span C
\___________________________________/
single trace
Enabling Tracing¶
from cello import App, OpenTelemetryConfig
app = App()
app.enable_telemetry(OpenTelemetryConfig(
service_name="api-gateway",
otlp_endpoint="http://collector:4317",
sampling_rate=0.1,
))
See the OpenTelemetry page for full configuration options.
Trace Context Propagation¶
Cello supports the W3C traceparent header format by default:
| Field | Description |
|---|---|
| Version | Always 00 |
| Trace ID | 32-character hex string identifying the trace |
| Parent Span ID | 16-character hex string for the parent span |
| Trace Flags | 01 = sampled |
When making outgoing HTTP calls from your handler, include the trace context:
import urllib.request
@app.get("/orders/{id}")
async def get_order(request):
# Forward trace context to downstream service
traceparent = request.get_header("traceparent")
req = urllib.request.Request(f"http://user-service:8001/users/{user_id}")
if traceparent:
req.add_header("traceparent", traceparent)
# ...
Span Attributes¶
Each automatically created span includes:
| Attribute | Description | Example |
|---|---|---|
http.method | HTTP method | GET |
http.route | Route pattern | /users/{id} |
http.url | Full URL path | /users/42 |
http.status_code | Response status | 200 |
http.request_content_length | Request body size | 256 |
http.response_content_length | Response body size | 1024 |
net.host.name | Server hostname | api-gateway |
service.name | Service name from config | api-gateway |
Viewing Traces in Jaeger¶
- Start Jaeger:
-
Configure the OTel collector to export to Jaeger.
-
Open the Jaeger UI at
http://localhost:16686. -
Select your service name from the dropdown and click "Find Traces".
Each trace shows a waterfall diagram of all spans, their durations, and parent-child relationships.
Viewing Traces in Zipkin¶
- Start Zipkin:
-
Configure the OTel collector with a Zipkin exporter.
-
Open the Zipkin UI at
http://localhost:9411.
Request ID Integration¶
When both tracing and request ID middleware are enabled, the request ID is included as a span attribute. This lets you correlate log entries with specific spans.
app.enable_request_id()
app.enable_telemetry(OpenTelemetryConfig(
service_name="my-service",
otlp_endpoint="http://collector:4317",
))
Performance Impact¶
Tracing is implemented in Rust with asynchronous span export. The overhead per request is typically under 50 microseconds. Use sampling_rate to reduce the volume of exported spans in high-traffic environments.
| Sampling Rate | Overhead | Trace Volume |
|---|---|---|
1.0 | Highest | All requests |
0.1 | Low | 10% of requests |
0.01 | Minimal | 1% of requests |
Next Steps¶
- See OpenTelemetry for collector setup and configuration.
- See Metrics for Prometheus-based dashboards.
- See the Microservices tutorial for a multi-service tracing example.