use std::sync::OnceLock;
use thiserror::Error;
static TRACING_INITIALIZED: OnceLock<()> = OnceLock::new();
#[derive(Debug, Error)]
pub enum TracingError {
#[error("OTLP exporter initialization failed: {0}")]
ExporterInit(String),
#[error("Tracer provider setup failed: {0}")]
ProviderSetup(String),
#[error("Tracing already initialized: global subscriber can only be set once per process")]
AlreadyInitialized,
#[error("Failed to set global subscriber: {0}")]
SubscriberSetup(String),
}
pub struct TracingGuard {
provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
}
impl TracingGuard {
pub fn init_with_otlp(endpoint: &str) -> Result<Self, TracingError> {
if TRACING_INITIALIZED.get().is_some() {
return Err(TracingError::AlreadyInitialized);
}
use opentelemetry::global;
use opentelemetry::trace::TracerProvider as OtelTracerProvider;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_opentelemetry::layer;
use tracing_subscriber::layer::SubscriberExt;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|e| TracingError::ExporterInit(e.to_string()))?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(Resource::builder().with_service_name("dbnexus").build())
.build();
let tracer = OtelTracerProvider::tracer(&provider, "dbnexus");
global::set_tracer_provider(provider.clone());
let otel_layer = layer().with_tracer(tracer);
let subscriber = tracing_subscriber::registry().with(otel_layer);
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| TracingError::SubscriberSetup(e.to_string()))?;
let _ = TRACING_INITIALIZED.set(());
Ok(TracingGuard {
provider: Some(provider),
})
}
}
impl Drop for TracingGuard {
fn drop(&mut self) {
if let Some(provider) = self.provider.take() {
let _ = provider.shutdown();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracing_error_display() {
let err = TracingError::ExporterInit("connection refused".to_string());
assert!(err.to_string().contains("OTLP exporter"));
assert!(err.to_string().contains("connection refused"));
let err = TracingError::AlreadyInitialized;
assert!(err.to_string().contains("already initialized"));
}
#[test]
fn test_tracing_guard_private_field() {
fn assert_send<T: Send>() {}
assert_send::<TracingGuard>();
}
}