//! Tracing setup (§16 NFR: observability). Always installs a fmt //! subscriber honoring `RUST_LOG`; with an OTLP endpoint configured, //! spans also export over OTLP/HTTP — to a collector shipped in the //! air-gapped bundle or any cloud backend. No endpoint, no export, no //! network: telemetry never phones home uninvited. use opentelemetry::trace::TracerProvider as _; use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::trace::SdkTracerProvider; use opentelemetry_sdk::Resource; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer}; /// Keeps the exporter alive; dropping it flushes pending spans. pub struct TelemetryGuard { provider: Option, } impl TelemetryGuard { /// Forces all batched spans out — call before process exit (tests /// call it to assert delivery). pub fn flush(&self) { if let Some(provider) = &self.provider { let _ = provider.force_flush(); } } } impl Drop for TelemetryGuard { fn drop(&mut self) { if let Some(provider) = self.provider.take() { let _ = provider.force_flush(); let _ = provider.shutdown(); } } } #[derive(Debug, thiserror::Error)] pub enum TelemetryError { #[error("otlp exporter: {0}")] Exporter(String), #[error("subscriber already installed")] AlreadyInstalled, } /// Installs the global subscriber. `otlp_endpoint` is the collector's /// OTLP/HTTP base (e.g. `http://otel-collector:4318`). pub fn init( service_name: &str, otlp_endpoint: Option<&str>, ) -> Result { let fmt = tracing_subscriber::fmt::layer().with_target(false); let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,hyper=warn")); match otlp_endpoint { Some(endpoint) => { let exporter = opentelemetry_otlp::SpanExporter::builder() .with_http() .with_endpoint(format!("{}/v1/traces", endpoint.trim_end_matches('/'))) .build() .map_err(|e| TelemetryError::Exporter(e.to_string()))?; let provider = SdkTracerProvider::builder() .with_batch_exporter(exporter) .with_resource( Resource::builder() .with_attributes([KeyValue::new("service.name", service_name.to_owned())]) .build(), ) .build(); let tracer = provider.tracer(service_name.to_owned()); let otel = tracing_opentelemetry::layer().with_tracer(tracer); tracing_subscriber::registry() .with(fmt.with_filter(filter)) .with(otel) .try_init() .map_err(|_| TelemetryError::AlreadyInstalled)?; Ok(TelemetryGuard { provider: Some(provider), }) } None => { tracing_subscriber::registry() .with(fmt.with_filter(filter)) .try_init() .map_err(|_| TelemetryError::AlreadyInstalled)?; Ok(TelemetryGuard { provider: None }) } } }