Skip to main content

rskit_logging/
module_levels.rs

1//! Per-module log level overrides configured from YAML/config.
2//!
3//! Translates a `HashMap<String, String>` of module → level entries into a [`tracing_subscriber::EnvFilter`]
4//! so that noisy dependencies can be silenced without changing the global level.
5//!
6//! # Example
7//!
8//! ```rust
9//! use std::collections::HashMap;
10//! use rskit_logging::module_levels::build_env_filter;
11//!
12//! let mut levels = HashMap::new();
13//! levels.insert("sqlx".to_string(), "warn".to_string());
14//! let filter = build_env_filter("info", &levels);
15//! assert!(format!("{filter}").contains("sqlx"));
16//! ```
17
18use std::collections::HashMap;
19
20use tracing_subscriber::EnvFilter;
21
22// ── Config ──────────────────────────────────────────────────────────────────
23
24/// Per-module log level overrides.
25///
26/// Typically deserialized from `logging.module_levels` in YAML config.
27#[derive(Debug, Clone, Default)]
28pub struct ModuleLevelsConfig {
29    /// Module name → minimum level (e.g. `{"sqlx": "warn", "rdkafka": "off"}`).
30    pub levels: HashMap<String, String>,
31}
32
33// ── Builder ─────────────────────────────────────────────────────────────────
34
35/// Build an [`EnvFilter`] from a base level and per-module overrides.
36///
37/// The `RUST_LOG` environment variable, when set, takes precedence
38/// and the returned filter reflects only `RUST_LOG`. When `RUST_LOG` is absent,
39/// the filter is assembled from `base_level` plus each entry in `module_levels`.
40///
41/// ```text
42/// base_level = "info"
43/// module_levels = {"sqlx": "warn", "rdkafka": "off"}
44/// → filter = "info,sqlx=warn,rdkafka=off"
45/// ```
46pub fn build_env_filter(base_level: &str, module_levels: &HashMap<String, String>) -> EnvFilter {
47    // Respect RUST_LOG when explicitly set.
48    if let Ok(filter) = EnvFilter::try_from_default_env() {
49        return filter;
50    }
51
52    let directives = build_directives(base_level, module_levels);
53    EnvFilter::new(&directives)
54}
55
56/// Build the raw directives string without checking `RUST_LOG`.
57///
58/// Useful for testing or when you want to inspect the generated filter.
59pub fn build_directives(base_level: &str, module_levels: &HashMap<String, String>) -> String {
60    if module_levels.is_empty() {
61        return base_level.to_string();
62    }
63
64    let mut parts: Vec<String> = Vec::with_capacity(module_levels.len() + 1);
65    parts.push(base_level.to_string());
66
67    // Sort for deterministic output.
68    let mut entries: Vec<_> = module_levels.iter().collect();
69    entries.sort_by_key(|(k, _)| k.as_str());
70
71    for (module, level) in entries {
72        parts.push(format!("{module}={level}"));
73    }
74
75    parts.join(",")
76}
77
78// ── Tests ───────────────────────────────────────────────────────────────────
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn empty_module_levels_uses_base_only() {
86        let directives = build_directives("info", &HashMap::new());
87        assert_eq!(directives, "info");
88    }
89
90    #[test]
91    fn single_override() {
92        let mut levels = HashMap::new();
93        levels.insert("sqlx".to_string(), "warn".to_string());
94        let directives = build_directives("info", &levels);
95        assert_eq!(directives, "info,sqlx=warn");
96    }
97
98    #[test]
99    fn multiple_overrides_sorted() {
100        let mut levels = HashMap::new();
101        levels.insert("sqlx".to_string(), "warn".to_string());
102        levels.insert("rdkafka".to_string(), "off".to_string());
103        levels.insert("hyper".to_string(), "error".to_string());
104        let directives = build_directives("debug", &levels);
105        assert_eq!(directives, "debug,hyper=error,rdkafka=off,sqlx=warn");
106    }
107
108    #[test]
109    fn base_level_respected() {
110        let levels = HashMap::new();
111        let directives = build_directives("trace", &levels);
112        assert_eq!(directives, "trace");
113    }
114
115    #[test]
116    fn build_env_filter_creates_valid_filter() {
117        let mut levels = HashMap::new();
118        levels.insert("sqlx".to_string(), "warn".to_string());
119        // Should not panic.
120        let _filter = build_env_filter("info", &levels);
121    }
122
123    #[test]
124    fn module_levels_config_default_is_empty() {
125        let cfg = ModuleLevelsConfig::default();
126        assert!(cfg.levels.is_empty());
127    }
128}