Skip to main content

starla_common/
logging.rs

1//! Logging infrastructure for Starla
2//!
3//! Provides flexible logging with:
4//! - stdout output by default (journalctl-friendly)
5//! - Optional file output with rotation
6//! - Feature-gated JSON structured logging
7//! - Environment-based log level configuration
8
9use std::path::PathBuf;
10pub use tracing_subscriber::{fmt, prelude::*, EnvFilter};
11
12/// Logging configuration
13#[derive(Debug, Clone)]
14pub struct LogConfig {
15    /// Optional log directory for file output
16    pub log_dir: Option<PathBuf>,
17
18    /// Log level (trace, debug, info, warn, error)
19    pub level: String,
20
21    /// Whether to use JSON format
22    pub json_format: bool,
23}
24
25impl Default for LogConfig {
26    fn default() -> Self {
27        Self {
28            log_dir: None,
29            level: "info".to_string(),
30            json_format: cfg!(feature = "json"),
31        }
32    }
33}
34
35/// Initialize logging subsystem
36///
37/// # Arguments
38///
39/// * `config` - Logging configuration
40///
41/// # Examples
42///
43/// ```no_run
44/// use starla_common::logging::{init_logging, LogConfig};
45///
46/// // Default: stdout with info level
47/// init_logging(LogConfig::default()).unwrap();
48///
49/// // With custom log directory
50/// let config = LogConfig {
51///     log_dir: Some("/var/log/starla".into()),
52///     level: "debug".to_string(),
53///     json_format: false,
54/// };
55/// init_logging(config).unwrap();
56/// ```
57pub fn init_logging(config: LogConfig) -> anyhow::Result<()> {
58    // Build filter from config and RUST_LOG env var
59    let filter =
60        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
61
62    match config.log_dir {
63        None => init_stdout_logging(filter, config.json_format),
64        Some(dir) => init_file_logging(dir, filter, config.json_format),
65    }
66}
67
68/// Initialize stdout logging (default)
69#[allow(unused_variables)]
70fn init_stdout_logging(filter: EnvFilter, json_format: bool) -> anyhow::Result<()> {
71    #[cfg(feature = "json")]
72    {
73        if json_format {
74            tracing_subscriber::registry()
75                .with(filter)
76                .with(
77                    fmt::layer()
78                        .json()
79                        .with_target(true)
80                        .with_current_span(true)
81                        .with_span_list(false)
82                        .with_thread_ids(true)
83                        .with_thread_names(true),
84                )
85                .init();
86            return Ok(());
87        }
88    }
89
90    // Default text format
91    tracing_subscriber::registry()
92        .with(filter)
93        .with(
94            fmt::layer()
95                .compact()
96                .with_target(true)
97                .with_thread_ids(false),
98        )
99        .init();
100
101    Ok(())
102}
103
104/// Initialize file-based logging with rotation
105#[allow(unused_variables)]
106fn init_file_logging(log_dir: PathBuf, filter: EnvFilter, json_format: bool) -> anyhow::Result<()> {
107    // Ensure log directory exists
108    std::fs::create_dir_all(&log_dir)?;
109
110    // Create daily rolling file appender
111    let file_appender = tracing_appender::rolling::daily(log_dir, "probe.log");
112    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
113
114    #[cfg(feature = "json")]
115    {
116        if json_format {
117            tracing_subscriber::registry()
118                .with(filter)
119                .with(
120                    fmt::layer()
121                        .json()
122                        .with_target(true)
123                        .with_current_span(true)
124                        .with_span_list(false)
125                        .with_thread_ids(true)
126                        .with_thread_names(true)
127                        .with_writer(non_blocking),
128                )
129                .init();
130            return Ok(());
131        }
132    }
133
134    // Default text format for files
135    tracing_subscriber::registry()
136        .with(filter)
137        .with(
138            fmt::layer()
139                .with_target(true)
140                .with_thread_ids(true)
141                .with_ansi(false) // No ANSI colors in log files
142                .with_writer(non_blocking),
143        )
144        .init();
145
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_default_config() {
155        let config = LogConfig::default();
156        assert_eq!(config.level, "info");
157        assert!(config.log_dir.is_none());
158    }
159}