//! Tracing init with optional OTLP export. //! //! Both bin targets (`worker`, `hrrr_point_worker`) call `init(svc)` at //! startup. The local fmt layer (JSON to stdout) is always on so kubectl //! logs keeps working. When `OTEL_EXPORTER_OTLP_ENDPOINT` is set we //! additionally install a `tracing-opentelemetry` layer that ships //! spans over OTLP/gRPC to the cluster's OTel Collector — see //! `k8s/deployment-grid-rs.yaml` + `deployment-hrrr-point-rs.yaml` //! for the env wiring. //! //! Returning the `TracerProvider` from `init` is deliberate: the caller //! must hold it until shutdown and call `.shutdown()` so queued spans //! flush before the process exits. Dropping it mid-run cancels the //! background exporter and loses recent spans. //! //! Service identity is set via OpenTelemetry resource attributes //! (`service.name`, `service.namespace`) so Tempo groups spans per //! binary. `service.namespace=prop` matches the k8s namespace. use opentelemetry::{global, trace::TracerProvider as _, KeyValue}; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; /// Opaque handle the caller must hold until process shutdown; dropping /// it stops the OTLP exporter. Returns `None` when OTLP is disabled /// (no endpoint env var) so callers don't need to care which mode is /// active. pub struct TelemetryGuard { provider: Option, } impl TelemetryGuard { /// Flush queued spans and shut down the exporter. Safe to call /// multiple times; subsequent calls are no-ops. pub fn shutdown(mut self) { if let Some(provider) = self.provider.take() { let _ = provider.shutdown(); } } } impl Drop for TelemetryGuard { fn drop(&mut self) { if let Some(provider) = self.provider.take() { let _ = provider.shutdown(); } } } /// Initialize tracing for a binary. `service_name` becomes the /// `service.name` attribute in exported spans and the log target /// prefix. pub fn init(service_name: &'static str) -> TelemetryGuard { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let fmt_layer = tracing_subscriber::fmt::layer().json(); // If the collector endpoint isn't set, skip the OTLP layer entirely // — useful for local cargo runs where there's no collector to ship // to. The fmt layer alone keeps dev parity with prod log output. let otlp_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); match otlp_endpoint { Some(endpoint) if !endpoint.is_empty() => { let exporter = opentelemetry_otlp::SpanExporter::builder() .with_tonic() .with_endpoint(endpoint) .build() .expect("build OTLP span exporter"); let resource = Resource::builder() .with_attributes([ KeyValue::new("service.name", service_name), KeyValue::new("service.namespace", "prop"), ]) .build(); let provider = SdkTracerProvider::builder() .with_batch_exporter(exporter) .with_resource(resource) .build(); let tracer = provider.tracer(service_name); global::set_tracer_provider(provider.clone()); let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .with(otel_layer) .init(); TelemetryGuard { provider: Some(provider), } } _ => { tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .init(); TelemetryGuard { provider: None } } } }