Skip to main content

cljrs_logging/
lib.rs

1//! Feature-gated logging for clojurust.
2//!
3//! Provides per-feature debug and trace logging controlled at runtime via CLI flags.
4//! Features are arbitrary strings (e.g. "gc", "jit", "reader") — any feature name
5//! is accepted even if no code ever logs with it.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! // Set features from CLI flags
11//! cljrs_logging::set_feature_level("gc", Level::Debug);
12//! cljrs_logging::set_feature_level("jit", Level::Trace);
13//!
14//! // In code:
15//! feat_debug!("gc", "starting collection, heap_size={}", heap_size);
16//! feat_trace!("gc", "visiting object at {:p}", ptr);
17//! ```
18
19use std::collections::HashMap;
20use std::sync::RwLock;
21
22static FEATURE_LEVELS: RwLock<Option<HashMap<String, Level>>> = RwLock::new(None);
23
24/// Log level for a feature.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum Level {
27    /// No logging (default for unregistered features).
28    Off = 0,
29    /// Debug-level messages.
30    Debug = 1,
31    /// Trace-level messages (most verbose).
32    Trace = 2,
33}
34
35/// Set the logging level for a single feature.
36pub fn set_feature_level(feature: &str, level: Level) {
37    let mut guard = FEATURE_LEVELS.write().unwrap();
38    let map = guard.get_or_insert_with(HashMap::new);
39    map.insert(feature.to_string(), level);
40}
41
42/// Get the logging level for a feature. Returns `Level::Off` if the feature
43/// has not been configured.
44pub fn feature_level(feature: &str) -> Level {
45    let guard = FEATURE_LEVELS.read().unwrap();
46    guard
47        .as_ref()
48        .and_then(|m| m.get(feature).copied())
49        .unwrap_or(Level::Off)
50}
51
52/// Returns true if the given feature is enabled at least at `level`.
53#[inline]
54pub fn is_enabled(feature: &str, level: Level) -> bool {
55    feature_level(feature) >= level
56}
57
58/// Parse a `-X` flag value like `"debug:gc,jit"` or `"trace:reader"` and
59/// register the appropriate feature levels.
60///
61/// Format: `<level>:<feature1>,<feature2>,...`
62///
63/// Returns `Err` with a message if the format is invalid.
64pub fn parse_x_flag(value: &str) -> Result<(), String> {
65    let (level_str, features_str) = value
66        .split_once(':')
67        .ok_or_else(|| format!("expected <level>:<features>, got: {value}"))?;
68
69    let level = match level_str {
70        "debug" => Level::Debug,
71        "trace" => Level::Trace,
72        other => {
73            return Err(format!(
74                "unknown level '{other}', expected 'debug' or 'trace'"
75            ));
76        }
77    };
78
79    for feature in features_str.split(',') {
80        let feature = feature.trim();
81        if feature.is_empty() {
82            continue;
83        }
84        set_feature_level(feature, level);
85    }
86    Ok(())
87}
88
89/// Log a message at debug level for a feature. Only prints if the feature
90/// is enabled at debug level or higher.
91///
92/// ```ignore
93/// feat_debug!("gc", "heap size: {}", size);
94/// ```
95#[macro_export]
96macro_rules! feat_debug {
97    ($feature:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
98        if $crate::is_enabled($feature, $crate::Level::Debug) {
99            eprint!("[DEBUG:{}] ", $feature);
100            eprintln!($fmt $(, $arg)*);
101        }
102    };
103}
104
105/// Log a message at trace level for a feature.
106///
107/// ```ignore
108/// feat_trace!("gc", "visiting {:p}", ptr);
109/// ```
110#[macro_export]
111macro_rules! feat_trace {
112    ($feature:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
113        if $crate::is_enabled($feature, $crate::Level::Trace) {
114            eprint!("[TRACE:{}] ", $feature);
115            eprintln!($fmt $(, $arg)*);
116        }
117    };
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_parse_x_flag() {
126        parse_x_flag("debug:gc,jit").unwrap();
127        assert_eq!(feature_level("gc"), Level::Debug);
128        assert_eq!(feature_level("jit"), Level::Debug);
129        assert_eq!(feature_level("reader"), Level::Off);
130    }
131
132    #[test]
133    fn test_parse_x_flag_trace() {
134        parse_x_flag("trace:reader").unwrap();
135        assert_eq!(feature_level("reader"), Level::Trace);
136        assert!(is_enabled("reader", Level::Debug));
137        assert!(is_enabled("reader", Level::Trace));
138    }
139
140    #[test]
141    fn test_parse_x_flag_invalid() {
142        assert!(parse_x_flag("bogus").is_err());
143        assert!(parse_x_flag("warn:gc").is_err());
144    }
145}