prop/rust/prop_grid_rs/src/telemetry.rs

108 lines
3.9 KiB
Rust

//! 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<SdkTracerProvider>,
}
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 }
}
}
}