1use once_cell::sync::OnceCell;
5use std::{
6 path::{Path, PathBuf},
7 sync::RwLock,
8};
9use tracing::{self, event};
10use tracing_appender::rolling::{RollingFileAppender, RollingWriter, Rotation};
11use tracing_subscriber::{
12 Registry,
13 filter::Filtered,
14 fmt::{
15 Layer,
16 format::{DefaultFields, Format},
17 },
18 layer::Layered,
19};
20
21use tracing_subscriber::{
22 self,
23 filter::{self, LevelFilter},
24 prelude::*,
25 reload::{self, Handle},
26};
27
28use std::str::FromStr;
29
30type InnerFiltered = Filtered<Layer<Registry>, LevelFilter, Registry>;
32type InnerLayered = Layered<reload::Layer<InnerFiltered, Registry>, Registry>;
34type FileReload = Handle<
36 Filtered<
37 Layer<InnerLayered, DefaultFields, Format, LazyRollingFileAppender>,
38 LevelFilter,
39 InnerLayered,
40 >,
41 InnerLayered,
42>;
43
44pub struct Reloads {
45 console_reload: RwLock<reload::Handle<InnerFiltered, Registry>>,
46 file_reload: RwLock<FileReload>,
47}
48
49pub struct InitiateOnce {
50 init_once: OnceCell<Reloads>,
51}
52
53pub static INITIATE_ONCE: InitiateOnce = InitiateOnce {
54 init_once: OnceCell::new(),
55};
56
57const FILE_DIRECTORY: &str = "glide-logs";
58const ENV_GLIDE_LOG_DIR: &str = "GLIDE_LOG_DIR";
59
60struct LazyRollingFileAppender {
64 file_appender: OnceCell<RollingFileAppender>,
65 rotation: Rotation,
66 directory: PathBuf,
67 filename_prefix: PathBuf,
68}
69
70impl LazyRollingFileAppender {
71 fn new(
72 rotation: Rotation,
73 directory: impl AsRef<Path>,
74 filename_prefix: impl AsRef<Path>,
75 ) -> LazyRollingFileAppender {
76 LazyRollingFileAppender {
77 file_appender: OnceCell::new(),
78 rotation,
79 directory: directory.as_ref().to_path_buf(),
80 filename_prefix: filename_prefix.as_ref().to_path_buf(),
81 }
82 }
83}
84
85impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for LazyRollingFileAppender {
86 type Writer = RollingWriter<'a>;
87 fn make_writer(&'a self) -> Self::Writer {
88 let file_appender = self.file_appender.get_or_init(|| {
89 RollingFileAppender::new(
90 self.rotation.clone(),
91 self.directory.clone(),
92 self.filename_prefix.clone(),
93 )
94 });
95 file_appender.make_writer()
96 }
97}
98
99#[derive(Debug)]
100pub enum Level {
101 Error = 0,
102 Warn = 1,
103 Info = 2,
104 Debug = 3,
105 Trace = 4,
106 Off = 5,
107}
108impl Level {
109 fn to_filter(&self) -> filter::LevelFilter {
110 match self {
111 Level::Trace => LevelFilter::TRACE,
112 Level::Debug => LevelFilter::DEBUG,
113 Level::Info => LevelFilter::INFO,
114 Level::Warn => LevelFilter::WARN,
115 Level::Error => LevelFilter::ERROR,
116 Level::Off => LevelFilter::OFF,
117 }
118 }
119}
120
121pub fn create_directory_from_env(envname: &str) -> Option<String> {
125 let Ok(dirpath) = std::env::var(envname) else {
126 return None;
127 };
128
129 if dirpath.trim().is_empty() || std::fs::create_dir_all(&dirpath).is_err() {
130 return None;
131 }
132
133 Some(dirpath)
134}
135
136pub fn init(minimal_level: Option<Level>, file_name: Option<&str>) -> Level {
141 let level = minimal_level.unwrap_or(Level::Warn);
142 let level_filter = level.to_filter();
143 let reloads = INITIATE_ONCE.init_once.get_or_init(|| {
144 let stdout_fmt = tracing_subscriber::fmt::layer()
145 .with_ansi(true)
146 .with_filter(LevelFilter::OFF);
147
148 let (stdout_layer, stdout_reload) = reload::Layer::new(stdout_fmt);
149
150 let logs_dir =
152 create_directory_from_env(ENV_GLIDE_LOG_DIR).unwrap_or(FILE_DIRECTORY.to_string());
153 let file_appender = LazyRollingFileAppender::new(
154 Rotation::HOURLY,
155 logs_dir,
156 file_name.unwrap_or("output.log"),
157 );
158
159 let file_fmt = tracing_subscriber::fmt::layer()
160 .with_writer(file_appender)
161 .with_filter(LevelFilter::OFF);
162 let (file_layer, file_reload) = reload::Layer::new(file_fmt);
163
164 let log_level = if let Ok(level) = std::env::var("RUST_LOG") {
166 let trace_level = tracing::Level::from_str(&level).unwrap_or(tracing::Level::TRACE);
167 LevelFilter::from(trace_level)
168 } else {
169 LevelFilter::TRACE
170 };
171
172 let targets_filter = filter::Targets::new()
174 .with_target("glide", log_level)
175 .with_target("redis", log_level)
176 .with_target("logger_core", log_level)
177 .with_target(std::env!("CARGO_PKG_NAME"), log_level);
178
179 tracing_subscriber::registry()
183 .with(stdout_layer)
184 .with(file_layer)
185 .with(targets_filter)
186 .try_init()
187 .ok(); let reloads: Reloads = Reloads {
190 console_reload: RwLock::new(stdout_reload),
191 file_reload: RwLock::new(file_reload),
192 };
193 reloads
194 });
195
196 match file_name {
197 None => {
198 let _ = reloads
199 .console_reload
200 .write()
201 .expect("error reloading stdout")
202 .modify(|layer| *layer.filter_mut() = level_filter);
203 let _ = reloads
204 .file_reload
205 .write()
206 .expect("error reloading file appender")
207 .modify(|layer| {
208 *layer.filter_mut() = LevelFilter::OFF;
209 });
210 }
211 Some(file) => {
212 let logs_dir =
214 create_directory_from_env(ENV_GLIDE_LOG_DIR).unwrap_or(FILE_DIRECTORY.to_string());
215 let file_appender = LazyRollingFileAppender::new(Rotation::HOURLY, logs_dir, file);
216 let _ = reloads
217 .file_reload
218 .write()
219 .expect("error reloading file appender")
220 .modify(|layer| {
221 *layer.filter_mut() = level_filter;
222 *layer.inner_mut().writer_mut() = file_appender;
223 });
224 let _ = reloads
225 .console_reload
226 .write()
227 .expect("error reloading stdout")
228 .modify(|layer| *layer.filter_mut() = LevelFilter::OFF);
229 }
230 };
231 level
232}
233
234macro_rules! create_log {
235 ($name:ident, $uppercase_level:tt) => {
236 pub fn $name<Message: AsRef<str>, Identifier: AsRef<str>>(
237 log_identifier: Identifier,
238 message: Message,
239 ) {
240 if INITIATE_ONCE.init_once.get().is_none() {
241 init(Some(Level::Warn), None);
242 };
243 let message_ref = message.as_ref();
244 let identifier_ref = log_identifier.as_ref();
245 event!(
246 tracing::Level::$uppercase_level,
247 "{identifier_ref} - {message_ref}"
248 )
249 }
250 };
251}
252
253create_log!(log_trace, TRACE);
254create_log!(log_debug, DEBUG);
255create_log!(log_info, INFO);
256create_log!(log_warn, WARN);
257create_log!(log_error, ERROR);
258
259#[macro_export]
268macro_rules! log_trace_lazy {
269 ($identifier:expr, $message:expr) => {
270 if tracing::event_enabled!(tracing::Level::TRACE) {
271 $crate::log_trace($identifier, $message);
272 }
273 };
274}
275
276#[macro_export]
277macro_rules! log_debug_lazy {
278 ($identifier:expr, $message:expr) => {
279 if tracing::event_enabled!(tracing::Level::DEBUG) {
280 $crate::log_debug($identifier, $message);
281 }
282 };
283}
284
285#[macro_export]
286macro_rules! log_info_lazy {
287 ($identifier:expr, $message:expr) => {
288 if tracing::event_enabled!(tracing::Level::INFO) {
289 $crate::log_info($identifier, $message);
290 }
291 };
292}
293
294#[macro_export]
295macro_rules! log_warn_lazy {
296 ($identifier:expr, $message:expr) => {
297 if tracing::event_enabled!(tracing::Level::WARN) {
298 $crate::log_warn($identifier, $message);
299 }
300 };
301}
302
303#[macro_export]
304macro_rules! log_error_lazy {
305 ($identifier:expr, $message:expr) => {
306 if tracing::event_enabled!(tracing::Level::ERROR) {
307 $crate::log_error($identifier, $message);
308 }
309 };
310}
311
312#[macro_export]
320macro_rules! log_warn_rate_limited {
321 ($identifier:expr, $interval_secs:expr, $message:expr) => {{
322 static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
323 let now = std::time::SystemTime::now()
324 .duration_since(std::time::UNIX_EPOCH)
325 .unwrap_or_default()
326 .as_secs();
327 let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
328 if now >= last + $interval_secs {
329 LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
330 $crate::log_warn($identifier, $message);
331 }
332 }};
333}
334
335#[macro_export]
336macro_rules! log_info_rate_limited {
337 ($identifier:expr, $interval_secs:expr, $message:expr) => {{
338 static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
339 let now = std::time::SystemTime::now()
340 .duration_since(std::time::UNIX_EPOCH)
341 .unwrap_or_default()
342 .as_secs();
343 let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
344 if now >= last + $interval_secs {
345 LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
346 $crate::log_info($identifier, $message);
347 }
348 }};
349}
350
351#[macro_export]
352macro_rules! log_debug_rate_limited {
353 ($identifier:expr, $interval_secs:expr, $message:expr) => {{
354 static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
355 let now = std::time::SystemTime::now()
356 .duration_since(std::time::UNIX_EPOCH)
357 .unwrap_or_default()
358 .as_secs();
359 let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
360 if now >= last + $interval_secs {
361 LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
362 $crate::log_debug($identifier, $message);
363 }
364 }};
365}
366
367pub fn log<Message: AsRef<str>, Identifier: AsRef<str>>(
372 log_level: Level,
373 log_identifier: Identifier,
374 message: Message,
375) {
376 match log_level {
377 Level::Debug => log_debug(log_identifier, message),
378 Level::Trace => log_trace(log_identifier, message),
379 Level::Info => log_info(log_identifier, message),
380 Level::Warn => log_warn(log_identifier, message),
381 Level::Error => log_error(log_identifier, message),
382 Level::Off => (),
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_directory_from_env() {
392 let dir_path = format!("{}/glide-logs", std::env::temp_dir().display());
393 let _ = std::fs::remove_dir_all(&dir_path);
396 assert!(std::fs::create_dir_all(&dir_path).is_ok());
398
399 unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, &dir_path) };
400 assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_some());
401 assert!(std::fs::metadata(&dir_path).is_ok());
402
403 let _ = std::fs::remove_dir_all(&dir_path);
405
406 assert!(std::fs::create_dir_all(&dir_path).is_ok());
408 assert!(std::fs::metadata(&dir_path).is_ok());
409
410 unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, &dir_path) };
411 assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_some());
412
413 let _ = std::fs::remove_dir_all(&dir_path);
415
416 unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, "") };
418 assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_none());
419 }
420}