//! OTLP tracer initialisation (Phase 6). //! //! Uses HTTP/proto transport to the OTLP endpoint. //! The endpoint URL is passed as-is to the HTTP exporter. //! Default Kubernetes otel-collector-agent accepts HTTP on :4318. //! //! Call `init_otlp()` **after** `init_tracing_subscriber()` so the fmt layer is //! already registered. This function rebuilds the global subscriber with the //! OTLP tracing layer on top. use crate::msg_json_fmt::MsgJsonFormat; use opentelemetry::trace::TracerProvider; use opentelemetry_otlp::{SpanExporter, WithExportConfig}; use opentelemetry_sdk::trace as sdktrace; use tracing_subscriber::{fmt, fmt::Layer as FmtLayer, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; /// Guard that shuts down the OTLP pipeline on drop. #[must_use] pub struct OtelGuard { provider: sdktrace::SdkTracerProvider, } impl OtelGuard { /// Force-flush any pending spans and shut down the OTLP exporter. pub async fn shutdown(self) { if let Err(e) = self.provider.shutdown() { tracing::warn!(error = %e, "OTLP tracer shutdown error"); } } } /// Initialise OTLP tracing and attach it to the global tracing subscriber. /// /// Uses HTTP/proto transport to the given endpoint. /// Returns `Ok(Some(guard))` on success; the caller should store the guard and /// call `guard.shutdown()` during app shutdown for a clean flush. /// /// The `fmt_registry` parameter should be the value returned by /// `init_tracing_subscriber(level, true)` — i.e. a registry that was built but /// not yet installed. This function extends that registry with the OTLP tracing /// layer and calls `try_init()` once, avoiding the "global default already set" error. pub fn init_otlp( endpoint: &str, service_name: &str, _service_version: &str, log_level: &str, ) -> Result, InitOtlError> { if endpoint.is_empty() { return Err(InitOtlError::EmptyEndpoint); } let endpoint = endpoint.trim_end_matches('/'); let exporter = SpanExporter::builder() .with_http() .with_endpoint(endpoint) .build() .map_err(|e| InitOtlError::ExporterInit(e.to_string()))?; let env_filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new(log_level)); let fmt_layer: FmtLayer<_, _, _, _> = fmt::layer() .event_format(MsgJsonFormat); let tracer_provider = sdktrace::SdkTracerProvider::builder() .with_batch_exporter(exporter) .build(); let tracer = tracer_provider.tracer(service_name.to_string()); let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); let layered = tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .with(otel_layer); tracing::Dispatch::new(layered) .try_init() .map_err(|e| InitOtlError::SubscriberInit(e.to_string()))?; tracing::debug!(endpoint = %endpoint, "OTLP tracer installed"); Ok(Some(OtelGuard { provider: tracer_provider })) } #[derive(Debug, thiserror::Error)] pub enum InitOtlError { #[error("endpoint is empty")] EmptyEndpoint, #[error("failed to build OTLP exporter: {0}")] ExporterInit(String), #[error("failed to set tracing subscriber: {0}")] SubscriberInit(String), }