hypen_engine/
logger.rs

1//! Logging system for the Hypen engine
2//!
3//! Provides a unified logging interface that can be configured with different
4//! log levels and scopes. For WASM builds, logs are routed through a host-provided
5//! logger callback.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Log level for filtering messages
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum LogLevel {
14    Trace = 0,
15    Debug = 1,
16    Info = 2,
17    Warn = 3,
18    Error = 4,
19}
20
21impl fmt::Display for LogLevel {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            LogLevel::Trace => write!(f, "trace"),
25            LogLevel::Debug => write!(f, "debug"),
26            LogLevel::Info => write!(f, "info"),
27            LogLevel::Warn => write!(f, "warn"),
28            LogLevel::Error => write!(f, "error"),
29        }
30    }
31}
32
33/// Log scope for categorizing messages
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum LogScope {
37    Parser,
38    Engine,
39    Reconciler,
40    Renderer,
41    Router,
42    Lifecycle,
43    State,
44    Component,
45    Wasm,
46}
47
48impl fmt::Display for LogScope {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            LogScope::Parser => write!(f, "parser"),
52            LogScope::Engine => write!(f, "engine"),
53            LogScope::Reconciler => write!(f, "reconciler"),
54            LogScope::Renderer => write!(f, "renderer"),
55            LogScope::Router => write!(f, "router"),
56            LogScope::Lifecycle => write!(f, "lifecycle"),
57            LogScope::State => write!(f, "state"),
58            LogScope::Component => write!(f, "component"),
59            LogScope::Wasm => write!(f, "wasm"),
60        }
61    }
62}
63
64/// Configuration for the logging system
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct LogConfig {
67    /// Minimum log level to display
68    pub min_level: LogLevel,
69    /// Scopes to enable (empty = all scopes enabled)
70    pub enabled_scopes: Vec<LogScope>,
71    /// Whether to include timestamps
72    pub timestamps: bool,
73}
74
75impl Default for LogConfig {
76    fn default() -> Self {
77        Self {
78            min_level: LogLevel::Info,
79            enabled_scopes: Vec::new(), // Empty means all enabled
80            timestamps: false,
81        }
82    }
83}
84
85impl LogConfig {
86    /// Check if a given scope and level should be logged
87    pub fn should_log(&self, scope: LogScope, level: LogLevel) -> bool {
88        // Check level
89        if level < self.min_level {
90            return false;
91        }
92
93        // Check scope (empty list means all enabled)
94        if !self.enabled_scopes.is_empty() && !self.enabled_scopes.contains(&scope) {
95            return false;
96        }
97
98        true
99    }
100
101    /// Enable all log levels (trace and above)
102    pub fn enable_all(mut self) -> Self {
103        self.min_level = LogLevel::Trace;
104        self
105    }
106
107    /// Enable specific scopes only
108    pub fn with_scopes(mut self, scopes: Vec<LogScope>) -> Self {
109        self.enabled_scopes = scopes;
110        self
111    }
112
113    /// Set minimum log level
114    pub fn with_level(mut self, level: LogLevel) -> Self {
115        self.min_level = level;
116        self
117    }
118
119    /// Enable timestamps
120    pub fn with_timestamps(mut self) -> Self {
121        self.timestamps = true;
122        self
123    }
124}
125
126/// A log message
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct LogMessage {
129    pub level: LogLevel,
130    pub scope: LogScope,
131    pub message: String,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub timestamp: Option<f64>,
134}
135
136/// Logger interface - can be implemented by host
137pub trait Logger: Send + Sync {
138    fn log(&self, level: LogLevel, scope: LogScope, message: &str);
139}
140
141// Global logger instance (for WASM builds)
142// Uses thread_local because WASM is single-threaded and js_sys::Function is !Send
143#[cfg(target_arch = "wasm32")]
144thread_local! {
145    static LOGGER: std::cell::RefCell<Option<WasmLogger>> = std::cell::RefCell::new(None);
146}
147
148#[cfg(target_arch = "wasm32")]
149pub struct WasmLogger {
150    config: LogConfig,
151    callback: Option<js_sys::Function>,
152}
153
154#[cfg(target_arch = "wasm32")]
155impl WasmLogger {
156    pub fn new(config: LogConfig) -> Self {
157        Self {
158            config,
159            callback: None,
160        }
161    }
162
163    pub fn set_callback(&mut self, callback: js_sys::Function) {
164        self.callback = Some(callback);
165    }
166
167    pub fn log(&self, level: LogLevel, scope: LogScope, message: &str) {
168        if !self.config.should_log(scope, level) {
169            return;
170        }
171
172        if let Some(ref callback) = self.callback {
173            // Create log message
174            let log_msg = LogMessage {
175                level,
176                scope,
177                message: message.to_string(),
178                timestamp: if self.config.timestamps {
179                    Some(js_sys::Date::now())
180                } else {
181                    None
182                },
183            };
184
185            // Serialize and call JavaScript callback
186            if let Ok(js_value) = serde_wasm_bindgen::to_value(&log_msg) {
187                let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &js_value);
188            }
189        } else {
190            // Fallback to console.log if no callback
191            self.fallback_log(level, scope, message);
192        }
193    }
194
195    #[cfg(target_arch = "wasm32")]
196    fn fallback_log(&self, level: LogLevel, scope: LogScope, message: &str) {
197        let formatted = format!("[{}] [{}] {}", level, scope, message);
198        let js_string = wasm_bindgen::JsValue::from_str(&formatted);
199
200        match level {
201            LogLevel::Error => web_sys::console::error_1(&js_string),
202            LogLevel::Warn => web_sys::console::warn_1(&js_string),
203            LogLevel::Info => web_sys::console::info_1(&js_string),
204            LogLevel::Debug => web_sys::console::debug_1(&js_string),
205            LogLevel::Trace => web_sys::console::log_1(&js_string),
206        }
207    }
208}
209
210/// Initialize the global logger (WASM only)
211#[cfg(target_arch = "wasm32")]
212pub fn init_logger(config: LogConfig) {
213    let logger = WasmLogger::new(config);
214    LOGGER.with(|l| {
215        *l.borrow_mut() = Some(logger);
216    });
217}
218
219/// Set the logger callback (WASM only)
220#[cfg(target_arch = "wasm32")]
221pub fn set_logger_callback(callback: js_sys::Function) {
222    LOGGER.with(|l| {
223        if let Some(ref mut logger) = *l.borrow_mut() {
224            logger.set_callback(callback);
225        }
226    });
227}
228
229/// Update logger configuration (WASM only)
230#[cfg(target_arch = "wasm32")]
231pub fn update_logger_config(config: LogConfig) {
232    LOGGER.with(|l| {
233        if let Some(ref mut logger) = *l.borrow_mut() {
234            logger.config = config;
235        }
236    });
237}
238
239/// Log a message (WASM only)
240#[cfg(target_arch = "wasm32")]
241pub fn log(level: LogLevel, scope: LogScope, message: &str) {
242    LOGGER.with(|l| {
243        if let Some(ref logger) = *l.borrow() {
244            logger.log(level, scope, message);
245        }
246    });
247}
248
249/// Log a message (native builds)
250#[cfg(not(target_arch = "wasm32"))]
251pub fn log(level: LogLevel, scope: LogScope, message: &str) {
252    eprintln!("[{}] [{}] {}", level, scope, message);
253}
254
255/// Convenience macros for logging
256#[macro_export]
257macro_rules! log_trace {
258    ($scope:expr, $($arg:tt)*) => {
259        $crate::logger::log($crate::logger::LogLevel::Trace, $scope, &format!($($arg)*))
260    };
261}
262
263#[macro_export]
264macro_rules! log_debug {
265    ($scope:expr, $($arg:tt)*) => {
266        $crate::logger::log($crate::logger::LogLevel::Debug, $scope, &format!($($arg)*))
267    };
268}
269
270#[macro_export]
271macro_rules! log_info {
272    ($scope:expr, $($arg:tt)*) => {
273        $crate::logger::log($crate::logger::LogLevel::Info, $scope, &format!($($arg)*))
274    };
275}
276
277#[macro_export]
278macro_rules! log_warn {
279    ($scope:expr, $($arg:tt)*) => {
280        $crate::logger::log($crate::logger::LogLevel::Warn, $scope, &format!($($arg)*))
281    };
282}
283
284#[macro_export]
285macro_rules! log_error {
286    ($scope:expr, $($arg:tt)*) => {
287        $crate::logger::log($crate::logger::LogLevel::Error, $scope, &format!($($arg)*))
288    };
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_log_level_ordering() {
297        assert!(LogLevel::Trace < LogLevel::Debug);
298        assert!(LogLevel::Debug < LogLevel::Info);
299        assert!(LogLevel::Info < LogLevel::Warn);
300        assert!(LogLevel::Warn < LogLevel::Error);
301    }
302
303    #[test]
304    fn test_log_config_should_log() {
305        let config = LogConfig::default(); // Info level by default
306
307        assert!(!config.should_log(LogScope::Engine, LogLevel::Trace));
308        assert!(!config.should_log(LogScope::Engine, LogLevel::Debug));
309        assert!(config.should_log(LogScope::Engine, LogLevel::Info));
310        assert!(config.should_log(LogScope::Engine, LogLevel::Warn));
311        assert!(config.should_log(LogScope::Engine, LogLevel::Error));
312    }
313
314    #[test]
315    fn test_log_config_scopes() {
316        let config = LogConfig::default()
317            .with_scopes(vec![LogScope::Engine, LogScope::Reconciler]);
318
319        assert!(config.should_log(LogScope::Engine, LogLevel::Info));
320        assert!(config.should_log(LogScope::Reconciler, LogLevel::Info));
321        assert!(!config.should_log(LogScope::Renderer, LogLevel::Info));
322    }
323}