Skip to main content

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 JS WASM builds)
142// Uses thread_local because WASM is single-threaded and js_sys::Function is !Send
143#[cfg(all(target_arch = "wasm32", feature = "js"))]
144thread_local! {
145    static LOGGER: std::cell::RefCell<Option<WasmLogger>> = const { std::cell::RefCell::new(None) };
146}
147
148#[cfg(all(target_arch = "wasm32", feature = "js"))]
149pub struct WasmLogger {
150    config: LogConfig,
151    callback: Option<js_sys::Function>,
152}
153
154#[cfg(all(target_arch = "wasm32", feature = "js"))]
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    fn fallback_log(&self, level: LogLevel, scope: LogScope, message: &str) {
196        let formatted = format!("[{}] [{}] {}", level, scope, message);
197        let js_string = wasm_bindgen::JsValue::from_str(&formatted);
198
199        match level {
200            LogLevel::Error => web_sys::console::error_1(&js_string),
201            LogLevel::Warn => web_sys::console::warn_1(&js_string),
202            LogLevel::Info => web_sys::console::info_1(&js_string),
203            LogLevel::Debug => web_sys::console::debug_1(&js_string),
204            LogLevel::Trace => web_sys::console::log_1(&js_string),
205        }
206    }
207}
208
209/// Initialize the global logger (JS WASM only)
210#[cfg(all(target_arch = "wasm32", feature = "js"))]
211pub fn init_logger(config: LogConfig) {
212    let logger = WasmLogger::new(config);
213    LOGGER.with(|l| {
214        *l.borrow_mut() = Some(logger);
215    });
216}
217
218/// Set the logger callback (JS WASM only)
219#[cfg(all(target_arch = "wasm32", feature = "js"))]
220pub fn set_logger_callback(callback: js_sys::Function) {
221    LOGGER.with(|l| {
222        if let Some(ref mut logger) = *l.borrow_mut() {
223            logger.set_callback(callback);
224        }
225    });
226}
227
228/// Update logger configuration (JS WASM only)
229#[cfg(all(target_arch = "wasm32", feature = "js"))]
230pub fn update_logger_config(config: LogConfig) {
231    LOGGER.with(|l| {
232        if let Some(ref mut logger) = *l.borrow_mut() {
233            logger.config = config;
234        }
235    });
236}
237
238/// Log a message (JS WASM only)
239#[cfg(all(target_arch = "wasm32", feature = "js"))]
240pub fn log(level: LogLevel, scope: LogScope, message: &str) {
241    LOGGER.with(|l| {
242        if let Some(ref logger) = *l.borrow() {
243            logger.log(level, scope, message);
244        }
245    });
246}
247
248/// Log a message (native builds and WASI WASM)
249#[cfg(not(all(target_arch = "wasm32", feature = "js")))]
250pub fn log(level: LogLevel, scope: LogScope, message: &str) {
251    eprintln!("[{}] [{}] {}", level, scope, message);
252}
253
254/// Convenience macros for logging
255#[macro_export]
256macro_rules! log_trace {
257    ($scope:expr, $($arg:tt)*) => {
258        $crate::logger::log($crate::logger::LogLevel::Trace, $scope, &format!($($arg)*))
259    };
260}
261
262#[macro_export]
263macro_rules! log_debug {
264    ($scope:expr, $($arg:tt)*) => {
265        $crate::logger::log($crate::logger::LogLevel::Debug, $scope, &format!($($arg)*))
266    };
267}
268
269#[macro_export]
270macro_rules! log_info {
271    ($scope:expr, $($arg:tt)*) => {
272        $crate::logger::log($crate::logger::LogLevel::Info, $scope, &format!($($arg)*))
273    };
274}
275
276#[macro_export]
277macro_rules! log_warn {
278    ($scope:expr, $($arg:tt)*) => {
279        $crate::logger::log($crate::logger::LogLevel::Warn, $scope, &format!($($arg)*))
280    };
281}
282
283#[macro_export]
284macro_rules! log_error {
285    ($scope:expr, $($arg:tt)*) => {
286        $crate::logger::log($crate::logger::LogLevel::Error, $scope, &format!($($arg)*))
287    };
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_log_level_ordering() {
296        assert!(LogLevel::Trace < LogLevel::Debug);
297        assert!(LogLevel::Debug < LogLevel::Info);
298        assert!(LogLevel::Info < LogLevel::Warn);
299        assert!(LogLevel::Warn < LogLevel::Error);
300    }
301
302    #[test]
303    fn test_log_config_should_log() {
304        let config = LogConfig::default(); // Info level by default
305
306        assert!(!config.should_log(LogScope::Engine, LogLevel::Trace));
307        assert!(!config.should_log(LogScope::Engine, LogLevel::Debug));
308        assert!(config.should_log(LogScope::Engine, LogLevel::Info));
309        assert!(config.should_log(LogScope::Engine, LogLevel::Warn));
310        assert!(config.should_log(LogScope::Engine, LogLevel::Error));
311    }
312
313    #[test]
314    fn test_log_config_scopes() {
315        let config = LogConfig::default().with_scopes(vec![LogScope::Engine, LogScope::Reconciler]);
316
317        assert!(config.should_log(LogScope::Engine, LogLevel::Info));
318        assert!(config.should_log(LogScope::Reconciler, LogLevel::Info));
319        assert!(!config.should_log(LogScope::Renderer, LogLevel::Info));
320    }
321}