1use std::sync::OnceLock;
33use tracing_subscriber::{
34 fmt::{self, format::FmtSpan},
35 layer::SubscriberExt,
36 util::SubscriberInitExt,
37 EnvFilter, Layer,
38};
39
40#[derive(Debug, Clone, Copy, Default)]
42pub enum OutputFormat {
43 #[default]
45 Text,
46 Json,
48 Compact,
50}
51
52#[derive(Debug, Clone)]
54pub struct TracingConfig {
55 pub level: String,
57 pub format: OutputFormat,
59 pub with_thread_ids: bool,
61 pub with_thread_names: bool,
63 pub with_target: bool,
65 pub with_file: bool,
67 pub with_line_number: bool,
69 pub span_events: FmtSpan,
71 pub ansi: bool,
73 pub env_filter_override: Option<String>,
75}
76
77impl Default for TracingConfig {
78 fn default() -> Self {
79 Self {
80 level: "info".to_string(),
81 format: OutputFormat::default(),
82 with_thread_ids: false,
83 with_thread_names: false,
84 with_target: true,
85 with_file: false,
86 with_line_number: false,
87 span_events: FmtSpan::NONE,
88 ansi: true,
89 env_filter_override: None,
90 }
91 }
92}
93
94impl TracingConfig {
95 pub fn production() -> Self {
97 Self {
98 level: "info".to_string(),
99 format: OutputFormat::Json,
100 with_thread_ids: true,
101 with_thread_names: true,
102 with_target: true,
103 with_file: false,
104 with_line_number: false,
105 span_events: FmtSpan::CLOSE,
106 ansi: false,
107 env_filter_override: None,
108 }
109 }
110
111 pub fn development() -> Self {
113 Self {
114 level: "debug".to_string(),
115 format: OutputFormat::Text,
116 with_thread_ids: false,
117 with_thread_names: false,
118 with_target: true,
119 with_file: true,
120 with_line_number: true,
121 span_events: FmtSpan::NEW | FmtSpan::CLOSE,
122 ansi: true,
123 env_filter_override: None,
124 }
125 }
126
127 pub fn testing() -> Self {
129 Self {
130 level: "trace".to_string(),
131 format: OutputFormat::Compact,
132 with_thread_ids: false,
133 with_thread_names: false,
134 with_target: false,
135 with_file: false,
136 with_line_number: false,
137 span_events: FmtSpan::NONE,
138 ansi: false,
139 env_filter_override: None,
140 }
141 }
142}
143
144static TRACING_INITIALIZED: OnceLock<bool> = OnceLock::new();
145
146pub fn init_tracing(config: TracingConfig) {
170 if TRACING_INITIALIZED.get().is_some() {
172 return;
173 }
174
175 let env_filter = if let Some(ref filter) = config.env_filter_override {
177 EnvFilter::new(filter)
178 } else {
179 EnvFilter::try_from_default_env()
180 .unwrap_or_else(|_| EnvFilter::new(&config.level))
181 };
182
183 let result = TRACING_INITIALIZED.set(true);
185
186 if result.is_err() {
187 return;
189 }
190
191 match config.format {
192 OutputFormat::Json => {
193 let layer = fmt::layer()
194 .json()
195 .with_thread_ids(config.with_thread_ids)
196 .with_thread_names(config.with_thread_names)
197 .with_target(config.with_target)
198 .with_file(config.with_file)
199 .with_line_number(config.with_line_number)
200 .with_span_events(config.span_events)
201 .with_filter(env_filter);
202
203 if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
204 eprintln!("Failed to initialize tracing: {:?}", e);
205 }
206 }
207 OutputFormat::Text => {
208 let layer = fmt::layer()
209 .with_thread_ids(config.with_thread_ids)
210 .with_thread_names(config.with_thread_names)
211 .with_target(config.with_target)
212 .with_file(config.with_file)
213 .with_line_number(config.with_line_number)
214 .with_span_events(config.span_events)
215 .with_ansi(config.ansi)
216 .with_filter(env_filter);
217
218 if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
219 eprintln!("Failed to initialize tracing: {:?}", e);
220 }
221 }
222 OutputFormat::Compact => {
223 let layer = fmt::layer()
224 .compact()
225 .with_thread_ids(config.with_thread_ids)
226 .with_thread_names(config.with_thread_names)
227 .with_target(config.with_target)
228 .with_file(config.with_file)
229 .with_line_number(config.with_line_number)
230 .with_ansi(config.ansi)
231 .with_filter(env_filter);
232
233 if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
234 eprintln!("Failed to initialize tracing: {:?}", e);
235 }
236 }
237 }
238}
239
240pub fn init_default() {
244 init_tracing(TracingConfig::default());
245}
246
247pub fn is_initialized() -> bool {
249 TRACING_INITIALIZED.get().is_some()
250}
251
252use std::sync::atomic::{AtomicU64, Ordering};
257use uuid::Uuid;
258
259static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
260
261pub fn generate_request_id() -> String {
269 let uuid = Uuid::new_v4();
270 let uuid_prefix = &uuid.to_string().replace('-', "")[..8];
271 let counter = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
272 format!("{}-{:06}", uuid_prefix, counter)
273}
274
275pub fn generate_span_id() -> String {
277 let uuid = Uuid::new_v4();
278 uuid.to_string().replace('-', "")[..16].to_string()
279}
280
281#[macro_export]
287macro_rules! query_span {
288 ($request_id:expr) => {
289 tracing::info_span!(
290 "query",
291 request_id = %$request_id,
292 sdk.component = "query",
293 sdk.version = env!("CARGO_PKG_VERSION")
294 )
295 };
296}
297
298#[macro_export]
300macro_rules! transport_span {
301 ($operation:expr, $transport_type:expr) => {
302 tracing::debug_span!(
303 "transport",
304 operation = %$operation,
305 transport_type = %$transport_type,
306 sdk.component = "transport"
307 )
308 };
309}
310
311#[macro_export]
313macro_rules! skill_span {
314 ($skill_name:expr, $operation:expr) => {
315 tracing::info_span!(
316 "skill",
317 skill_name = %$skill_name,
318 operation = %$operation,
319 sdk.component = "skills"
320 )
321 };
322}
323
324#[macro_export]
326macro_rules! pool_span {
327 ($operation:expr) => {
328 tracing::debug_span!(
329 "connection_pool",
330 operation = %$operation,
331 sdk.component = "pool"
332 )
333 };
334}
335
336#[macro_export]
338macro_rules! mcp_span {
339 ($tool_name:expr, $operation:expr) => {
340 tracing::info_span!(
341 "mcp",
342 tool_name = %$tool_name,
343 operation = %$operation,
344 sdk.component = "mcp"
345 )
346 };
347}
348
349#[macro_export]
355macro_rules! log_error_with_category {
356 ($error:expr, $category:expr, $message:expr) => {
357 match $category {
358 $crate::errors::ErrorCategory::Network => {
359 tracing::error!(
360 error.category = "network",
361 error.code = %$error.error_code(),
362 error.retryable = $error.is_retryable(),
363 error.http_status = $error.http_status().code(),
364 message = %$message,
365 error = %$error
366 )
367 }
368 $crate::errors::ErrorCategory::Process => {
369 tracing::error!(
370 error.category = "process",
371 error.code = %$error.error_code(),
372 error.retryable = $error.is_retryable(),
373 message = %$message,
374 error = %$error
375 )
376 }
377 $crate::errors::ErrorCategory::Parsing => {
378 tracing::error!(
379 error.category = "parsing",
380 error.code = %$error.error_code(),
381 error.retryable = false,
382 message = %$message,
383 error = %$error
384 )
385 }
386 $crate::errors::ErrorCategory::Configuration => {
387 tracing::error!(
388 error.category = "configuration",
389 error.code = %$error.error_code(),
390 error.retryable = false,
391 message = %$message,
392 error = %$error
393 )
394 }
395 $crate::errors::ErrorCategory::Validation => {
396 tracing::error!(
397 error.category = "validation",
398 error.code = %$error.error_code(),
399 error.retryable = false,
400 message = %$message,
401 error = %$error
402 )
403 }
404 $crate::errors::ErrorCategory::Permission => {
405 tracing::error!(
406 error.category = "permission",
407 error.code = %$error.error_code(),
408 error.retryable = false,
409 error.http_status = $error.http_status().code(),
410 message = %$message,
411 error = %$error
412 )
413 }
414 $crate::errors::ErrorCategory::Resource => {
415 tracing::error!(
416 error.category = "resource",
417 error.code = %$error.error_code(),
418 error.retryable = $error.is_retryable(),
419 message = %$message,
420 error = %$error
421 )
422 }
423 $crate::errors::ErrorCategory::Internal => {
424 tracing::error!(
425 error.category = "internal",
426 error.code = %$error.error_code(),
427 error.retryable = false,
428 message = %$message,
429 error = %$error
430 )
431 }
432 $crate::errors::ErrorCategory::External => {
433 tracing::error!(
434 error.category = "external",
435 error.code = %$error.error_code(),
436 error.retryable = $error.is_retryable(),
437 message = %$message,
438 error = %$error
439 )
440 }
441 }
442 };
443}
444
445#[macro_export]
447macro_rules! log_retryable_error {
448 ($error:expr, $attempt:expr, $max_attempts:expr, $message:expr) => {
449 tracing::warn!(
450 error.category = ?$error.category(),
451 error.code = %$error.error_code(),
452 retry.attempt = $attempt,
453 retry.max_attempts = $max_attempts,
454 message = %$message,
455 error = %$error,
456 "Retryable error, will retry"
457 )
458 };
459}
460
461pub fn log_timing(operation: &str, duration_ms: u64, labels: &[(&str, &str)]) {
467 let labels_str = labels
468 .iter()
469 .map(|(k, v)| format!("{}={}", k, v))
470 .collect::<Vec<_>>()
471 .join(",");
472
473 tracing::info!(
474 metric.name = operation,
475 metric.kind = "timing",
476 metric.value_ms = duration_ms,
477 metric.labels = %labels_str,
478 "Operation completed"
479 );
480}
481
482pub fn log_counter(name: &str, increment: u64, labels: &[(&str, &str)]) {
484 let labels_str = labels
485 .iter()
486 .map(|(k, v)| format!("{}={}", k, v))
487 .collect::<Vec<_>>()
488 .join(",");
489
490 tracing::debug!(
491 metric.name = name,
492 metric.kind = "counter",
493 metric.increment = increment,
494 metric.labels = %labels_str,
495 "Counter incremented"
496 );
497}
498
499pub fn log_gauge(name: &str, value: f64, labels: &[(&str, &str)]) {
501 let labels_str = labels
502 .iter()
503 .map(|(k, v)| format!("{}={}", k, v))
504 .collect::<Vec<_>>()
505 .join(",");
506
507 tracing::debug!(
508 metric.name = name,
509 metric.kind = "gauge",
510 metric.value = value,
511 metric.labels = %labels_str,
512 "Gauge recorded"
513 );
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_generate_request_id() {
522 let id1 = generate_request_id();
523 let id2 = generate_request_id();
524
525 assert_ne!(id1, id2);
527
528 assert_eq!(id1.len(), 15); assert!(id1.contains('-'));
531
532 let parts: Vec<&str> = id1.split('-').collect();
533 assert_eq!(parts.len(), 2);
534 assert_eq!(parts[0].len(), 8);
535 assert_eq!(parts[1].len(), 6);
536 }
537
538 #[test]
539 fn test_generate_span_id() {
540 let span_id = generate_span_id();
541 assert_eq!(span_id.len(), 16);
542 assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
543 }
544
545 #[test]
546 fn test_tracing_config_defaults() {
547 let config = TracingConfig::default();
548 assert_eq!(config.level, "info");
549 assert!(matches!(config.format, OutputFormat::Text));
550 assert!(config.with_target);
551 assert!(!config.with_file);
552 }
553
554 #[test]
555 fn test_tracing_config_production() {
556 let config = TracingConfig::production();
557 assert_eq!(config.level, "info");
558 assert!(matches!(config.format, OutputFormat::Json));
559 assert!(config.with_thread_ids);
560 assert!(!config.ansi);
561 }
562
563 #[test]
564 fn test_tracing_config_development() {
565 let config = TracingConfig::development();
566 assert_eq!(config.level, "debug");
567 assert!(matches!(config.format, OutputFormat::Text));
568 assert!(config.ansi);
569 assert!(config.with_file);
570 }
571
572 #[test]
573 fn test_is_initialized_before_init() {
574 let _ = is_initialized();
577 }
578
579 #[test]
580 fn test_log_timing() {
581 log_timing("test_operation", 100, &[("key", "value")]);
584 }
585
586 #[test]
587 fn test_log_counter() {
588 log_counter("test_counter", 1, &[("endpoint", "/api/test")]);
589 }
590
591 #[test]
592 fn test_log_gauge() {
593 log_gauge("test_gauge", 42.5, &[("location", "room1")]);
594 }
595}