otel-bootstrap
One-call OpenTelemetry bootstrap for Rust services — traces, metrics, and logs over OTLP with sensible defaults.
The standard opentelemetry + opentelemetry-otlp + tracing-subscriber wiring is the same in every service. This crate does it once: call init_telemetry("my-service"), keep the handle alive, and drop it to flush.
Features
- One call to wire it all up — traces, metrics, and logs over OTLP,
tracing-subscriberconfigured, W3C TraceContext + Baggage propagators registered. - gRPC or HTTP/protobuf — select transport via cargo feature.
- Env-var configuration — follows the OpenTelemetry spec (
OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAME,OTEL_TRACES_SAMPLER, …). - Builder API — version, environment, sampler, custom meter setup, extra subscriber layers.
- Optional axum middleware —
OtelTraceLayerfor inbound HTTP trace propagation. - Generic
enduser.*span enrichment — implementEnrichSpanon your context type and plug it intoSpanEnricherLayer<T>with no brefwiz dependencies required. - Graceful shutdown — drop
TelemetryHandlesto flush and shut down both providers.
Quick start
[]
= "2"
= { = "1", = ["full"] }
use init_telemetry;
async
For more control, use the builder:
use ;
let _telemetry = builder
.with_version
.with_environment
.with_log_filter
.with_log_format
.init?;
with_log_filter and with_log_format take precedence over environment-backed
logging defaults, so typed configuration sources can configure logging without
mutating process-global environment.
Cargo features
| Feature | Default | Description |
|---|---|---|
grpc |
✅ | OTLP/gRPC transport via tonic |
http |
❌ | OTLP/HTTP-protobuf transport via reqwest |
axum |
❌ | OtelTraceLayer + SpanEnricherLayer<T> for axum servers |
tonic-tracing |
❌ | grpc_client_layer() + grpc_server_layer() for raw tonic gRPC clients/servers |
testing |
❌ | In-memory exporters for unit tests |
At least one of grpc or http must be enabled (enforced at compile time).
Configuration
| Variable | Default |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 (gRPC) / http://localhost:4318 (HTTP) |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
OTEL_EXPORTER_OTLP_TIMEOUT |
10000 ms |
OTEL_SERVICE_NAME |
overridden by the service_name argument |
OTEL_TRACES_SAMPLER |
parentbased_always_on |
OTEL_TRACES_SAMPLER_ARG |
sampler-specific (e.g. ratio for traceidratio) |
Full reference: OTEL_TRACES_SAMPLER values.
Axum middleware
= { = "2", = ["axum"] }
use axum_layer;
let app = new
.route
.layer;
This layer reads the traceparent / tracestate headers, starts a server span, and propagates context to all child spans.
enduser.* span enrichment
= { = "2", = ["axum"] }
Implement EnrichSpan on your context type, then plug it into SpanEnricherLayer:
use ;
use Span;
let app = new
.route
.layer;
Routes with no MyContext extension are silently skipped.
Span-field log propagation
When with_logs(true) is set, otel-bootstrap replaces the upstream log bridge with SpanAwareLogBridge. It propagates span-level attributes into every OTLP log record so that request.id, enduser.*, and http.* fields appear as Loki labels alongside the trace context.
Two propagation paths are supported:
- Tracing fields declared at span creation (
info_span!("req", "request.id" = id)) — captured automatically for names in [PROPAGATED_SPAN_FIELDS]. - Post-creation attributes set via
record_span_log_attr_on— use this from middleware orEnrichSpan::enrich_spanfor fields that arrive after span creation.
use ;
use ;
// In your request handler / middleware:
emit_request_id; // → appears in traces + logs
// In EnrichSpan::enrich_span for enduser fields:
span.set_attribute;
record_span_log_attr_on;
Override the captured field set per service:
const FIELDS: & = &;
let _telemetry = builder
.with_logs
.with_propagated_span_fields
.init?;
Continuous profiling
Profiling is the fourth OTLP signal alongside traces, metrics, and logs (ADR platform/0201). It ships as an explicit, off-by-default feature:
| Feature | Default | Description |
|---|---|---|
profiling |
❌ | Enables Telemetry::builder(...).with_profiling(endpoint) |
profiling-bridge-pyroscope-rs |
❌ | Implies profiling; wires the pyroscope-rs exporter |
let _telemetry = builder
.with_profiling
.init?;
Single export plane. There is no direct-to-backend pyroscope client
path. pyroscope-rs hardcodes its own HTTP client with no TLS/identity
hook, so profiles are pushed over plain HTTP to a localhost-only
SPIFFE-terminating sidecar that carries the workload identity upstream
(ADR platform/0203). The with_profiling endpoint is validated at init
time to be loopback-only (127.0.0.1, ::1, localhost, or a unix
socket) — anything else is rejected. This bridge is a temporary,
sunset-bound exception (ADR platform/0202, Tracks #40) pending a native Rust OTLP
profiles exporter; every profile carries trace_id/span_id tags for
correlation with the trace pillar.
SVID rotation requires a pod restart. Like the rest of the telemetry
channel, mTLS material for the profiling sidecar path is read once at
init() time and held for the process lifetime. SPIFFE SVID rotation is
not hot-reloaded into an already-running telemetry channel — a rotated
SVID only takes effect on the next process start (pod restart). This is a
standing platform constraint, not a profiling-specific bug.
Examples
basic_setup— minimal initshutdown_handling— explicit graceful flushcustom_config— builder API with version, environment, sampleraxum_span_enricher— axum + genericEnrichSpanenrichmenttelemetry_profiling— continuous profiling via the builder
License
MIT — see LICENSE.