This service implements distributed tracing using OpenTelemetry to track requests across service boundaries.
- Trace context propagation across HTTP calls
- Configurable sampling rates
- Support for multiple backends (Jaeger, Zipkin)
- Automatic span creation at service boundaries
- Request correlation with trace IDs
OTEL_EXPORTER_OTLP_ENDPOINT is validated as a parseable URL at startup.
An invalid value (e.g. not-a-url) causes the service to exit immediately
with a clear error rather than failing silently during the first export.
After the tracing subscriber is initialised, a TCP connectivity check (2-second timeout) is attempted against the configured endpoint. If the endpoint is unreachable the service continues to start but:
- Logs a
WARNmessage referencing the endpoint and error. - Increments
otel_export_errors_total{reason="unreachable"}.
Monitor that counter to detect collector outages before they cause silent data loss in production.
| Metric | Labels | Description |
|---|---|---|
otel_export_errors_total |
reason |
Export failures — unreachable (startup TCP check), export_failed (runtime) |
Configure tracing via environment variables:
# OTLP endpoint (leave unset to disable trace export)
# Must be a valid URL — invalid values fail the service at startup.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# ── Sampling (OTel standard env vars — preferred) ────────────────────────────
# OTEL_TRACES_SAMPLER selects the sampler strategy.
# OTEL_TRACES_SAMPLER_ARG sets the ratio for ratio-based samplers (0.0–1.0).
# These take precedence over TRACE_SAMPLE_RATE when set.
#
# Default for production: traceidratio at 10% (0.1)
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
# Supported OTEL_TRACES_SAMPLER values:
# always_on — 100% sampling
# always_off — 0% sampling
# traceidratio — ratio set by OTEL_TRACES_SAMPLER_ARG
# parentbased_always_on — 100% sampling
# parentbased_always_off — 0% sampling
# parentbased_traceidratio — ratio set by OTEL_TRACES_SAMPLER_ARG
# ── Legacy fallback ──────────────────────────────────────────────────────────
# TRACE_SAMPLE_RATE is used when OTEL_TRACES_SAMPLER is not set.
# Default: 0.1 (10%)
TRACE_SAMPLE_RATE=0.1
# Environment name for trace metadata
ENVIRONMENT=development- Start Jaeger using Docker Compose:
docker-compose -f docker-compose.tracing.yml up -d jaeger- Configure the API to export traces:
export OTLP_ENDPOINT=http://localhost:4317
export TRACE_SAMPLE_RATE=1.0- Start the API:
cargo run- Open Jaeger UI:
http://localhost:16686
- Make some API requests and view traces in Jaeger
The OpenTelemetry Collector can export to multiple backends simultaneously:
- Start the full tracing stack:
docker-compose -f docker-compose.tracing.yml up -dThis starts:
- Jaeger (UI at http://localhost:16686)
- Zipkin (UI at http://localhost:9411)
- OpenTelemetry Collector (receives traces and exports to both)
- Configure the API:
export OTLP_ENDPOINT=http://localhost:4317
export TRACE_SAMPLE_RATE=1.0Traces automatically propagate across service boundaries using W3C Trace Context headers:
traceparent: Contains trace ID, span ID, and sampling decisiontracestate: Vendor-specific trace information
When making HTTP requests to other services, the trace context is automatically injected into request headers.
OTEL_TRACES_SAMPLER=always_onTraces 100% of requests. Use for development and debugging.
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1Traces 10% of requests. This is the default when no sampler is configured. Reduces overhead while maintaining visibility.
OTEL_TRACES_SAMPLER=always_off
# or unset OTLP_ENDPOINTDisables trace export. Tracing instrumentation remains but spans are not exported.
- Open http://localhost:16686
- Select "predictiq-api" from the Service dropdown
- Click "Find Traces"
- Click on a trace to view the full request flow
- Open http://localhost:9411
- Click "Run Query" to see recent traces
- Click on a trace to view details
Each span includes:
service.name: "predictiq-api"service.version: API version from Cargo.tomldeployment.environment: From ENVIRONMENT env var- HTTP method, path, status code
- Request duration
- Error information (if applicable)
To propagate traces to downstream services:
use crate::tracing_config::{extract_trace_context, inject_trace_context};
// Extract context from incoming request
let context = extract_trace_context(&headers);
// Inject context into outgoing request
let mut headers = reqwest::header::HeaderMap::new();
inject_trace_context(&mut headers, &context);
let response = client
.get("http://downstream-service/api")
.headers(headers)
.send()
.await?;In production, use a low sampling rate (0.01–0.1) to reduce overhead. The default is 10 % when no sampler env vars are set.
# 10% (default)
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
# 5%
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.05Use a managed tracing backend:
- Jaeger (self-hosted or managed)
- Zipkin
- AWS X-Ray
- Google Cloud Trace
- Datadog APM
- New Relic
Tracing adds minimal overhead:
- ~1-2ms per traced request
- Batched export reduces network calls
- Memory-limited to prevent OOM
- Traces may contain sensitive data (URLs, headers)
- Configure backend access controls
- Consider PII scrubbing in production
- Use TLS for OTLP export in production
-
Check
otel_export_errors_totalin/metrics— any non-zero value means the startup connectivity check failed. -
Check OTLP endpoint is reachable:
curl http://localhost:4317- Check API logs for tracing initialization:
Distributed tracing initialized service_name="predictiq-api"
- Verify sampling rate is > 0:
echo $OTEL_TRACES_SAMPLER_ARG # OTel standard
echo $TRACE_SAMPLE_RATE # legacy fallbackReduce batch size or increase export frequency in otel-collector-config.yml:
processors:
batch:
timeout: 5s # Export more frequently
send_batch_size: 512 # Smaller batchesEnsure trace context is propagated:
- Check
traceparentheader is present in requests - Verify downstream services support W3C Trace Context