use anyhow::Result;
#[derive(Debug, Clone, Default)]
pub struct TracingConfig {
pub service_name: String,
pub service_version: Option<String>,
pub otlp_endpoint: Option<String>,
pub sample_ratio: Option<f64>,
}
impl TracingConfig {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
..Default::default()
}
}
pub fn with_otlp(mut self, endpoint: impl Into<String>) -> Self {
self.otlp_endpoint = Some(endpoint.into());
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.service_version = Some(version.into());
self
}
pub fn with_sample_ratio(mut self, ratio: f64) -> Self {
self.sample_ratio = Some(ratio.clamp(0.0, 1.0));
self
}
}
#[cfg(feature = "otlp")]
pub fn init_tracing(config: &TracingConfig) -> Result<TracingGuard> {
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
let mut provider_builder = SdkTracerProvider::builder();
if let Some(ratio) = config.sample_ratio {
provider_builder = provider_builder.with_sampler(Sampler::TraceIdRatioBased(ratio));
}
if let Some(endpoint) = &config.otlp_endpoint {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
provider_builder = provider_builder.with_batch_exporter(exporter);
tracing::info!(endpoint = %endpoint, "OTLP exporter configured");
}
let mut attributes = vec![KeyValue::new("service.name", config.service_name.clone())];
if let Some(version) = &config.service_version {
attributes.push(KeyValue::new("service.version", version.clone()));
}
let resource = Resource::builder().with_attributes(attributes).build();
provider_builder = provider_builder.with_resource(resource);
let provider = provider_builder.build();
global::set_tracer_provider(provider.clone());
tracing::info!(
service_name = %config.service_name,
"OpenTelemetry tracer provider initialized"
);
Ok(TracingGuard {
provider: Some(provider),
})
}
#[cfg(not(feature = "otlp"))]
pub fn init_tracing(_config: &TracingConfig) -> Result<TracingGuard> {
tracing::debug!("OTLP feature not enabled, skipping tracer initialization");
Ok(TracingGuard { provider: None })
}
#[cfg(feature = "telemetry")]
pub fn create_otel_layer() -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
tracing_opentelemetry::layer()
}
pub struct TracingGuard {
#[cfg(feature = "otlp")]
provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
#[cfg(not(feature = "otlp"))]
#[allow(dead_code)]
provider: Option<()>,
}
impl TracingGuard {
pub fn is_initialized(&self) -> bool {
self.provider.is_some()
}
}
impl Drop for TracingGuard {
fn drop(&mut self) {
#[cfg(feature = "otlp")]
if let Some(provider) = self.provider.take() {
tracing::info!("Shutting down OpenTelemetry tracer provider");
if let Err(e) = provider.shutdown() {
tracing::warn!("Failed to shutdown tracer provider: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracing_config_builder() {
let config = TracingConfig::new("test-service")
.with_otlp("http://localhost:4317")
.with_version("1.0.0")
.with_sample_ratio(0.5);
assert_eq!(config.service_name, "test-service");
assert_eq!(
config.otlp_endpoint,
Some("http://localhost:4317".to_string())
);
assert_eq!(config.service_version, Some("1.0.0".to_string()));
assert_eq!(config.sample_ratio, Some(0.5));
}
#[test]
fn test_sample_ratio_clamping() {
let config = TracingConfig::new("test").with_sample_ratio(1.5);
assert_eq!(config.sample_ratio, Some(1.0));
let config = TracingConfig::new("test").with_sample_ratio(-0.5);
assert_eq!(config.sample_ratio, Some(0.0));
}
}