1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct LogConfig {
67 pub min_level: LogLevel,
69 pub enabled_scopes: Vec<LogScope>,
71 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(), timestamps: false,
81 }
82 }
83}
84
85impl LogConfig {
86 pub fn should_log(&self, scope: LogScope, level: LogLevel) -> bool {
88 if level < self.min_level {
90 return false;
91 }
92
93 if !self.enabled_scopes.is_empty() && !self.enabled_scopes.contains(&scope) {
95 return false;
96 }
97
98 true
99 }
100
101 pub fn enable_all(mut self) -> Self {
103 self.min_level = LogLevel::Trace;
104 self
105 }
106
107 pub fn with_scopes(mut self, scopes: Vec<LogScope>) -> Self {
109 self.enabled_scopes = scopes;
110 self
111 }
112
113 pub fn with_level(mut self, level: LogLevel) -> Self {
115 self.min_level = level;
116 self
117 }
118
119 pub fn with_timestamps(mut self) -> Self {
121 self.timestamps = true;
122 self
123 }
124}
125
126#[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
136pub trait Logger: Send + Sync {
138 fn log(&self, level: LogLevel, scope: LogScope, message: &str);
139}
140
141#[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 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 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 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#[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#[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#[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#[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#[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#[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(); 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}