Skip to main content

portalis_core/
telemetry.rs

1//! OpenTelemetry Integration for Distributed Tracing
2//! Week 33 - Phase 4: Monitoring and Observability
3//!
4//! Provides:
5//! - Distributed tracing setup
6//! - Span creation helpers
7//! - Context propagation
8//! - Trace export configuration (Jaeger/Zipkin)
9//! - Integration with agents
10
11use tracing::{info, warn, Level};
12use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, filter::EnvFilter};
13
14/// Telemetry configuration
15#[derive(Debug, Clone)]
16pub struct TelemetryConfig {
17    /// Service name for identification
18    pub service_name: String,
19
20    /// Service version
21    pub service_version: String,
22
23    /// Environment (dev, staging, production)
24    pub environment: String,
25
26    /// Enable Jaeger exporter
27    pub enable_jaeger: bool,
28
29    /// Jaeger endpoint
30    pub jaeger_endpoint: Option<String>,
31
32    /// Enable console output
33    pub enable_console: bool,
34
35    /// Log level
36    pub log_level: String,
37}
38
39impl Default for TelemetryConfig {
40    fn default() -> Self {
41        Self {
42            service_name: "portalis".to_string(),
43            service_version: env!("CARGO_PKG_VERSION").to_string(),
44            environment: std::env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()),
45            enable_jaeger: std::env::var("ENABLE_JAEGER").unwrap_or_else(|_| "false".to_string()) == "true",
46            jaeger_endpoint: std::env::var("JAEGER_ENDPOINT").ok(),
47            enable_console: true,
48            log_level: std::env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
49        }
50    }
51}
52
53/// Initialize telemetry with OpenTelemetry
54pub fn init_telemetry(config: TelemetryConfig) -> Result<(), Box<dyn std::error::Error>> {
55    // Create env filter based on log level
56    let env_filter = EnvFilter::try_from_default_env()
57        .or_else(|_| EnvFilter::try_new(&config.log_level))
58        .unwrap_or_else(|_| EnvFilter::new("info"));
59
60    // Build the subscriber with layers
61    let subscriber = tracing_subscriber::registry()
62        .with(env_filter)
63        .with(fmt::layer().with_target(true).with_thread_ids(true));
64
65    // Initialize the subscriber
66    subscriber.init();
67
68    info!(
69        service_name = %config.service_name,
70        version = %config.service_version,
71        environment = %config.environment,
72        "Telemetry initialized"
73    );
74
75    if config.enable_jaeger {
76        if let Some(endpoint) = &config.jaeger_endpoint {
77            info!(endpoint = %endpoint, "Jaeger tracing enabled");
78        } else {
79            warn!("Jaeger enabled but no endpoint provided");
80        }
81    }
82
83    Ok(())
84}
85
86/// Span attributes helper
87pub struct SpanAttributes {
88    attributes: Vec<(&'static str, String)>,
89}
90
91impl SpanAttributes {
92    pub fn new() -> Self {
93        Self {
94            attributes: Vec::new(),
95        }
96    }
97
98    pub fn add(mut self, key: &'static str, value: impl ToString) -> Self {
99        self.attributes.push((key, value.to_string()));
100        self
101    }
102
103    pub fn get_attributes(&self) -> &[(&'static str, String)] {
104        &self.attributes
105    }
106}
107
108/// Trace context for distributed tracing
109#[derive(Debug, Clone)]
110pub struct TraceContext {
111    pub trace_id: String,
112    pub span_id: String,
113    pub parent_span_id: Option<String>,
114}
115
116impl TraceContext {
117    pub fn new() -> Self {
118        Self {
119            trace_id: uuid::Uuid::new_v4().to_string(),
120            span_id: uuid::Uuid::new_v4().to_string(),
121            parent_span_id: None,
122        }
123    }
124
125    pub fn with_parent(parent_span_id: String) -> Self {
126        Self {
127            trace_id: uuid::Uuid::new_v4().to_string(),
128            span_id: uuid::Uuid::new_v4().to_string(),
129            parent_span_id: Some(parent_span_id),
130        }
131    }
132
133    pub fn child_span(&self) -> Self {
134        Self {
135            trace_id: self.trace_id.clone(),
136            span_id: uuid::Uuid::new_v4().to_string(),
137            parent_span_id: Some(self.span_id.clone()),
138        }
139    }
140}
141
142/// Helper macro for creating instrumented spans
143#[macro_export]
144macro_rules! trace_span {
145    ($name:expr) => {
146        tracing::info_span!($name)
147    };
148    ($name:expr, $($key:tt = $value:expr),*) => {
149        tracing::info_span!($name, $($key = $value),*)
150    };
151}
152
153/// Helper macro for instrumented async functions
154#[macro_export]
155macro_rules! instrument_async {
156    ($name:expr, $future:expr) => {{
157        use tracing::Instrument;
158        let span = tracing::info_span!($name);
159        $future.instrument(span).await
160    }};
161}
162
163/// Agent tracing wrapper
164pub struct AgentTracer {
165    agent_name: String,
166    trace_context: TraceContext,
167}
168
169impl AgentTracer {
170    pub fn new(agent_name: impl ToString) -> Self {
171        Self {
172            agent_name: agent_name.to_string(),
173            trace_context: TraceContext::new(),
174        }
175    }
176
177    pub fn with_context(agent_name: impl ToString, trace_context: TraceContext) -> Self {
178        Self {
179            agent_name: agent_name.to_string(),
180            trace_context,
181        }
182    }
183
184    pub fn start_span(&self, operation: &str) -> TraceContext {
185        let span = self.trace_context.child_span();
186        tracing::info!(
187            agent = %self.agent_name,
188            operation = %operation,
189            trace_id = %span.trace_id,
190            span_id = %span.span_id,
191            parent_span_id = ?span.parent_span_id,
192            "Starting operation"
193        );
194        span
195    }
196
197    pub fn end_span(&self, span: &TraceContext, success: bool, duration_ms: f64) {
198        tracing::info!(
199            agent = %self.agent_name,
200            trace_id = %span.trace_id,
201            span_id = %span.span_id,
202            success = success,
203            duration_ms = duration_ms,
204            "Operation completed"
205        );
206    }
207
208    pub fn record_error(&self, span: &TraceContext, error: &str) {
209        tracing::error!(
210            agent = %self.agent_name,
211            trace_id = %span.trace_id,
212            span_id = %span.span_id,
213            error = %error,
214            "Operation failed"
215        );
216    }
217}
218
219/// Pipeline tracing helper
220pub struct PipelineTracer {
221    pipeline_id: String,
222    trace_context: TraceContext,
223}
224
225impl PipelineTracer {
226    pub fn new(pipeline_id: impl ToString) -> Self {
227        Self {
228            pipeline_id: pipeline_id.to_string(),
229            trace_context: TraceContext::new(),
230        }
231    }
232
233    pub fn trace_phase(&self, phase_name: &str) -> TraceContext {
234        let span = self.trace_context.child_span();
235        tracing::info!(
236            pipeline_id = %self.pipeline_id,
237            phase = %phase_name,
238            trace_id = %span.trace_id,
239            span_id = %span.span_id,
240            "Starting pipeline phase"
241        );
242        span
243    }
244
245    pub fn phase_completed(&self, span: &TraceContext, phase_name: &str, duration_ms: f64) {
246        tracing::info!(
247            pipeline_id = %self.pipeline_id,
248            phase = %phase_name,
249            trace_id = %span.trace_id,
250            span_id = %span.span_id,
251            duration_ms = duration_ms,
252            "Pipeline phase completed"
253        );
254    }
255
256    pub fn get_trace_id(&self) -> &str {
257        &self.trace_context.trace_id
258    }
259}
260
261/// Translation tracing helper
262pub struct TranslationTracer {
263    translation_id: String,
264    trace_context: TraceContext,
265}
266
267impl TranslationTracer {
268    pub fn new(translation_id: impl ToString) -> Self {
269        Self {
270            translation_id: translation_id.to_string(),
271            trace_context: TraceContext::new(),
272        }
273    }
274
275    pub fn trace_step(&self, step_name: &str, metadata: &[(&str, &str)]) -> TraceContext {
276        let span = self.trace_context.child_span();
277        tracing::info!(
278            translation_id = %self.translation_id,
279            step = %step_name,
280            trace_id = %span.trace_id,
281            span_id = %span.span_id,
282            ?metadata,
283            "Starting translation step"
284        );
285        span
286    }
287
288    pub fn step_completed(
289        &self,
290        span: &TraceContext,
291        step_name: &str,
292        lines_processed: usize,
293        duration_ms: f64,
294    ) {
295        tracing::info!(
296            translation_id = %self.translation_id,
297            step = %step_name,
298            trace_id = %span.trace_id,
299            span_id = %span.span_id,
300            lines_processed = lines_processed,
301            duration_ms = duration_ms,
302            "Translation step completed"
303        );
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_telemetry_config_default() {
313        let config = TelemetryConfig::default();
314        assert_eq!(config.service_name, "portalis");
315        assert!(!config.enable_jaeger);
316    }
317
318    #[test]
319    fn test_trace_context_creation() {
320        let context = TraceContext::new();
321        assert!(!context.trace_id.is_empty());
322        assert!(!context.span_id.is_empty());
323        assert!(context.parent_span_id.is_none());
324    }
325
326    #[test]
327    fn test_trace_context_child_span() {
328        let parent = TraceContext::new();
329        let child = parent.child_span();
330
331        assert_eq!(parent.trace_id, child.trace_id);
332        assert_ne!(parent.span_id, child.span_id);
333        assert_eq!(child.parent_span_id, Some(parent.span_id.clone()));
334    }
335
336    #[test]
337    fn test_agent_tracer() {
338        let tracer = AgentTracer::new("test-agent");
339        assert_eq!(tracer.agent_name, "test-agent");
340    }
341
342    #[test]
343    fn test_span_attributes() {
344        let attrs = SpanAttributes::new()
345            .add("key1", "value1")
346            .add("key2", 42);
347
348        assert_eq!(attrs.get_attributes().len(), 2);
349    }
350}