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, };
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 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 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()); 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()), )
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()), )
162 }
163 } else {
164 None
165 };
166
167 let tracy_layer_maybe = if config.enable_tracy {
169 Some(
170 tracing_tracy::TracyLayer::default()
171 .with_filter(create_env_filter()),
172 ) } 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}