Skip to main content

embeddenator_obs/obs/
tracing.rs

1//! Tracing and Span Instrumentation
2//!
3//! Provides structured tracing with span instrumentation for performance
4//! analysis and debugging. Built on the `tracing` ecosystem.
5//!
6//! # Features
7//!
8//! - Low-overhead span instrumentation
9//! - Hierarchical span nesting
10//! - Automatic timing capture
11//! - Structured field logging
12//! - Multiple subscriber backends
13//!
14//! # Usage
15//!
16//! ```rust,ignore
17//! use embeddenator_obs::tracing::{init_tracing, span_operation};
18//!
19//! // Initialize once at startup
20//! init_tracing();
21//!
22//! // Instrument a function
23//! #[span_operation]
24//! fn process_query(query: &str) -> Result<Vec<u8>> {
25//!     // Automatically creates span with function name and arguments
26//!     // ...
27//! }
28//!
29//! // Manual span creation
30//! let _span = create_span("custom_operation", &[("key", "value")]);
31//! // Work happens here, timing is automatic
32//! ```
33//!
34//! # Performance
35//!
36//! When the `tracing` feature is disabled, all instrumentation compiles
37//! to zero-cost. With the feature enabled, typical overhead is <100ns per span.
38
39#[cfg(feature = "tracing")]
40use tracing::{span, Level, Span};
41
42/// Initialize tracing with environment-based configuration.
43///
44/// Reads configuration from:
45/// - `EMBEDDENATOR_LOG`: custom log filter (e.g., "embeddenator=debug")
46/// - `RUST_LOG`: fallback log filter
47/// - `EMBEDDENATOR_TRACE_FORMAT`: output format ("compact", "pretty", "json")
48///
49/// Default: disabled (filter="off")
50#[cfg(feature = "tracing")]
51pub fn init_tracing() {
52    use tracing_subscriber::{fmt, EnvFilter};
53
54    let filter = std::env::var("EMBEDDENATOR_LOG")
55        .ok()
56        .or_else(|| std::env::var("RUST_LOG").ok())
57        .unwrap_or_else(|| "off".to_string());
58
59    let format = std::env::var("EMBEDDENATOR_TRACE_FORMAT")
60        .ok()
61        .unwrap_or_else(|| "compact".to_string());
62
63    let env_filter = EnvFilter::try_from_default_env()
64        .or_else(|_| EnvFilter::try_new(&filter))
65        .unwrap_or_else(|_| EnvFilter::new("off"));
66
67    match format.as_str() {
68        "json" => {
69            let _ = fmt().json().with_env_filter(env_filter).try_init();
70        }
71        "pretty" => {
72            let _ = fmt().pretty().with_env_filter(env_filter).try_init();
73        }
74        _ => {
75            let _ = fmt().compact().with_env_filter(env_filter).try_init();
76        }
77    }
78}
79
80#[cfg(not(feature = "tracing"))]
81pub fn init_tracing() {}
82
83/// Create a named span with optional fields.
84///
85/// The span is automatically entered and will record timing information
86/// when dropped.
87///
88/// # Example
89///
90/// ```rust,ignore
91/// let _span = create_span("query", &[("dim", "768"), ("k", "10")]);
92/// // Work happens here
93/// // Span automatically closes and records timing on drop
94/// ```
95#[cfg(feature = "tracing")]
96pub fn create_span(name: &str, fields: &[(&str, &str)]) -> Span {
97    let span = span!(Level::INFO, "op", name = name);
98    for (key, value) in fields {
99        span.record(*key, value);
100    }
101    span
102}
103
104#[cfg(not(feature = "tracing"))]
105pub fn create_span(_name: &str, _fields: &[(&str, &str)]) {}
106
107/// Create a debug-level span (only active when debug logging enabled).
108#[cfg(feature = "tracing")]
109pub fn create_debug_span(name: &str, fields: &[(&str, &str)]) -> Span {
110    let span = span!(Level::DEBUG, "debug_op", name = name);
111    for (key, value) in fields {
112        span.record(*key, value);
113    }
114    span
115}
116
117#[cfg(not(feature = "tracing"))]
118pub fn create_debug_span(_name: &str, _fields: &[(&str, &str)]) {}
119
120/// Create a trace-level span (highest detail, for deep debugging).
121#[cfg(feature = "tracing")]
122pub fn create_trace_span(name: &str, fields: &[(&str, &str)]) -> Span {
123    let span = span!(Level::TRACE, "trace_op", name = name);
124    for (key, value) in fields {
125        span.record(*key, value);
126    }
127    span
128}
129
130#[cfg(not(feature = "tracing"))]
131pub fn create_trace_span(_name: &str, _fields: &[(&str, &str)]) {}
132
133/// Span guard type (transparent across feature gate).
134#[cfg(feature = "tracing")]
135pub type SpanGuard = Span;
136
137#[cfg(not(feature = "tracing"))]
138pub type SpanGuard = ();
139
140/// Macro for quick span creation with automatic entry.
141///
142/// # Example
143///
144/// ```rust,ignore
145/// span_scope!("operation_name", field1 = "value1", field2 = "value2");
146/// // Code here is instrumented
147/// ```
148#[macro_export]
149#[cfg(feature = "tracing")]
150macro_rules! span_scope {
151    ($name:expr) => {
152        let _guard = $crate::tracing::create_span($name, &[]);
153    };
154    ($name:expr, $($key:tt = $val:expr),*) => {
155        {
156            let fields = vec![$(( stringify!($key), &format!("{}", $val) as &str ),)*];
157            let _guard = $crate::tracing::create_span($name, &fields);
158        }
159    };
160}
161
162#[cfg(not(feature = "tracing"))]
163#[macro_export]
164macro_rules! span_scope {
165    ($name:expr $(, $key:tt = $val:expr)*) => {
166        ()
167    };
168}
169
170/// Record an event in the current span.
171#[cfg(feature = "tracing")]
172pub fn record_event(level: EventLevel, message: &str, fields: &[(&str, &str)]) {
173    match level {
174        EventLevel::Error => tracing::error!(message = %message, ?fields),
175        EventLevel::Warn => tracing::warn!(message = %message, ?fields),
176        EventLevel::Info => tracing::info!(message = %message, ?fields),
177        EventLevel::Debug => tracing::debug!(message = %message, ?fields),
178        EventLevel::Trace => tracing::trace!(message = %message, ?fields),
179    }
180}
181
182#[cfg(not(feature = "tracing"))]
183pub fn record_event(_level: EventLevel, message: &str, _fields: &[(&str, &str)]) {
184    if matches!(_level, EventLevel::Error | EventLevel::Warn) {
185        eprintln!("[{}] {}", _level.as_str(), message);
186    }
187}
188
189/// Event severity level.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum EventLevel {
192    Error,
193    Warn,
194    Info,
195    Debug,
196    Trace,
197}
198
199impl EventLevel {
200    pub fn as_str(&self) -> &'static str {
201        match self {
202            EventLevel::Error => "ERROR",
203            EventLevel::Warn => "WARN",
204            EventLevel::Info => "INFO",
205            EventLevel::Debug => "DEBUG",
206            EventLevel::Trace => "TRACE",
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_init_tracing_no_panic() {
217        // Should not panic regardless of feature state
218        init_tracing();
219    }
220
221    #[test]
222    fn test_span_creation() {
223        let _span = create_span("test_op", &[("key", "value")]);
224        // Should compile and not panic
225    }
226
227    #[test]
228    fn test_event_recording() {
229        record_event(EventLevel::Info, "test message", &[("field", "value")]);
230        // Should compile and not panic
231    }
232
233    #[test]
234    fn test_event_level_str() {
235        assert_eq!(EventLevel::Error.as_str(), "ERROR");
236        assert_eq!(EventLevel::Info.as_str(), "INFO");
237    }
238}