//! OpenTelemetry initialization for traces and logs. //! //! Exports traces and logs via OTLP (gRPC) when `OTEL_EXPORTER_OTLP_ENDPOINT` //! is set. Always includes a `tracing_subscriber::fmt` layer for console output. //! //! Compatible with SigNoz, Grafana Tempo/Loki, Jaeger, and any OTLP-compatible //! collector. //! //! # Environment Variables //! //! | Variable | Description | Default | //! |---|---|---| //! | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (e.g. `http://localhost:4317`) | *(disabled)* | //! | `OTEL_SERVICE_NAME` | Service name for resource | `service_name` param | //! | `RUST_LOG` / standard `EnvFilter` | Log level filter | `info` | use opentelemetry::global; use opentelemetry::trace::TracerProvider as _; use opentelemetry::KeyValue; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::{LogExporter, SpanExporter, WithExportConfig}; use opentelemetry_sdk::{logs::SdkLoggerProvider, trace::SdkTracerProvider, Resource}; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer as _}; /// Guard that shuts down OTel providers on drop. /// /// Must be held for the lifetime of the application. When dropped, /// flushes and shuts down the tracer and logger providers. pub struct TelemetryGuard { tracer_provider: Option, logger_provider: Option, } impl Drop for TelemetryGuard { fn drop(&mut self) { if let Some(tp) = self.tracer_provider.take() { if let Err(e) = tp.shutdown() { eprintln!("Failed to shutdown tracer provider: {e}"); } } if let Some(lp) = self.logger_provider.take() { if let Err(e) = lp.shutdown() { eprintln!("Failed to shutdown logger provider: {e}"); } } } } fn build_resource(service_name: &str) -> Resource { let name = std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| service_name.to_string()); Resource::builder() .with_service_name(name) .with_attributes([KeyValue::new("service.version", env!("CARGO_PKG_VERSION"))]) .build() } /// Initialize telemetry (tracing + logging). /// /// If `OTEL_EXPORTER_OTLP_ENDPOINT` is set, traces and logs are exported /// via OTLP/gRPC. Console fmt output is always enabled. /// /// Returns a [`TelemetryGuard`] that must be held alive for the application /// lifetime. Dropping it triggers a graceful shutdown of OTel providers. /// /// # Panics /// /// Panics if the tracing subscriber cannot be initialized (e.g. called twice). pub fn init_telemetry(service_name: &str) -> TelemetryGuard { let otel_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let fmt_layer = tracing_subscriber::fmt::layer(); match otel_endpoint { Some(ref endpoint) => { let resource = build_resource(service_name); // Traces #[allow(clippy::expect_used)] let span_exporter = SpanExporter::builder() .with_tonic() .with_endpoint(endpoint) .build() .expect("failed to create OTLP span exporter"); let tracer_provider = SdkTracerProvider::builder() .with_batch_exporter(span_exporter) .with_resource(resource.clone()) .build(); global::set_tracer_provider(tracer_provider.clone()); let tracer = tracer_provider.tracer(service_name.to_string()); let otel_trace_layer = OpenTelemetryLayer::new(tracer); // Logs #[allow(clippy::expect_used)] let log_exporter = LogExporter::builder() .with_tonic() .with_endpoint(endpoint) .build() .expect("failed to create OTLP log exporter"); let logger_provider = SdkLoggerProvider::builder() .with_batch_exporter(log_exporter) .with_resource(resource) .build(); let otel_log_layer = OpenTelemetryTracingBridge::new(&logger_provider); // Filter to prevent telemetry-induced-telemetry loops let otel_filter = EnvFilter::new("info") .add_directive("hyper=off".parse().unwrap_or_default()) .add_directive("tonic=off".parse().unwrap_or_default()) .add_directive("h2=off".parse().unwrap_or_default()) .add_directive("reqwest=off".parse().unwrap_or_default()); tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .with(otel_trace_layer) .with(otel_log_layer.with_filter(otel_filter)) .init(); tracing::info!( endpoint = endpoint.as_str(), service = service_name, "OpenTelemetry OTLP export enabled" ); TelemetryGuard { tracer_provider: Some(tracer_provider), logger_provider: Some(logger_provider), } } None => { tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .init(); tracing::info!("OpenTelemetry disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)"); TelemetryGuard { tracer_provider: None, logger_provider: None, } } } }