ceylon_observability/
logging.rs

1use std::fmt::Debug;
2use std::path::PathBuf;
3use tracing::info;
4use tracing_subscriber::{fmt, prelude::*, EnvFilter};
5use uuid::Uuid;
6
7/// Configuration for the logging system
8#[derive(Debug, Clone)]
9pub struct LoggingConfig {
10    pub log_level: String,
11    pub log_file_path: Option<PathBuf>,
12    pub json_output: bool,
13}
14
15impl Default for LoggingConfig {
16    fn default() -> Self {
17        Self {
18            log_level: "info".to_string(),
19            log_file_path: None,
20            json_output: false,
21        }
22    }
23}
24
25/// Guards to keep file appenders alive
26pub struct LoggingGuards {
27    pub _file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
28}
29
30/// Initialize the logging system
31pub fn init_logging(config: &LoggingConfig) -> LoggingGuards {
32    let env_filter =
33        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
34
35    let registry = tracing_subscriber::registry().with(env_filter);
36
37    let mut file_guard = None;
38    let file_writer = if let Some(path) = &config.log_file_path {
39        let file_appender = tracing_appender::rolling::daily(
40            path.parent().unwrap_or(&PathBuf::from(".")),
41            path.file_name().unwrap_or("ceylon.log".as_ref()),
42        );
43        let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
44        file_guard = Some(guard);
45        Some(non_blocking)
46    } else {
47        None
48    };
49
50    let fmt_layer = fmt::layer()
51        .with_target(true)
52        .with_thread_ids(true)
53        .with_level(true);
54
55    if config.json_output {
56        let stdout_layer = fmt_layer.json();
57        if let Some(writer) = file_writer {
58            let file_layer = fmt::layer().with_ansi(false).with_writer(writer).json();
59            if let Err(e) = registry.with(stdout_layer).with(file_layer).try_init() {
60                eprintln!("Global logging already initialized: {}", e);
61            }
62        } else {
63            if let Err(e) = registry.with(stdout_layer).try_init() {
64                eprintln!("Global logging already initialized: {}", e);
65            }
66        }
67    } else {
68        let stdout_layer = fmt_layer.compact();
69        if let Some(writer) = file_writer {
70            let file_layer = fmt::layer().with_ansi(false).with_writer(writer).json();
71            if let Err(e) = registry.with(stdout_layer).with(file_layer).try_init() {
72                eprintln!("Global logging already initialized: {}", e);
73            }
74        } else {
75            if let Err(e) = registry.with(stdout_layer).try_init() {
76                eprintln!("Global logging already initialized: {}", e);
77            }
78        }
79    }
80
81    info!("Logging initialized");
82
83    LoggingGuards {
84        _file_guard: file_guard,
85    }
86}
87
88/// Correlation ID for tracking requests across boundaries
89#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct CorrelationId(String);
91
92impl CorrelationId {
93    pub fn new() -> Self {
94        Self(Uuid::new_v4().to_string())
95    }
96
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100}
101
102impl Default for CorrelationId {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl std::fmt::Display for CorrelationId {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}", self.0)
111    }
112}