Skip to main content

backbone_observability/
tracing.rs

1//! OpenTelemetry tracing setup
2//!
3//! Provides distributed tracing instrumentation.
4
5use tracing_subscriber::{layer::SubscriberExt, Registry, EnvFilter};
6use thiserror::Error;
7use crate::config::ObservabilityConfig;
8
9/// Tracing initialization error
10#[derive(Debug, Error)]
11pub enum TracingError {
12    /// Failed to setup tracing (subscriber, exporter, configuration)
13    #[error("Trace setup error: {0}")]
14    SetupError(String),
15}
16
17/// Initialize tracing with OpenTelemetry support (when feature enabled)
18///
19/// # Arguments
20/// * `config` - Observability configuration
21pub fn init_tracing(config: &ObservabilityConfig) -> Result<(), TracingError> {
22    if !config.tracing_enabled {
23        return Ok(());
24    }
25
26    // Set up tracing subscriber with env filter
27    let filter = EnvFilter::try_from_default_env()
28        .unwrap_or_else(|_| EnvFilter::new(&config.log_level));
29
30    let subscriber = Registry::default()
31        .with(filter)
32        .with(
33            tracing_subscriber::fmt::layer()
34                .with_target(true)
35                .with_thread_ids(false)
36                .with_line_number(true)
37        );
38
39    // Set global subscriber
40    tracing::subscriber::set_global_default(subscriber)
41        .map_err(|e| TracingError::SetupError(e.to_string()))?;
42
43    tracing::info!("Tracing initialized for service: {}", config.service_name);
44
45    #[cfg(feature = "otel-tracing")]
46    {
47        tracing::info!("OpenTelemetry tracing is available via init_tracing_with_otel()");
48    }
49
50    Ok(())
51}
52
53/// Initialize OpenTelemetry tracing with exporter
54///
55/// # Arguments
56/// * `config` - Observability configuration
57///
58/// # Returns
59/// Returns a shutdown guard that flushes traces when dropped
60///
61/// # Example
62/// ```no_run
63/// # #[cfg(feature = "otel-tracing")]
64/// # {
65/// use backbone_observability::tracing::init_tracing_with_otel;
66/// use backbone_observability::ObservabilityConfig;
67///
68/// # #[tokio::main]
69/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
70/// let config = ObservabilityConfig {
71///     tracing_enabled: true,
72///     service_name: "my-service".to_string(),
73///     otlp_endpoint: Some("http://localhost:4317".to_string()),
74///     ..Default::default()
75/// };
76///
77/// let _guard = init_tracing_with_otel(&config)?;
78/// # Ok(())
79/// # }
80/// # }
81/// ```
82#[cfg(feature = "otel-tracing")]
83pub fn init_tracing_with_otel(config: &ObservabilityConfig) -> Result<OtelGuard, TracingError> {
84    use opentelemetry::trace::TracerProvider;
85    use opentelemetry_sdk::trace::TracerProvider as SdkTracerProvider;
86    use opentelemetry_sdk::Resource;
87    use opentelemetry::KeyValue;
88
89    if !config.tracing_enabled {
90        return Ok(OtelGuard::None);
91    }
92
93    // Configure the service resource
94    let resource = Resource::new(vec![
95        KeyValue::new("service.name", config.service_name.clone()),
96        KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
97    ]);
98
99    // Set up tracing subscriber with env filter
100    let filter = EnvFilter::try_from_default_env()
101        .unwrap_or_else(|_| EnvFilter::new(&config.log_level));
102
103    // Create a simple tracer provider without exporter
104    let tracer_provider = SdkTracerProvider::builder()
105        .with_resource(resource)
106        .build();
107
108    // Create OpenTelemetry layer
109    let otel_layer = tracing_opentelemetry::layer()
110        .with_tracer(tracer_provider.tracer(config.service_name.clone()));
111
112    let subscriber = Registry::default()
113        .with(filter)
114        .with(otel_layer)
115        .with(
116            tracing_subscriber::fmt::layer()
117                .with_target(true)
118                .with_thread_ids(false)
119                .with_line_number(true)
120        );
121
122    tracing::subscriber::set_global_default(subscriber)
123        .map_err(|e| TracingError::SetupError(e.to_string()))?;
124
125    tracing::info!("OpenTelemetry tracing initialized for service: {}", config.service_name);
126
127    if let Some(endpoint) = &config.otlp_endpoint {
128        tracing::info!("OTLP endpoint configured: {}", endpoint);
129        tracing::info!("Note: OTLP exporter setup requires additional configuration");
130    } else {
131        tracing::info!("No OTLP endpoint configured - tracing to console only");
132    }
133
134    Ok(OtelGuard::Some(tracer_provider))
135}
136
137/// Shutdown guard for OpenTelemetry tracer
138///
139/// When dropped, this guard ensures the tracer provider is properly
140/// shutdown and all remaining traces are flushed.
141#[cfg(feature = "otel-tracing")]
142pub enum OtelGuard {
143    /// Contains the tracer provider that will be shutdown on drop
144    Some(opentelemetry_sdk::trace::TracerProvider),
145    /// No tracing was initialized (disabled in config)
146    None,
147}
148
149#[cfg(feature = "otel-tracing")]
150impl Drop for OtelGuard {
151    fn drop(&mut self) {
152        if let Self::Some(_) = self {
153            tracing::debug!("OtelGuard dropped - tracer provider shutdown");
154        }
155    }
156}
157
158/// Create a span for HTTP request tracking
159///
160/// # Example
161/// ```no_run
162/// use backbone_observability::tracing::http_request_span;
163///
164/// let span = http_request_span("GET", "/api/users");
165/// let _enter = span.enter();
166/// // Handle request...
167/// ```
168pub fn http_request_span(method: &str, path: &str) -> tracing::Span {
169    tracing::info_span!(
170        "http_request",
171        method = %method,
172        path = %path,
173        status = tracing::field::Empty,
174        duration_ms = tracing::field::Empty,
175    )
176}
177
178/// Build an OpenTelemetry tracer and provider with optional OTLP exporter.
179///
180/// Returns `(Tracer, TracerProvider)`. The caller wraps the tracer in
181/// `tracing_opentelemetry::layer().with_tracer(tracer)` and composes it
182/// into the subscriber (so the generic `S` type parameter is inferred
183/// correctly without boxing).
184///
185/// Also sets the global W3C TraceContext propagator so incoming
186/// `traceparent` / `tracestate` headers are parsed automatically.
187#[cfg(feature = "otel-tracing")]
188pub fn build_otel_tracer(
189    config: &ObservabilityConfig,
190) -> Result<(opentelemetry_sdk::trace::Tracer, opentelemetry_sdk::trace::TracerProvider), TracingError>
191{
192    use opentelemetry::trace::TracerProvider;
193    use opentelemetry_sdk::trace::TracerProvider as SdkTracerProvider;
194    use opentelemetry_sdk::Resource;
195    use opentelemetry::KeyValue;
196    use opentelemetry_otlp::WithExportConfig;
197
198    // Set W3C TraceContext propagator globally (enables traceparent extraction)
199    opentelemetry::global::set_text_map_propagator(
200        opentelemetry_sdk::propagation::TraceContextPropagator::new()
201    );
202
203    let resource = Resource::new(vec![
204        KeyValue::new("service.name", config.service_name.clone()),
205        KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
206    ]);
207
208    let mut provider_builder = SdkTracerProvider::builder().with_resource(resource);
209
210    // Wire OTLP exporter when endpoint is configured
211    if let Some(endpoint) = &config.otlp_endpoint {
212        let exporter = opentelemetry_otlp::SpanExporter::builder()
213            .with_tonic()
214            .with_endpoint(endpoint)
215            .build()
216            .map_err(|e| TracingError::SetupError(format!("OTLP exporter: {e}")))?;
217
218        provider_builder = provider_builder.with_simple_exporter(exporter);
219    }
220
221    let provider = provider_builder.build();
222    let tracer = provider.tracer(config.service_name.clone());
223
224    Ok((tracer, provider))
225}
226
227/// Common tracing span attributes
228pub mod span_attributes {
229    /// HTTP method attribute
230    pub const HTTP_METHOD: &str = "http.method";
231
232    /// HTTP path attribute
233    pub const HTTP_PATH: &str = "http.path";
234
235    /// HTTP status code attribute
236    pub const HTTP_STATUS: &str = "http.status_code";
237
238    /// User ID attribute
239    pub const USER_ID: &str = "user.id";
240
241    /// Request ID attribute
242    pub const REQUEST_ID: &str = "request.id";
243
244    /// Module attribute
245    pub const MODULE: &str = "code.module";
246
247    /// Function attribute
248    pub const FUNCTION: &str = "code.function";
249}