ferrite_logging/
lib.rs

1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4use tracing::{instrument, Level};
5use tracing_appender::non_blocking::WorkerGuard;
6use tracing_appender::rolling;
7use tracing_subscriber::{
8    fmt::{self, format::FmtSpan},
9    layer::SubscriberExt,
10    util::SubscriberInitExt,
11    EnvFilter,
12    Layer, // Registry is implicitly used by .with() chain
13};
14
15pub mod metrics;
16pub use metrics::PerformanceMetrics;
17
18#[derive(Debug, Clone, Copy)]
19pub enum LogLevel {
20    Trace,
21    Debug,
22    Info,
23    Warn,
24    Error,
25}
26impl FromStr for LogLevel {
27    type Err = String;
28
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s.to_lowercase().as_str() {
31            "trace" => Ok(LogLevel::Trace),
32            "debug" => Ok(LogLevel::Debug),
33            "info" => Ok(LogLevel::Info),
34            "warn" => Ok(LogLevel::Warn),
35            "error" => Ok(LogLevel::Error),
36            _ => Err(format!("Invalid log level: {}", s)),
37        }
38    }
39}
40
41impl From<LogLevel> for Level {
42    fn from(level: LogLevel) -> Self {
43        match level {
44            LogLevel::Trace => Level::TRACE,
45            LogLevel::Debug => Level::DEBUG,
46            LogLevel::Info => Level::INFO,
47            LogLevel::Warn => Level::WARN,
48            LogLevel::Error => Level::ERROR,
49        }
50    }
51}
52
53#[derive(Debug)]
54pub struct LogConfig {
55    pub level: LogLevel,
56    pub enable_tracy: bool,
57    pub log_spans: bool,
58    pub file_path: Option<PathBuf>,
59}
60
61impl Default for LogConfig {
62    fn default() -> Self {
63        Self {
64            level: LogLevel::Info,
65            enable_tracy: false,
66            log_spans: true,
67            file_path: None,
68        }
69    }
70}
71
72#[instrument(skip(config), fields(level = ?config.level, tracy = config.enable_tracy, file = ?config.file_path, spans = config.log_spans))]
73pub fn init(config: LogConfig) -> Option<WorkerGuard> {
74    let base_level = Level::from(config.level);
75
76    // Helper closure to create EnvFilter instances
77    // This ensures RUST_LOG is read and parsed for each layer,
78    // which is fine for startup.
79    let create_env_filter = || {
80        EnvFilter::builder()
81            .with_default_directive(base_level.into())
82            .from_env_lossy()
83    };
84
85    let span_events =
86        if config.log_spans { FmtSpan::CLOSE } else { FmtSpan::NONE };
87
88    // Console Layer
89    let console_fmt_layer = fmt::layer()
90        .with_ansi(true)
91        .with_target(true)
92        .with_file(true)
93        .with_line_number(true)
94        .with_thread_ids(true)
95        .with_timer(fmt::time::ChronoUtc::rfc_3339())
96        .with_span_events(span_events.clone())
97        .with_filter(create_env_filter()); // Use a new filter instance
98
99    // File Layer (optional)
100    let mut file_guard: Option<WorkerGuard> = None;
101    let file_fmt_layer_maybe = if let Some(log_path) = &config.file_path {
102        let parent_dir = log_path
103            .parent()
104            .unwrap_or_else(|| Path::new("."));
105
106        if !parent_dir.as_os_str().is_empty() && !parent_dir.exists() {
107            if let Err(e) = std::fs::create_dir_all(parent_dir) {
108                eprintln!(
109                    "Error: Failed to create log directory {}: {}. File logging will be disabled.",
110                    parent_dir.display(),
111                    e
112                );
113                None
114            } else {
115                let file_name = log_path
116                    .file_name()
117                    .unwrap_or_else(|| OsStr::new("ferrite.log"));
118                let file_appender = rolling::never(parent_dir, file_name);
119                let (non_blocking_writer, guard) =
120                    tracing_appender::non_blocking(file_appender);
121                file_guard = Some(guard);
122
123                Some(
124                    fmt::layer()
125                        .with_ansi(false)
126                        .with_writer(non_blocking_writer)
127                        .with_target(true)
128                        .with_file(true)
129                        .with_line_number(true)
130                        .with_thread_ids(true)
131                        .with_timer(fmt::time::ChronoUtc::rfc_3339())
132                        .with_span_events(span_events.clone())
133                        .with_filter(create_env_filter()), // Use a new filter instance
134                )
135            }
136        } else {
137            let file_name = log_path
138                .file_name()
139                .unwrap_or_else(|| OsStr::new("ferrite.log"));
140            let effective_parent_dir = if parent_dir.as_os_str().is_empty() {
141                Path::new(".")
142            } else {
143                parent_dir
144            };
145            let file_appender = rolling::never(effective_parent_dir, file_name);
146            let (non_blocking_writer, guard) =
147                tracing_appender::non_blocking(file_appender);
148            file_guard = Some(guard);
149
150            Some(
151                fmt::layer()
152                    .with_ansi(false)
153                    .with_writer(non_blocking_writer)
154                    .with_target(true)
155                    .with_file(true)
156                    .with_line_number(true)
157                    .with_thread_ids(true)
158                    .with_timer(fmt::time::ChronoUtc::rfc_3339())
159                    .with_span_events(span_events)
160                    .with_filter(create_env_filter()), // Use a new filter instance
161            )
162        }
163    } else {
164        None
165    };
166
167    // Tracy Layer (optional)
168    let tracy_layer_maybe = if config.enable_tracy {
169        Some(
170            tracing_tracy::TracyLayer::default()
171                .with_filter(create_env_filter()),
172        ) // Use a new filter instance
173    } else {
174        None
175    };
176
177    tracing_subscriber::registry()
178        .with(console_fmt_layer)
179        .with(file_fmt_layer_maybe)
180        .with(tracy_layer_maybe)
181        .try_init()
182        .expect("Failed to initialize logging subscriber");
183
184    if config.enable_tracy {
185        tracy_client::frame_mark();
186    }
187
188    file_guard
189}