Skip to main content

mabi_core/logging/
dynamic.rs

1//! Dynamic log level management.
2//!
3//! This module provides functionality to change log levels at runtime
4//! without restarting the application. This is essential for debugging
5//! production systems where you may need to temporarily increase verbosity.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use mabi_core::logging::{LogLevelController, LogLevel};
11//!
12//! // Get the global controller (if logging was initialized with dynamic_level = true)
13//! if let Some(controller) = LogLevelController::global() {
14//!     // Change global log level
15//!     controller.set_level(LogLevel::Debug);
16//!
17//!     // Change level for a specific module
18//!     controller.set_module_level("mabi_core::engine", LogLevel::Trace);
19//!
20//!     // Get current level
21//!     let level = controller.current_level();
22//! }
23//! ```
24
25use std::sync::{Arc, OnceLock};
26
27use parking_lot::RwLock;
28use tracing_subscriber::reload;
29use tracing_subscriber::EnvFilter;
30
31use super::config::LogLevel;
32use crate::error::Result;
33
34/// Type alias for the reload handle used to change log levels at runtime.
35pub type ReloadHandle = reload::Handle<EnvFilter, tracing_subscriber::Registry>;
36
37/// Global storage for the log level controller.
38static GLOBAL_CONTROLLER: OnceLock<LogLevelController> = OnceLock::new();
39
40/// Controller for dynamic log level management.
41///
42/// This struct provides methods to change log levels at runtime.
43/// It wraps a `reload::Handle` from `tracing-subscriber`.
44#[derive(Clone)]
45pub struct LogLevelController {
46    inner: Arc<LogLevelControllerInner>,
47}
48
49struct LogLevelControllerInner {
50    /// The reload handle for the filter layer.
51    handle: ReloadHandle,
52
53    /// Current global log level.
54    current_level: RwLock<LogLevel>,
55
56    /// Per-module log levels.
57    module_levels: RwLock<std::collections::HashMap<String, LogLevel>>,
58}
59
60impl LogLevelController {
61    /// Create a new log level controller with the given reload handle.
62    pub fn new(handle: ReloadHandle, initial_level: LogLevel) -> Self {
63        Self {
64            inner: Arc::new(LogLevelControllerInner {
65                handle,
66                current_level: RwLock::new(initial_level),
67                module_levels: RwLock::new(std::collections::HashMap::new()),
68            }),
69        }
70    }
71
72    /// Register this controller as the global instance.
73    ///
74    /// Returns true if registration succeeded, false if a controller
75    /// was already registered.
76    pub fn register_global(self) -> bool {
77        GLOBAL_CONTROLLER.set(self).is_ok()
78    }
79
80    /// Get the global log level controller, if one is registered.
81    pub fn global() -> Option<&'static LogLevelController> {
82        GLOBAL_CONTROLLER.get()
83    }
84
85    /// Get the current global log level.
86    pub fn current_level(&self) -> LogLevel {
87        *self.inner.current_level.read()
88    }
89
90    /// Get all current module-specific log levels.
91    pub fn module_levels(&self) -> std::collections::HashMap<String, LogLevel> {
92        self.inner.module_levels.read().clone()
93    }
94
95    /// Set the global log level.
96    ///
97    /// This immediately affects all log output that doesn't have
98    /// a module-specific override.
99    pub fn set_level(&self, level: LogLevel) -> Result<()> {
100        {
101            let mut current = self.inner.current_level.write();
102            *current = level;
103        }
104        self.apply_filter()
105    }
106
107    /// Set the log level for a specific module.
108    ///
109    /// # Arguments
110    /// * `module` - The module path (e.g., "mabi_core::engine")
111    /// * `level` - The log level to set
112    pub fn set_module_level(&self, module: impl Into<String>, level: LogLevel) -> Result<()> {
113        {
114            let mut levels = self.inner.module_levels.write();
115            levels.insert(module.into(), level);
116        }
117        self.apply_filter()
118    }
119
120    /// Remove a module-specific log level override.
121    pub fn remove_module_level(&self, module: &str) -> Result<()> {
122        {
123            let mut levels = self.inner.module_levels.write();
124            levels.remove(module);
125        }
126        self.apply_filter()
127    }
128
129    /// Clear all module-specific log level overrides.
130    pub fn clear_module_levels(&self) -> Result<()> {
131        {
132            let mut levels = self.inner.module_levels.write();
133            levels.clear();
134        }
135        self.apply_filter()
136    }
137
138    /// Temporarily increase verbosity for debugging.
139    ///
140    /// This is a convenience method that sets the global level to Debug
141    /// and can be easily reverted.
142    pub fn enable_debug_mode(&self) -> Result<DebugModeGuard> {
143        let previous_level = self.current_level();
144        let previous_modules = self.module_levels();
145
146        self.set_level(LogLevel::Debug)?;
147
148        Ok(DebugModeGuard {
149            controller: self.clone(),
150            previous_level,
151            previous_modules,
152        })
153    }
154
155    /// Temporarily increase verbosity for a specific module.
156    pub fn enable_module_trace(&self, module: impl Into<String>) -> Result<ModuleTraceGuard> {
157        let module = module.into();
158        let previous_level = self.inner.module_levels.read().get(&module).copied();
159
160        self.set_module_level(module.clone(), LogLevel::Trace)?;
161
162        Ok(ModuleTraceGuard {
163            controller: self.clone(),
164            module,
165            previous_level,
166        })
167    }
168
169    /// Build and apply the current filter configuration.
170    fn apply_filter(&self) -> Result<()> {
171        let filter = self.build_filter();
172        self.inner
173            .handle
174            .reload(filter)
175            .map_err(|e| crate::error::Error::Config(format!("Failed to reload log filter: {}", e)))
176    }
177
178    /// Build an EnvFilter from current configuration.
179    fn build_filter(&self) -> EnvFilter {
180        let level = *self.inner.current_level.read();
181        let modules = self.inner.module_levels.read();
182
183        let mut filter_str = format!("trap_sim={}", level.as_filter_str());
184
185        for (module, module_level) in modules.iter() {
186            filter_str.push_str(&format!(",{}={}", module, module_level.as_filter_str()));
187        }
188
189        // Default external crate levels
190        if !modules.contains_key("tokio") {
191            filter_str.push_str(",tokio=warn");
192        }
193        if !modules.contains_key("hyper") {
194            filter_str.push_str(",hyper=warn");
195        }
196
197        EnvFilter::try_new(&filter_str).unwrap_or_else(|_| EnvFilter::new(level.as_filter_str()))
198    }
199
200    /// Get the current filter string (for debugging purposes).
201    pub fn current_filter_string(&self) -> String {
202        let level = *self.inner.current_level.read();
203        let modules = self.inner.module_levels.read();
204
205        let mut parts = vec![format!("trap_sim={}", level.as_filter_str())];
206
207        for (module, module_level) in modules.iter() {
208            parts.push(format!("{}={}", module, module_level.as_filter_str()));
209        }
210
211        parts.join(",")
212    }
213}
214
215impl std::fmt::Debug for LogLevelController {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.debug_struct("LogLevelController")
218            .field("current_level", &self.current_level())
219            .field("module_levels", &self.module_levels())
220            .finish()
221    }
222}
223
224/// Guard that restores the previous log level when dropped.
225///
226/// This is useful for temporarily increasing verbosity during debugging.
227#[must_use = "Debug mode will be reverted when the guard is dropped"]
228pub struct DebugModeGuard {
229    controller: LogLevelController,
230    previous_level: LogLevel,
231    previous_modules: std::collections::HashMap<String, LogLevel>,
232}
233
234impl Drop for DebugModeGuard {
235    fn drop(&mut self) {
236        // Restore previous level
237        let _ = self.controller.set_level(self.previous_level);
238
239        // Restore module levels
240        {
241            let mut levels = self.controller.inner.module_levels.write();
242            *levels = self.previous_modules.clone();
243        }
244        let _ = self.controller.apply_filter();
245    }
246}
247
248/// Guard that restores the previous module log level when dropped.
249#[must_use = "Module trace will be disabled when the guard is dropped"]
250pub struct ModuleTraceGuard {
251    controller: LogLevelController,
252    module: String,
253    previous_level: Option<LogLevel>,
254}
255
256impl Drop for ModuleTraceGuard {
257    fn drop(&mut self) {
258        let _ = match self.previous_level {
259            Some(level) => self.controller.set_module_level(&self.module, level),
260            None => self.controller.remove_module_level(&self.module),
261        };
262    }
263}
264
265/// Level change event for observability.
266#[derive(Debug, Clone)]
267pub struct LevelChangeEvent {
268    /// The type of change.
269    pub change_type: LevelChangeType,
270    /// Previous level (if applicable).
271    pub previous_level: Option<LogLevel>,
272    /// New level.
273    pub new_level: LogLevel,
274    /// Timestamp of the change.
275    pub timestamp: std::time::SystemTime,
276}
277
278/// Type of log level change.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum LevelChangeType {
281    /// Global level change.
282    Global,
283    /// Module-specific level change.
284    Module(String),
285}
286
287/// Statistics about log level changes.
288#[derive(Debug, Clone, Default)]
289pub struct LevelChangeStats {
290    /// Total number of level changes.
291    pub total_changes: u64,
292    /// Number of global level changes.
293    pub global_changes: u64,
294    /// Number of module-specific changes.
295    pub module_changes: u64,
296    /// Last change timestamp.
297    pub last_change: Option<std::time::SystemTime>,
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    // Note: Many tests for LogLevelController require actual initialization
305    // of the tracing subscriber, which can only be done once per process.
306    // These tests focus on the parts that can be tested in isolation.
307
308    #[test]
309    fn test_level_change_event() {
310        let event = LevelChangeEvent {
311            change_type: LevelChangeType::Global,
312            previous_level: Some(LogLevel::Info),
313            new_level: LogLevel::Debug,
314            timestamp: std::time::SystemTime::now(),
315        };
316
317        assert_eq!(event.change_type, LevelChangeType::Global);
318        assert_eq!(event.previous_level, Some(LogLevel::Info));
319        assert_eq!(event.new_level, LogLevel::Debug);
320    }
321
322    #[test]
323    fn test_level_change_type() {
324        let global = LevelChangeType::Global;
325        let module = LevelChangeType::Module("test::module".to_string());
326
327        assert_ne!(global, module);
328    }
329
330    #[test]
331    fn test_level_change_stats() {
332        let mut stats = LevelChangeStats::default();
333        stats.total_changes = 5;
334        stats.global_changes = 2;
335        stats.module_changes = 3;
336
337        assert_eq!(stats.total_changes, 5);
338        assert_eq!(stats.global_changes + stats.module_changes, 5);
339    }
340}