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 {
_private: (),
}
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::runtime::Tokio;
use opentelemetry_sdk::trace::TracerProvider;
use tracing_opentelemetry::layer;
use tracing_subscriber::layer::SubscriberExt;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
.build()
.map_err(|e| TracingError::ExporterInit(e.to_string()))?;
let provider = TracerProvider::builder()
.with_batch_exporter(exporter, Tokio)
.with_resource(Resource::new(vec![opentelemetry::KeyValue::new(
"service.name",
"dbnexus",
)]))
.build();
let tracer = OtelTracerProvider::tracer(&provider, "dbnexus");
global::set_tracer_provider(provider);
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 { _private: () })
}
}
impl Drop for TracingGuard {
fn drop(&mut self) {
opentelemetry::global::shutdown_tracer_provider();
}
}
#[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>();
}
}