# Distributed Tracing Setup
This guide walks you through setting up **OpenTelemetry distributed tracing** with **Jaeger** or **Uptrace** for the
Cyber Ware framework for testing purposes.
## Overview
The Cyber Ware framework includes first-class support for distributed tracing with:
- **Automatic trace context extraction** from incoming HTTP requests (W3C Trace Context)
- **Automatic trace context injection** for outgoing HTTP requests
- **Centralized configuration** via YAML
- **HttpClient with OTEL layer** for instrumented HTTP calls
- **Integration with existing logging**
## Quick Start with Jaeger
### 1. Start Jaeger (Local Development)
```bash
# Start Jaeger All-in-One with OTLP support
docker run -d --name jaeger \
-p 16686:16686 \ # UI: http://localhost:16686
-p 4317:4317 \ # OTLP gRPC
-p 4318:4318 \ # OTLP HTTP
-e COLLECTOR_OTLP_ENABLED=true \
jaegertracing/all-in-one:latest
Create a configuration file (e.g., config/with-tracing.yaml):
server:
home_dir: "~/.cyberware"
host: "127.0.0.1"
port: 8087
# Enable OpenTelemetry tracing
tracing:
enabled: true
service_name: "cyberware-api"
exporter:
kind: "otlp_grpc"
endpoint: "http://127.0.0.1:4317"
timeout_ms: 5000
sampler:
parent_based_ratio:
ratio: 0.1 # Sample 10% of traces
propagation:
w3c_trace_context: true
resource:
service.version: "1.0.0"
deployment.environment: "dev"
logging:
default:
console_level: "info"
file: "logs/cyberware.log"
cargo run --bin cyberware-server -- --config config/with-tracing.yaml
Open http://localhost:16686 and search for service cyberware-api.
Uptrace is a modern tracing UI that works with OpenTelemetry and ClickHouse/Postgres.
services:
uptrace:
image: uptrace/uptrace:2.0.1
ports:
- "14318:80" # Web UI: http://localhost:14318
- "14317:4317" # OTLP gRPC
- "14319:4318" # OTLP HTTP
volumes:
- ./uptrace.yml:/etc/uptrace/config.yml
depends_on:
- clickhouse
- postgres
- redis
clickhouse:
image: clickhouse/clickhouse-server:25.8
ports: [ "9000:9000" ]
postgres:
image: postgres:16
environment:
POSTGRES_DB: uptrace
POSTGRES_USER: uptrace
POSTGRES_PASSWORD: uptrace
redis:
image: redis:8.2
tracing:
enabled: true
service_name: "cyberware-api"
exporter:
kind: "otlp_grpc"
endpoint: "http://127.0.0.1:14317"
timeout_ms: 5000
headers:
uptrace-dsn: "http://project1_secret@localhost:14318?grpc=14317"
sampler:
always_on: { }
resource:
service.version: "1.3.7"
deployment.environment: "dev"
service.namespace: "cyberware"
cargo run --bin cyberware-server -- --config config/with-tracing.yaml
Open http://localhost:14318 and search for service cyberware-api.
tracing:
enabled: true # Enable/disable tracing
service_name: "my-service" # Service name in traces
tracing:
exporter:
kind: "otlp_grpc"
endpoint: "http://127.0.0.1:4317"
timeout_ms: 5000
headers: # Optional auth headers
authorization: "Bearer token"
tracing:
exporter:
kind: "otlp_http"
endpoint: "http://127.0.0.1:4318/v1/traces"
timeout_ms: 5000
(rest of your original doc unchanged below)
tracing:
sampler:
always_on: { }
tracing:
sampler:
always_off: { }
tracing:
sampler:
parent_based_ratio:
ratio: 0.1 # Sample 10% of traces
Add metadata to all spans:
tracing:
resource:
service.version: "1.2.3"
deployment.environment: "production"
service.namespace: "cyberware"
k8s.cluster.name: "prod-cluster"
k8s.namespace.name: "cyberware-ns"
tracing:
http:
inject_request_id_header: "x-request-id"
record_headers:
- "user-agent"
- "x-forwarded-for"
- "authorization" # Be careful with sensitive headers
Important: Feature Gate Required
The
.with_otel()method onHttpClientBuilderrequires theotelfeature to be enabled. Add this to yourCargo.toml:[dependencies] modkit-http = { workspace = true, features = ["otel"] }Without this feature, the
with_otel()method will not be available, and you'll get a compile error.
use modkit_http::{HttpClient, HttpClientBuilder};
#[async_trait]
impl MyModule {
async fn call_external_api(&self) -> Result<String> {
// Build client with OTEL tracing enabled
let client = HttpClientBuilder::new()
.with_otel() // Enable OpenTelemetry tracing
.build()?;
// Trace context is automatically injected via W3C traceparent header
// RequestBuilder API: chain methods then send
let data = client
.get("https://api.example.com/data")
.send()
.await?
.checked_bytes()
.await?;
Ok(String::from_utf8_lossy(&data).into_owned())
}
}
use modkit_http::{HttpClient, HttpClientBuilder};
use std::time::Duration;
// Full configuration example
let client = HttpClientBuilder::new()
.with_otel() // Enable OpenTelemetry tracing
.timeout(Duration::from_secs(30)) // Request timeout
.user_agent("my-service/1.0") // Custom User-Agent
.max_body_size(10 * 1024 * 1024) // 10MB body limit
.build()?;
Create custom spans for business logic:
use modkit_http::HttpClientBuilder;
use tracing::{info_span, Instrument, info};
async fn process_user_data(user_id: u64) -> Result<()> {
// Create a span for this operation
let span = info_span!("process_user", user.id = user_id);
async {
// Your business logic here
info!("Processing user {}", user_id);
// Child operations will be traced automatically with OTEL-enabled client
let client = HttpClientBuilder::new()
.with_otel()
.build()?;
let url = format!("https://api.example.com/users/{}", user_id);
let user_data = client.get(&url).send().await?.checked_bytes().await?;
Ok(())
}.instrument(span).await
}
services:
jaeger:
image: ${REGISTRY:-}jaegertracing/jaeger:${JAEGER_VERSION:-latest}
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"
environment:
- LOG_LEVEL=debug
- COLLECTOR_OTLP_ENABLED=true
networks:
- jaeger-example
networks:
jaeger-example:
You can override any config via environment variables:
# Enable tracing
export APP__TRACING__ENABLED=true
export APP__TRACING__SERVICE_NAME=cyberware-prod
# Configure exporter
export APP__TRACING__EXPORTER__KIND=otlp_grpc
export APP__TRACING__EXPORTER__ENDPOINT=http://jaeger:4317
# Configure sampling
export APP__TRACING__SAMPLER__STRATEGY=parentbased_ratio
export APP__TRACING__SAMPLER__RATIO=0.01 # 1% sampling in prod
- Check Jaeger is running: Visit http://localhost:16686
- Verify endpoint: Ensure
exporter.endpointmatches Jaeger's OTLP port - Check sampling: Set
sampler.strategy: {always_on: {}}for testing - View logs: Look for "OpenTelemetry tracing initialized" message
- Use sampling in production: Set appropriate
ratio(0.01 = 1%) - Monitor resource usage: Tracing adds some CPU/memory overhead
- Batch export: The framework uses batched export by default
- Check headers: Ensure upstream sends
traceparentheader - Verify propagation: Set
propagation.w3c_trace_context: true - Use HttpClient with OTEL: Ensure outgoing calls use
HttpClientBuilder::new().with_otel()
Use consistent attribute names:
tracing::info_span!(
"user_operation",
user.id = user_id,
user.email = %user_email,
operation.type = "create",
operation.result = "success"
)
Mark spans with errors:
let span = tracing::info_span!("risky_operation");
let _guard = span.enter();
match risky_operation().await {
Ok(result) => {
span.record("operation.result", "success");
Ok(result)
}
Err(e) => {
span.record("error", true);
span.record("error.message", % e);
span.record("operation.result", "error");
Err(e)
}
}