Tracing and OpenTelemetry Patterns
Use this recipe for structured logs, distributed tracing, OTLP export, and shutdown/flush behavior.
Structured Logging Baseline
use tracing::{error, info, warn};
use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init_logging() -> Result<()> {
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer().json())
.try_init()
.map_err(|error| ServiceError::invalid_input(format!("logging already initialized: {error}")))?;
Ok(())
}
info!(service.name = "service-name", "service starting");
warn!(queue_depth = 128, "queue depth is high");
error!(error.code = "PUBLISH_FAILED", ?error, "failed to publish message");Rules:
- Prefer structured fields over interpolated strings.
- Use
%valuefor display-safe values and?valuefor debug-safe values. - Do not log secrets, tokens, connection strings, or full Authorization headers.
- Use stable field names for operational dashboards.
OpenTelemetry Dependencies
Verify current versions before generating. Current known pattern (verified 2026-08-26):
[dependencies]
opentelemetry = "0.32"
opentelemetry_sdk = "0.32"
opentelemetry-otlp = { version = "0.32", default-features = false, features = ["trace", "grpc-tonic"] }
opentelemetry-semantic-conventions = "0.32"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
tracing-opentelemetry = "0.33"OTLP Tracing Initialization
Keep the provider so it can be shut down during graceful exit:
use opentelemetry::{global, trace::TracerProvider as _};
use opentelemetry_otlp::SpanExporter;
use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::SdkTracerProvider};
use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
pub struct TelemetryGuard {
tracer_provider: Option<SdkTracerProvider>,
}
impl TelemetryGuard {
pub fn shutdown(self) -> Result<()> {
if let Some(provider) = self.tracer_provider {
provider
.shutdown()
.map_err(|error| ServiceError::invalid_input(format!("telemetry shutdown failed: {error}")))?;
}
Ok(())
}
}
pub fn init_tracing(service_name: &'static str) -> Result<TelemetryGuard> {
let env_filter = EnvFilter::from_default_env();
let fmt_layer = tracing_subscriber::fmt::layer().json();
global::set_text_map_propagator(TraceContextPropagator::new());
if std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok() {
let exporter = SpanExporter::builder()
.with_tonic()
.build()
.map_err(|error| ServiceError::invalid_input(format!("OTLP exporter failed: {error}")))?;
let tracer_provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
let tracer = tracer_provider.tracer(service_name);
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(otel_layer)
.try_init()
.map_err(|error| ServiceError::invalid_input(format!("tracing already initialized: {error}")))?;
global::set_tracer_provider(tracer_provider.clone());
Ok(TelemetryGuard {
tracer_provider: Some(tracer_provider),
})
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.try_init()
.map_err(|error| ServiceError::invalid_input(format!("tracing already initialized: {error}")))?;
Ok(TelemetryGuard {
tracer_provider: None,
})
}
}Shutdown Usage
#[tokio::main]
async fn main() -> Result<()> {
let telemetry = init_tracing("service-name")?;
let result = run().await;
telemetry.shutdown()?;
result
}Rules:
- Check for
OTEL_EXPORTER_OTLP_ENDPOINTbefore enabling export. - Fall back to structured logs when OTLP is not configured.
- Set W3C trace context propagation.
- Retain and explicitly shut down the provider; do not rely on removed global shutdown APIs.
- Verify exact builder names against crate docs before generating final code.