oxify_vector/
otel.rs

1//! OpenTelemetry Tracing Integration
2//!
3//! Provides distributed tracing for vector search operations using OpenTelemetry.
4//!
5//! ## Features
6//!
7//! - **Search Tracing**: Automatic span creation for search operations
8//! - **Metadata Annotations**: Enrich spans with search parameters (k, metric, filters)
9//! - **Performance Metrics**: Capture latency and result counts
10//! - **Distributed Context**: Propagate trace context across services
11//!
12//! ## Example
13//!
14//! ```rust,ignore
15//! use oxify_vector::otel::{TracingConfig, init_tracing, trace_search};
16//!
17//! // Initialize tracing (requires "otel" feature)
18//! let config = TracingConfig::default();
19//! init_tracing(config)?;
20//!
21//! // Trace a search operation
22//! let query = vec![0.1, 0.2, 0.3];
23//! let results: Vec<String> = trace_search("my_index", &query, 10, || {
24//!     // Perform search
25//!     vec!["doc1".to_string(), "doc2".to_string()]
26//! })?;
27//! ```
28
29#[cfg(feature = "otel")]
30use opentelemetry::{
31    global,
32    trace::{Span, Status, Tracer},
33    KeyValue,
34};
35
36#[cfg(feature = "otel")]
37use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider};
38
39use anyhow::Result;
40
41/// Configuration for OpenTelemetry tracing
42#[derive(Debug, Clone)]
43pub struct TracingConfig {
44    /// Service name for this application
45    pub service_name: String,
46    /// Service version
47    pub service_version: String,
48    /// Sampling ratio (0.0 to 1.0)
49    pub sampling_ratio: f64,
50}
51
52impl Default for TracingConfig {
53    fn default() -> Self {
54        Self {
55            service_name: "oxify-vector".to_string(),
56            service_version: env!("CARGO_PKG_VERSION").to_string(),
57            sampling_ratio: 1.0,
58        }
59    }
60}
61
62/// Initialize OpenTelemetry tracing
63///
64/// This sets up the global tracer provider with the specified configuration.
65/// Call this once at application startup.
66#[cfg(feature = "otel")]
67pub fn init_tracing(_config: TracingConfig) -> Result<()> {
68    // Note: In opentelemetry_sdk 0.31+, Resource creation API has changed
69    // For now, we use a basic provider without custom resource attributes
70    // The tracer provider will use default resource detection
71    let provider = SdkTracerProvider::builder()
72        .with_id_generator(RandomIdGenerator::default())
73        .with_sampler(Sampler::AlwaysOn)
74        .build();
75
76    global::set_tracer_provider(provider);
77
78    Ok(())
79}
80
81/// Shutdown tracing (call before application exit)
82#[cfg(feature = "otel")]
83pub fn shutdown_tracing() {
84    // Note: In opentelemetry 0.31+, shutdown is handled by dropping the provider
85    // The global provider will be cleaned up when the process exits
86}
87
88/// Trace a search operation
89///
90/// Creates a span for the search operation and records key metrics.
91///
92/// # Arguments
93///
94/// * `index_name` - Name of the index being searched
95/// * `query` - Query vector
96/// * `k` - Number of results requested
97/// * `f` - Function that performs the search
98#[cfg(feature = "otel")]
99pub fn trace_search<F, T>(index_name: &str, query: &[f32], k: usize, f: F) -> Result<T>
100where
101    F: FnOnce() -> T,
102{
103    let tracer = global::tracer("oxify-vector");
104    let mut span = tracer
105        .span_builder(format!("search.{}", index_name))
106        .start(&tracer);
107
108    // Add attributes
109    span.set_attribute(KeyValue::new("vector.dimensions", query.len() as i64));
110    span.set_attribute(KeyValue::new("vector.k", k as i64));
111    span.set_attribute(KeyValue::new("index.name", index_name.to_string()));
112
113    // Execute search
114    let result = f();
115
116    // Mark span as successful
117    span.set_status(Status::Ok);
118    span.end();
119
120    Ok(result)
121}
122
123/// Trace a search operation with additional metadata
124///
125/// Like `trace_search`, but allows specifying the distance metric and filter info.
126#[cfg(feature = "otel")]
127#[allow(clippy::too_many_arguments)]
128pub fn trace_search_detailed<F, T>(
129    index_name: &str,
130    query: &[f32],
131    k: usize,
132    metric: &str,
133    filter_applied: bool,
134    result_count: usize,
135    f: F,
136) -> Result<T>
137where
138    F: FnOnce() -> T,
139{
140    let tracer = global::tracer("oxify-vector");
141    let mut span = tracer
142        .span_builder(format!("search.{}", index_name))
143        .start(&tracer);
144
145    // Add attributes
146    span.set_attribute(KeyValue::new("vector.dimensions", query.len() as i64));
147    span.set_attribute(KeyValue::new("vector.k", k as i64));
148    span.set_attribute(KeyValue::new("index.name", index_name.to_string()));
149    span.set_attribute(KeyValue::new("search.metric", metric.to_string()));
150    span.set_attribute(KeyValue::new("search.filtered", filter_applied));
151    span.set_attribute(KeyValue::new("search.result_count", result_count as i64));
152
153    // Execute search
154    let result = f();
155
156    // Mark span as successful
157    span.set_status(Status::Ok);
158    span.end();
159
160    Ok(result)
161}
162
163/// Helper to record an error in the current span
164#[cfg(feature = "otel")]
165pub fn record_error_message(error_msg: &str) {
166    let tracer = global::tracer("oxify-vector");
167    let mut span = tracer.span_builder("error").start(&tracer);
168
169    span.set_status(Status::error(error_msg.to_string()));
170    span.set_attribute(KeyValue::new("error.message", error_msg.to_string()));
171    span.end();
172}
173
174// Stub implementations when otel feature is disabled
175#[cfg(not(feature = "otel"))]
176pub fn init_tracing(_config: TracingConfig) -> Result<()> {
177    Ok(())
178}
179
180#[cfg(not(feature = "otel"))]
181pub fn shutdown_tracing() {}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_tracing_config_default() {
189        let config = TracingConfig::default();
190        assert_eq!(config.service_name, "oxify-vector");
191        assert_eq!(config.sampling_ratio, 1.0);
192    }
193
194    #[test]
195    fn test_tracing_config_custom() {
196        let config = TracingConfig {
197            service_name: "my-service".to_string(),
198            service_version: "1.0.0".to_string(),
199            sampling_ratio: 0.5,
200        };
201        assert_eq!(config.service_name, "my-service");
202        assert_eq!(config.service_version, "1.0.0");
203        assert_eq!(config.sampling_ratio, 0.5);
204    }
205
206    #[test]
207    #[cfg(feature = "otel")]
208    fn test_init_and_shutdown_tracing() {
209        let config = TracingConfig::default();
210        let result = init_tracing(config);
211        assert!(result.is_ok());
212        shutdown_tracing();
213    }
214
215    #[test]
216    #[cfg(feature = "otel")]
217    fn test_trace_search() {
218        let config = TracingConfig::default();
219        init_tracing(config).unwrap();
220
221        let query = vec![0.1, 0.2, 0.3];
222        let result = trace_search("test_index", &query, 5, || vec!["doc1", "doc2"]);
223
224        assert!(result.is_ok());
225        let docs = result.unwrap();
226        assert_eq!(docs.len(), 2);
227
228        shutdown_tracing();
229    }
230
231    #[test]
232    #[cfg(feature = "otel")]
233    fn test_trace_search_detailed() {
234        let config = TracingConfig::default();
235        init_tracing(config).unwrap();
236
237        let query = vec![0.1, 0.2, 0.3];
238        let result = trace_search_detailed("test_index", &query, 5, "cosine", true, 2, || {
239            vec!["doc1", "doc2"]
240        });
241
242        assert!(result.is_ok());
243        let docs = result.unwrap();
244        assert_eq!(docs.len(), 2);
245
246        shutdown_tracing();
247    }
248
249    #[test]
250    #[cfg(not(feature = "otel"))]
251    fn test_stub_functions() {
252        // These should do nothing when otel feature is disabled
253        let config = TracingConfig::default();
254        let result = init_tracing(config);
255        assert!(result.is_ok());
256        shutdown_tracing();
257    }
258}