Modern, composable OpenTelemetry observability helpers for Rust.
Provides a single init() call that sets up traces, logs, and metrics export via OTLP, with dynamic subscriber layer composition, feature-gated transport selection, standard OTel env var support, and async-aware shutdown.
use otel_rs::OtelConfig;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut guard = OtelConfig::builder()
.service_name("my-service")
.service_version("1.0.0")
.allow_crate("my_service")
.init()
.await?;
tracing::info!("Hello from my-service!");
guard.shutdown().await?;
Ok(())
}Add to your Cargo.toml:
[dependencies]
otel-rs = "0.1"| Feature | Default | Description |
|---|---|---|
tracing |
✅ | Distributed trace export |
logs |
✅ | Log export via OTLP |
metrics |
✅ | Metrics export via OTLP |
grpc |
✅ | gRPC transport (tonic) |
http |
❌ | HTTP/protobuf transport (reqwest) |
To use HTTP instead of gRPC:
[dependencies]
otel-rs = { version = "0.1", default-features = false, features = ["tracing", "logs", "metrics", "http"] }Configuration is split into composable sub-configs, each with its own builder:
use otel_rs::{
OtelConfig, ExporterConfig, TracingConfig, MetricsConfig,
OtlpProtocol, SamplingStrategy, LogLevel, OutputFormat,
};
use std::time::Duration;
let mut guard = OtelConfig::builder()
// Service identity
.service_name("my-service")
.service_version("1.0.0")
.environment("production")
.namespace("backend")
// Exporter (endpoint, auth, protocol, timeout)
.exporter(ExporterConfig::builder()
.endpoint("https://otel.example.com:4317")
.bearer_token("your-api-key")
.timeout(Duration::from_secs(30))
.build())
// Tracing (sampling, batching)
.tracing(TracingConfig::builder()
.sampling(SamplingStrategy::TraceIdRatio(0.1))
.batch_schedule_delay(Duration::from_secs(5))
.build())
// Metrics (export interval)
.metrics(MetricsConfig::builder()
.export_interval(Duration::from_secs(30))
.build())
// Console & filtering
.log_level(LogLevel::Info)
.output_format(OutputFormat::Json)
.allow_crate("my_service")
.allow_crate("my_lib")
// Custom resource attributes
.attribute("deployment.region", "us-east-1")
.init()
.await?;Standard OTel environment variables are respected as defaults. Builder values always take precedence.
| Variable | Effect |
|---|---|
OTEL_SERVICE_NAME |
Service name |
OTEL_EXPORTER_OTLP_ENDPOINT |
Collector endpoint |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc or http/protobuf |
OTEL_EXPORTER_OTLP_HEADERS |
Comma-separated key=value pairs |
OTEL_EXPORTER_OTLP_TIMEOUT |
Timeout in milliseconds |
OTEL_TRACES_SAMPLER |
always_on, always_off, traceidratio, parentbased_always_on |
OTEL_TRACES_SAMPLER_ARG |
Sampler argument (e.g., ratio 0.1) |
RUST_LOG |
Full override of log filtering |
// Bearer token (Honeycomb, Grafana Cloud, etc.)
ExporterConfig::builder()
.bearer_token("your-api-key")
.build()
// Basic auth
ExporterConfig::builder()
.basic_auth("user", "password")
.build()
// Custom headers
ExporterConfig::builder()
.header("x-api-key", "abc")
.header("x-team-id", "eng")
.build()OtelConfig::builder()
.service_name("lightweight")
.disable_tracing() // No span export
.disable_metrics() // No metrics export
.logging(false) // No log export
.console_output(false) // No stdout
.init()
.await?;Errors are automatically recorded to spans using extension traits and macros:
use otel_rs::{SpanExt, InstrumentedResult};
use tracing::Span;
#[tracing::instrument]
async fn work() -> Result<String, MyError> {
// Option 1: Extension trait — records error and passes through
let data = fetch_data().await.record_to_span()?;
// Option 2: Macro — records error, early-returns on Err
let parsed = otel_rs::try_record_return!(parse(data));
// Option 3: Explicit recording
let result = validate(&parsed);
Span::current().record_result(&result);
Ok(parsed)
}When metrics are enabled, the guard provides a Metrics handle:
use opentelemetry::KeyValue;
let guard = OtelConfig::builder()
.service_name("my-service")
.init()
.await?;
if let Some(metrics) = guard.metrics() {
let counter = metrics.counter("requests_total");
counter.add(1, &[KeyValue::new("method", "GET")]);
let histogram = metrics.histogram("request_duration_ms");
histogram.record(42.5, &[]);
let gauge = metrics.gauge("active_connections");
gauge.record(10, &[]);
}OtelGuard supports both async and sync shutdown:
// Preferred: async shutdown with error reporting
guard.shutdown().await?;
// Fallback: Drop triggers sync best-effort shutdown
drop(guard);Always prefer the async path — it flushes pending telemetry and reports errors. The Drop impl is a safety net for cases where async shutdown isn't called.
Uses an allow-list approach (default: all crates silenced):
OtelConfig::builder()
.log_level(LogLevel::Debug)
.allow_crate("my_app") // my_app=debug
.allow_crate("my_lib") // my_lib=debug
.custom_filter("hyper=warn") // Override for specific crate
.init()
.await?;For advanced filtering, use FilterBuilder directly:
use otel_rs::FilterBuilder;
let filter = FilterBuilder::new()
.default_level("info")
.allow("my_app")
.allow_at("my_lib", "trace")
.directive("tokio=warn")
.build();RUST_LOG takes full precedence when set.
Licensed under either of
at your option.