Skip to main content

glide_logger/
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    sync::atomic::{AtomicUsize, Ordering},
9};
10use tracing::{self, event};
11use tracing_appender::rolling::{RollingFileAppender, RollingWriter, Rotation};
12use tracing_subscriber::{
13    Registry,
14    filter::Filtered,
15    fmt::{
16        Layer,
17        format::{DefaultFields, Format},
18    },
19    layer::Layered,
20};
21
22use tracing_subscriber::{
23    self,
24    filter::{self, LevelFilter},
25    prelude::*,
26    reload::{self, Handle},
27};
28
29use std::str::FromStr;
30
31// Layer-Filter pair determines whether a log will be collected
32type InnerFiltered = Filtered<Layer<Registry>, LevelFilter, Registry>;
33// A Reloadable pair of layer-filter
34type InnerLayered = Layered<reload::Layer<InnerFiltered, Registry>, Registry>;
35// A reloadable layer of subscriber to a rolling file
36type FileReload = Handle<
37    Filtered<
38        Layer<InnerLayered, DefaultFields, Format, LazyRollingFileAppender>,
39        LevelFilter,
40        InnerLayered,
41    >,
42    InnerLayered,
43>;
44
45pub struct Reloads {
46    console_reload: RwLock<reload::Handle<InnerFiltered, Registry>>,
47    file_reload: RwLock<FileReload>,
48}
49
50pub struct InitiateOnce {
51    init_once: OnceCell<Reloads>,
52}
53
54pub static INITIATE_ONCE: InitiateOnce = InitiateOnce {
55    init_once: OnceCell::new(),
56};
57
58/// Cheap, cached effective max log verbosity (0=Off,1=Error,2=Warn,3=Info,4=Debug,5=Trace).
59/// Read on every lazy-log macro invocation to short-circuit the (expensive, RwLock-guarded)
60/// reload-subscriber `enabled` check when the level is disabled — the common hot-path case
61/// (e.g. per-command trace/debug logs while running at the default Warn level). Updated by
62/// `init` whenever the effective level changes; set as an UPPER BOUND on the true effective
63/// level so it can never suppress a log that should be emitted.
64pub static CURRENT_MAX_VERBOSITY: AtomicUsize = AtomicUsize::new(0);
65
66/// Maps `tracing::Level` to the verbosity ordering used by `CURRENT_MAX_VERBOSITY`.
67#[inline]
68pub fn level_may_be_enabled(level: tracing::Level) -> bool {
69    let needed = match level {
70        tracing::Level::ERROR => 1,
71        tracing::Level::WARN => 2,
72        tracing::Level::INFO => 3,
73        tracing::Level::DEBUG => 4,
74        tracing::Level::TRACE => 5,
75    };
76    CURRENT_MAX_VERBOSITY.load(Ordering::Relaxed) >= needed
77}
78
79const FILE_DIRECTORY: &str = "glide-logs";
80const ENV_GLIDE_LOG_DIR: &str = "GLIDE_LOG_DIR";
81
82/// Wraps [RollingFileAppender] to defer initialization until logging is required,
83/// allowing [init] to disable file logging on read-only filesystems.
84/// This is needed because [RollingFileAppender] tries to create the log directory on initialization.
85struct LazyRollingFileAppender {
86    file_appender: OnceCell<RollingFileAppender>,
87    rotation: Rotation,
88    directory: PathBuf,
89    filename_prefix: PathBuf,
90}
91
92impl LazyRollingFileAppender {
93    fn new(
94        rotation: Rotation,
95        directory: impl AsRef<Path>,
96        filename_prefix: impl AsRef<Path>,
97    ) -> LazyRollingFileAppender {
98        LazyRollingFileAppender {
99            file_appender: OnceCell::new(),
100            rotation,
101            directory: directory.as_ref().to_path_buf(),
102            filename_prefix: filename_prefix.as_ref().to_path_buf(),
103        }
104    }
105}
106
107impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for LazyRollingFileAppender {
108    type Writer = RollingWriter<'a>;
109    fn make_writer(&'a self) -> Self::Writer {
110        let file_appender = self.file_appender.get_or_init(|| {
111            RollingFileAppender::new(
112                self.rotation.clone(),
113                self.directory.clone(),
114                self.filename_prefix.clone(),
115            )
116        });
117        file_appender.make_writer()
118    }
119}
120
121#[derive(Debug)]
122pub enum Level {
123    Error = 0,
124    Warn = 1,
125    Info = 2,
126    Debug = 3,
127    Trace = 4,
128    Off = 5,
129}
130impl Level {
131    fn to_filter(&self) -> filter::LevelFilter {
132        match self {
133            Level::Trace => LevelFilter::TRACE,
134            Level::Debug => LevelFilter::DEBUG,
135            Level::Info => LevelFilter::INFO,
136            Level::Warn => LevelFilter::WARN,
137            Level::Error => LevelFilter::ERROR,
138            Level::Off => LevelFilter::OFF,
139        }
140    }
141}
142
143/// Attempt to read a directory path from an environment variable. If the environment variable `envname` exists
144/// and contains a valid path - this function will create and return that path. In any case of failure,
145/// this method returns `None` (e.g. the environment variable exists but contains an empty path etc)
146pub fn create_directory_from_env(envname: &str) -> Option<String> {
147    let Ok(dirpath) = std::env::var(envname) else {
148        return None;
149    };
150
151    if dirpath.trim().is_empty() || std::fs::create_dir_all(&dirpath).is_err() {
152        return None;
153    }
154
155    Some(dirpath)
156}
157
158// Initialize the global logger to error level on the first call only
159// In any of the calls to the function, including the first - resetting the existence loggers to the new setting
160// provided by using the global reloadable handle
161// The logger will save only logs of the given level or above.
162pub fn init(minimal_level: Option<Level>, file_name: Option<&str>) -> Level {
163    let level = minimal_level.unwrap_or(Level::Warn);
164    let level_filter = level.to_filter();
165    let reloads = INITIATE_ONCE.init_once.get_or_init(|| {
166        let stdout_fmt = tracing_subscriber::fmt::layer()
167            .with_ansi(true)
168            .with_filter(LevelFilter::OFF);
169
170        let (stdout_layer, stdout_reload) = reload::Layer::new(stdout_fmt);
171
172        // Check if the environment variable GLIDE_LOG is set
173        let logs_dir =
174            create_directory_from_env(ENV_GLIDE_LOG_DIR).unwrap_or(FILE_DIRECTORY.to_string());
175        let file_appender = LazyRollingFileAppender::new(
176            Rotation::HOURLY,
177            logs_dir,
178            file_name.unwrap_or("output.log"),
179        );
180
181        let file_fmt = tracing_subscriber::fmt::layer()
182            .with_writer(file_appender)
183            .with_filter(LevelFilter::OFF);
184        let (file_layer, file_reload) = reload::Layer::new(file_fmt);
185
186        // If user has set the environment variable "RUST_LOG" with a valid log verbosity, use it
187        let log_level = if let Ok(level) = std::env::var("RUST_LOG") {
188            let trace_level = tracing::Level::from_str(&level).unwrap_or(tracing::Level::TRACE);
189            LevelFilter::from(trace_level)
190        } else {
191            LevelFilter::TRACE
192        };
193
194        // Enable logging only from allowed crates
195        let targets_filter = filter::Targets::new()
196            .with_target("glide", log_level)
197            .with_target("redis", log_level)
198            .with_target("glide_logger", log_level)
199            .with_target(std::env!("CARGO_PKG_NAME"), log_level);
200
201        // Use try_init() instead of init() to gracefully handle the case where
202        // a tracing subscriber has already been set by the application.
203        // This allows applications to control their own logging configuration.
204        tracing_subscriber::registry()
205            .with(stdout_layer)
206            .with(file_layer)
207            .with(targets_filter)
208            .try_init()
209            .ok(); // Ignore the error if subscriber already set
210
211        let reloads: Reloads = Reloads {
212            console_reload: RwLock::new(stdout_reload),
213            file_reload: RwLock::new(file_reload),
214        };
215        reloads
216    });
217
218    match file_name {
219        None => {
220            let _ = reloads
221                .console_reload
222                .write()
223                .expect("error reloading stdout")
224                .modify(|layer| *layer.filter_mut() = level_filter);
225            let _ = reloads
226                .file_reload
227                .write()
228                .expect("error reloading file appender")
229                .modify(|layer| {
230                    *layer.filter_mut() = LevelFilter::OFF;
231                });
232        }
233        Some(file) => {
234            // Check if the environment variable GLIDE_LOG is set
235            let logs_dir =
236                create_directory_from_env(ENV_GLIDE_LOG_DIR).unwrap_or(FILE_DIRECTORY.to_string());
237            let file_appender = LazyRollingFileAppender::new(Rotation::HOURLY, logs_dir, file);
238            let _ = reloads
239                .file_reload
240                .write()
241                .expect("error reloading file appender")
242                .modify(|layer| {
243                    *layer.filter_mut() = level_filter;
244                    *layer.inner_mut().writer_mut() = file_appender;
245                });
246            let _ = reloads
247                .console_reload
248                .write()
249                .expect("error reloading stdout")
250                .modify(|layer| *layer.filter_mut() = LevelFilter::OFF);
251        }
252    };
253    // Publish the effective max verbosity for the cheap lazy-macro gate.
254    CURRENT_MAX_VERBOSITY.store(
255        match level {
256            Level::Off => 0,
257            Level::Error => 1,
258            Level::Warn => 2,
259            Level::Info => 3,
260            Level::Debug => 4,
261            Level::Trace => 5,
262        },
263        Ordering::Relaxed,
264    );
265    level
266}
267
268macro_rules! create_log {
269    ($name:ident, $uppercase_level:tt) => {
270        pub fn $name<Message: AsRef<str>, Identifier: AsRef<str>>(
271            log_identifier: Identifier,
272            message: Message,
273        ) {
274            if INITIATE_ONCE.init_once.get().is_none() {
275                init(Some(Level::Warn), None);
276            };
277            let message_ref = message.as_ref();
278            let identifier_ref = log_identifier.as_ref();
279            event!(
280                tracing::Level::$uppercase_level,
281                "{identifier_ref} - {message_ref}"
282            )
283        }
284    };
285}
286
287create_log!(log_trace, TRACE);
288create_log!(log_debug, DEBUG);
289create_log!(log_info, INFO);
290create_log!(log_warn, WARN);
291create_log!(log_error, ERROR);
292
293/// Lazy logging macros that only evaluate the message expression if the log level is enabled.
294/// This avoids the cost of `format!(...)` when the level is disabled.
295///
296/// Usage:
297/// ```ignore
298/// log_trace_lazy!("identifier", format!("expensive computation: {}", value));
299/// log_warn_lazy!("identifier", format!("something happened: {:?}", err));
300/// ```
301#[macro_export]
302macro_rules! log_trace_lazy {
303    ($identifier:expr, $message:expr) => {
304        if $crate::level_may_be_enabled(tracing::Level::TRACE)
305            && tracing::event_enabled!(tracing::Level::TRACE)
306        {
307            $crate::log_trace($identifier, $message);
308        }
309    };
310}
311
312#[macro_export]
313macro_rules! log_debug_lazy {
314    ($identifier:expr, $message:expr) => {
315        if $crate::level_may_be_enabled(tracing::Level::DEBUG)
316            && tracing::event_enabled!(tracing::Level::DEBUG)
317        {
318            $crate::log_debug($identifier, $message);
319        }
320    };
321}
322
323#[macro_export]
324macro_rules! log_info_lazy {
325    ($identifier:expr, $message:expr) => {
326        if $crate::level_may_be_enabled(tracing::Level::INFO)
327            && tracing::event_enabled!(tracing::Level::INFO)
328        {
329            $crate::log_info($identifier, $message);
330        }
331    };
332}
333
334#[macro_export]
335macro_rules! log_warn_lazy {
336    ($identifier:expr, $message:expr) => {
337        if tracing::event_enabled!(tracing::Level::WARN) {
338            $crate::log_warn($identifier, $message);
339        }
340    };
341}
342
343#[macro_export]
344macro_rules! log_error_lazy {
345    ($identifier:expr, $message:expr) => {
346        if tracing::event_enabled!(tracing::Level::ERROR) {
347            $crate::log_error($identifier, $message);
348        }
349    };
350}
351
352/// Rate-limited logging macro. Logs at most once per `interval_secs` seconds.
353/// Uses a static AtomicU64 per call site to track the last log time.
354///
355/// Usage:
356/// ```ignore
357/// log_warn_rate_limited!("identifier", 10, format!("something happened: {}", val));
358/// ```
359#[macro_export]
360macro_rules! log_warn_rate_limited {
361    ($identifier:expr, $interval_secs:expr, $message:expr) => {{
362        static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
363        let now = std::time::SystemTime::now()
364            .duration_since(std::time::UNIX_EPOCH)
365            .unwrap_or_default()
366            .as_secs();
367        let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
368        if now >= last + $interval_secs {
369            LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
370            $crate::log_warn($identifier, $message);
371        }
372    }};
373}
374
375#[macro_export]
376macro_rules! log_info_rate_limited {
377    ($identifier:expr, $interval_secs:expr, $message:expr) => {{
378        static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
379        let now = std::time::SystemTime::now()
380            .duration_since(std::time::UNIX_EPOCH)
381            .unwrap_or_default()
382            .as_secs();
383        let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
384        if now >= last + $interval_secs {
385            LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
386            $crate::log_info($identifier, $message);
387        }
388    }};
389}
390
391#[macro_export]
392macro_rules! log_debug_rate_limited {
393    ($identifier:expr, $interval_secs:expr, $message:expr) => {{
394        static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
395        let now = std::time::SystemTime::now()
396            .duration_since(std::time::UNIX_EPOCH)
397            .unwrap_or_default()
398            .as_secs();
399        let last = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
400        if now >= last + $interval_secs {
401            LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
402            $crate::log_debug($identifier, $message);
403        }
404    }};
405}
406
407// 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.
408// 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.
409// If this is called before a logger was initialized the log will not be registered.
410// If logger doesn't exist, create the default
411pub fn log<Message: AsRef<str>, Identifier: AsRef<str>>(
412    log_level: Level,
413    log_identifier: Identifier,
414    message: Message,
415) {
416    match log_level {
417        Level::Debug => log_debug(log_identifier, message),
418        Level::Trace => log_trace(log_identifier, message),
419        Level::Info => log_info(log_identifier, message),
420        Level::Warn => log_warn(log_identifier, message),
421        Level::Error => log_error(log_identifier, message),
422        Level::Off => (),
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_directory_from_env() {
432        let dir_path = format!("{}/glide-logs", std::env::temp_dir().display());
433        // Case 1: try to create an already existing folder
434        // make sure we are starting fresh
435        let _ = std::fs::remove_dir_all(&dir_path);
436        // Create the directory
437        assert!(std::fs::create_dir_all(&dir_path).is_ok());
438
439        unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, &dir_path) };
440        assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_some());
441        assert!(std::fs::metadata(&dir_path).is_ok());
442
443        // Case 2: try to create a new folder (i.e. the folder does not already exist)
444        let _ = std::fs::remove_dir_all(&dir_path);
445
446        // Create the directory
447        assert!(std::fs::create_dir_all(&dir_path).is_ok());
448        assert!(std::fs::metadata(&dir_path).is_ok());
449
450        unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, &dir_path) };
451        assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_some());
452
453        // make sure we are starting fresh
454        let _ = std::fs::remove_dir_all(&dir_path);
455
456        // Case 3: empty variable is not acceptable
457        unsafe { std::env::set_var(ENV_GLIDE_LOG_DIR, "") };
458        assert!(create_directory_from_env(ENV_GLIDE_LOG_DIR).is_none());
459    }
460}