Skip to main content

logger_core/
lib.rs

1/**
2 * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
3 */
4use 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
30// Layer-Filter pair determines whether a log will be collected
31type InnerFiltered = Filtered<Layer<Registry>, LevelFilter, Registry>;
32// A Reloadable pair of layer-filter
33type InnerLayered = Layered<reload::Layer<InnerFiltered, Registry>, Registry>;
34// A reloadable layer of subscriber to a rolling file
35type 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
60/// Wraps [RollingFileAppender] to defer initialization until logging is required,
61/// allowing [init] to disable file logging on read-only filesystems.
62/// This is needed because [RollingFileAppender] tries to create the log directory on initialization.
63struct 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
121/// Attempt to read a directory path from an environment variable. If the environment variable `envname` exists
122/// and contains a valid path - this function will create and return that path. In any case of failure,
123/// this method returns `None` (e.g. the environment variable exists but contains an empty path etc)
124pub 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
136// Initialize the global logger to error level on the first call only
137// In any of the calls to the function, including the first - resetting the existence loggers to the new setting
138// provided by using the global reloadable handle
139// The logger will save only logs of the given level or above.
140pub 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        // Check if the environment variable GLIDE_LOG is set
151        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        // If user has set the environment variable "RUST_LOG" with a valid log verbosity, use it
165        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        // Enable logging only from allowed crates
173        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        // Use try_init() instead of init() to gracefully handle the case where
180        // a tracing subscriber has already been set by the application.
181        // This allows applications to control their own logging configuration.
182        tracing_subscriber::registry()
183            .with(stdout_layer)
184            .with(file_layer)
185            .with(targets_filter)
186            .try_init()
187            .ok(); // Ignore the error if subscriber already set
188
189        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            // Check if the environment variable GLIDE_LOG is set
213            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/// Lazy logging macros that only evaluate the message expression if the log level is enabled.
260/// This avoids the cost of `format!(...)` when the level is disabled.
261///
262/// Usage:
263/// ```ignore
264/// log_trace_lazy!("identifier", format!("expensive computation: {}", value));
265/// log_warn_lazy!("identifier", format!("something happened: {:?}", err));
266/// ```
267#[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/// Rate-limited logging macro. Logs at most once per `interval_secs` seconds.
313/// Uses a static AtomicU64 per call site to track the last log time.
314///
315/// Usage:
316/// ```ignore
317/// log_warn_rate_limited!("identifier", 10, format!("something happened: {}", val));
318/// ```
319#[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
367// Logs the given log, with log_identifier and log level prefixed. If the given log level is below the threshold of given when the logger was initialized, the log will be ignored.
368// log_identifier should be used to add context to a log, and make it easier to connect it to other relevant logs. For example, it can be used to pass a task identifier.
369// If this is called before a logger was initialized the log will not be registered.
370// If logger doesn't exist, create the default
371pub 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        // Case 1: try to create an already existing folder
394        // make sure we are starting fresh
395        let _ = std::fs::remove_dir_all(&dir_path);
396        // Create the directory
397        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        // Case 2: try to create a new folder (i.e. the folder does not already exist)
404        let _ = std::fs::remove_dir_all(&dir_path);
405
406        // Create the directory
407        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        // make sure we are starting fresh
414        let _ = std::fs::remove_dir_all(&dir_path);
415
416        // Case 3: empty variable is not acceptable
417        unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, "") };
418        assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_none());
419    }
420}