1use std::path::PathBuf;
18use tracing_subscriber::{
19 filter::EnvFilter,
20 fmt::{self, time::LocalTime},
21 layer::SubscriberExt,
22 util::SubscriberInitExt,
23 Layer,
24};
25
26pub fn log_dir() -> PathBuf {
28 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
29 PathBuf::from(home).join(".mlstack").join("logs")
30}
31
32pub fn init_logging(context: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
48 let log_path = log_dir();
49 if let Err(e) = std::fs::create_dir_all(&log_path) {
50 eprintln!(
51 "[WARN] Could not create log directory {}: {}",
52 log_path.display(),
53 e
54 );
55 let filter = EnvFilter::try_from_default_env()
57 .unwrap_or_else(|_| EnvFilter::new("rusty_stack=info,warn"));
58 let stdout_layer = fmt::layer().with_target(false).with_filter(filter);
59 tracing_subscriber::registry().with(stdout_layer).init();
60 return None;
61 }
62
63 let file_appender =
65 tracing_appender::rolling::daily(&log_path, format!("{}-rusty-stack", context));
66 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
67
68 let file_layer = fmt::layer()
70 .json()
71 .with_timer(LocalTime::rfc_3339())
72 .with_writer(non_blocking)
73 .with_target(true)
74 .with_span_events(fmt::format::FmtSpan::CLOSE)
75 .with_filter(
76 EnvFilter::try_from_default_env()
77 .unwrap_or_else(|_| EnvFilter::new("rusty_stack=debug,info")),
78 );
79
80 let stdout_layer = fmt::layer()
82 .with_target(false)
83 .with_timer(fmt::time::LocalTime::new(
84 time::format_description::well_known::iso8601::Iso8601::DEFAULT,
85 ))
86 .with_filter(
87 EnvFilter::try_from_default_env()
88 .unwrap_or_else(|_| EnvFilter::new("rusty_stack=info,warn")),
89 );
90
91 tracing_subscriber::registry()
92 .with(file_layer)
93 .with(stdout_layer)
94 .init();
95
96 tracing::info!(
97 context = context,
98 log_dir = %log_path.display(),
99 "Logging initialized"
100 );
101
102 Some(guard)
103}
104
105pub fn init_batch_logging(context: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
110 let log_path = log_dir();
111 if let Err(e) = std::fs::create_dir_all(&log_path) {
112 eprintln!(
113 "[WARN] Could not create log directory {}: {}",
114 log_path.display(),
115 e
116 );
117 return None;
118 }
119
120 let file_appender =
121 tracing_appender::rolling::daily(&log_path, format!("{}-rusty-stack", context));
122 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
123
124 let file_layer = fmt::layer()
126 .json()
127 .with_timer(LocalTime::rfc_3339())
128 .with_writer(non_blocking)
129 .with_target(true)
130 .with_span_events(fmt::format::FmtSpan::CLOSE)
131 .with_filter(
132 EnvFilter::try_from_default_env()
133 .unwrap_or_else(|_| EnvFilter::new("rusty_stack=debug")),
134 );
135
136 let stderr_layer = fmt::layer()
138 .with_target(false)
139 .with_writer(std::io::stderr)
140 .with_filter(
141 EnvFilter::try_from_default_env()
142 .unwrap_or_else(|_| EnvFilter::new("rusty_stack=warn")),
143 );
144
145 tracing_subscriber::registry()
146 .with(file_layer)
147 .with(stderr_layer)
148 .init();
149
150 tracing::info!(
151 context = context,
152 log_dir = %log_path.display(),
153 "Batch logging initialized"
154 );
155
156 Some(guard)
157}
158
159pub fn log_file_path(context: &str) -> PathBuf {
163 log_dir().join(format!("{}-rusty-stack", context))
164}
165
166mod time {
168 pub use time::format_description;
169}
170
171#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_log_dir_is_under_mlstack() {
181 let dir = log_dir();
182 assert!(dir.to_string_lossy().contains(".mlstack"));
183 assert!(dir.to_string_lossy().contains("logs"));
184 }
185
186 #[test]
187 fn test_log_file_path_contains_context() {
188 let path = log_file_path("update");
189 assert!(path.to_string_lossy().contains("update-rusty-stack"));
190 }
191}