1use std::time::Duration;
7
8#[derive(Debug, Clone)]
10pub struct OtelConfig {
11 pub service_name: String,
13 pub endpoint: Option<String>,
15 pub enabled: bool,
17 pub sample_rate: f64,
19 pub batch_size: usize,
21 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 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 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 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, batch_size: 512,
85 export_interval: Duration::from_secs(5),
86 }
87 }
88}
89
90pub fn init_tracing(config: &OtelConfig) {
96 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
97
98 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 }
157
158pub trait VexSpanExt {
160 fn record_user_id(&self, user_id: &str);
162 fn record_agent_id(&self, agent_id: &str);
164 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}