1use tracing::{info, warn, Level};
12use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, filter::EnvFilter};
13
14#[derive(Debug, Clone)]
16pub struct TelemetryConfig {
17 pub service_name: String,
19
20 pub service_version: String,
22
23 pub environment: String,
25
26 pub enable_jaeger: bool,
28
29 pub jaeger_endpoint: Option<String>,
31
32 pub enable_console: bool,
34
35 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
53pub fn init_telemetry(config: TelemetryConfig) -> Result<(), Box<dyn std::error::Error>> {
55 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 let subscriber = tracing_subscriber::registry()
62 .with(env_filter)
63 .with(fmt::layer().with_target(true).with_thread_ids(true));
64
65 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
86pub 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#[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#[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#[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
163pub 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
219pub 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
261pub 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}