vex_api/
telemetry.rs

1//! OpenTelemetry tracing configuration
2//!
3//! Provides configuration for distributed tracing with OpenTelemetry.
4//! Supports OTLP export and integration with existing tracing infrastructure.
5
6use std::time::Duration;
7
8/// OpenTelemetry configuration
9#[derive(Debug, Clone)]
10pub struct OtelConfig {
11    /// Service name for tracing
12    pub service_name: String,
13    /// OTLP endpoint (e.g., "http://localhost:4317")
14    pub endpoint: Option<String>,
15    /// Whether tracing is enabled
16    pub enabled: bool,
17    /// Sample rate (0.0-1.0)
18    pub sample_rate: f64,
19    /// Export batch size
20    pub batch_size: usize,
21    /// Export interval
22    pub export_interval: Duration,
23}
24
25impl Default for OtelConfig {
26    fn default() -> Self {
27        Self {
28            service_name: "vex-api".to_string(),
29            endpoint: None,
30            enabled: false,
31            sample_rate: 1.0,
32            batch_size: 512,
33            export_interval: Duration::from_secs(5),
34        }
35    }
36}
37
38impl OtelConfig {
39    /// Create config from environment variables
40    ///
41    /// Reads:
42    /// - OTEL_SERVICE_NAME: Service name (default: "vex-api")
43    /// - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint
44    /// - OTEL_TRACES_SAMPLER_ARG: Sample rate (default: 1.0)
45    pub fn from_env() -> Self {
46        let service_name =
47            std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "vex-api".to_string());
48
49        let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
50
51        let sample_rate = std::env::var("OTEL_TRACES_SAMPLER_ARG")
52            .ok()
53            .and_then(|s| s.parse().ok())
54            .unwrap_or(1.0);
55
56        Self {
57            service_name,
58            endpoint: endpoint.clone(),
59            enabled: endpoint.is_some(),
60            sample_rate,
61            ..Default::default()
62        }
63    }
64
65    /// Create a development config with console output
66    pub fn development() -> Self {
67        Self {
68            service_name: "vex-api-dev".to_string(),
69            endpoint: None,
70            enabled: true,
71            sample_rate: 1.0,
72            batch_size: 1,
73            export_interval: Duration::from_secs(1),
74        }
75    }
76
77    /// Create a production config
78    pub fn production(endpoint: &str) -> Self {
79        Self {
80            service_name: "vex-api".to_string(),
81            endpoint: Some(endpoint.to_string()),
82            enabled: true,
83            sample_rate: 0.1, // 10% sampling in production
84            batch_size: 512,
85            export_interval: Duration::from_secs(5),
86        }
87    }
88}
89
90/// Initialize tracing with optional OpenTelemetry export
91///
92/// This sets up the tracing subscriber with:
93/// - Console output (always)
94/// - OpenTelemetry OTLP export (if configured)
95pub fn init_tracing(config: &OtelConfig) {
96    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
97
98    // Build the base subscriber with env filter
99    let env_filter =
100        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,vex=debug"));
101
102    let fmt_layer = tracing_subscriber::fmt::layer()
103        .with_target(true)
104        .with_level(true)
105        .with_thread_ids(false)
106        .with_file(false)
107        .with_line_number(false);
108
109    if config.enabled {
110        if let Some(ref endpoint) = config.endpoint {
111            tracing::info!(
112                service = %config.service_name,
113                endpoint = %endpoint,
114                sample_rate = config.sample_rate,
115                "OpenTelemetry tracing enabled"
116            );
117        } else {
118            tracing::info!(
119                service = %config.service_name,
120                "OpenTelemetry tracing enabled (console only)"
121            );
122        }
123    }
124
125    tracing_subscriber::registry()
126        .with(env_filter)
127        .with(fmt_layer)
128        .init();
129
130    // Note: Full OTLP export requires adding opentelemetry crates:
131    // opentelemetry = "0.21"
132    // opentelemetry-otlp = "0.14"
133    // opentelemetry_sdk = "0.21"
134    // tracing-opentelemetry = "0.22"
135    //
136    // Example with full OTLP:
137    // ```
138    // let tracer = opentelemetry_otlp::new_pipeline()
139    //     .tracing()
140    //     .with_exporter(
141    //         opentelemetry_otlp::new_exporter()
142    //             .tonic()
143    //             .with_endpoint(endpoint)
144    //     )
145    //     .with_trace_config(
146    //         opentelemetry_sdk::trace::config()
147    //             .with_sampler(opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(sample_rate))
148    //             .with_resource(Resource::new(vec![
149    //                 KeyValue::new("service.name", service_name),
150    //             ]))
151    //     )
152    //     .install_batch(opentelemetry_sdk::runtime::Tokio)?;
153    //
154    // let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
155    // ```
156}
157
158/// Span extension trait for adding VEX-specific attributes
159pub trait VexSpanExt {
160    /// Add user ID to current span
161    fn record_user_id(&self, user_id: &str);
162    /// Add agent ID to current span
163    fn record_agent_id(&self, agent_id: &str);
164    /// Add request ID to current span
165    fn record_request_id(&self, request_id: &str);
166}
167
168impl VexSpanExt for tracing::Span {
169    fn record_user_id(&self, user_id: &str) {
170        self.record("user_id", user_id);
171    }
172
173    fn record_agent_id(&self, agent_id: &str) {
174        self.record("agent_id", agent_id);
175    }
176
177    fn record_request_id(&self, request_id: &str) {
178        self.record("request_id", request_id);
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_otel_config_default() {
188        let config = OtelConfig::default();
189        assert_eq!(config.service_name, "vex-api");
190        assert!(!config.enabled);
191        assert!(config.endpoint.is_none());
192    }
193
194    #[test]
195    fn test_otel_config_production() {
196        let config = OtelConfig::production("http://otel:4317");
197        assert!(config.enabled);
198        assert_eq!(config.endpoint, Some("http://otel:4317".to_string()));
199        assert_eq!(config.sample_rate, 0.1);
200    }
201}