use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Trace => write!(f, "trace"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Info => write!(f, "info"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogScope {
Parser,
Engine,
Reconciler,
Renderer,
Router,
Lifecycle,
State,
Component,
Wasm,
}
impl fmt::Display for LogScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogScope::Parser => write!(f, "parser"),
LogScope::Engine => write!(f, "engine"),
LogScope::Reconciler => write!(f, "reconciler"),
LogScope::Renderer => write!(f, "renderer"),
LogScope::Router => write!(f, "router"),
LogScope::Lifecycle => write!(f, "lifecycle"),
LogScope::State => write!(f, "state"),
LogScope::Component => write!(f, "component"),
LogScope::Wasm => write!(f, "wasm"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub min_level: LogLevel,
pub enabled_scopes: Vec<LogScope>,
pub timestamps: bool,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
min_level: LogLevel::Info,
enabled_scopes: Vec::new(), timestamps: false,
}
}
}
impl LogConfig {
pub fn should_log(&self, scope: LogScope, level: LogLevel) -> bool {
if level < self.min_level {
return false;
}
if !self.enabled_scopes.is_empty() && !self.enabled_scopes.contains(&scope) {
return false;
}
true
}
pub fn enable_all(mut self) -> Self {
self.min_level = LogLevel::Trace;
self
}
pub fn with_scopes(mut self, scopes: Vec<LogScope>) -> Self {
self.enabled_scopes = scopes;
self
}
pub fn with_level(mut self, level: LogLevel) -> Self {
self.min_level = level;
self
}
pub fn with_timestamps(mut self) -> Self {
self.timestamps = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogMessage {
pub level: LogLevel,
pub scope: LogScope,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<f64>,
}
pub trait Logger: Send + Sync {
fn log(&self, level: LogLevel, scope: LogScope, message: &str);
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
thread_local! {
static LOGGER: std::cell::RefCell<Option<WasmLogger>> = const { std::cell::RefCell::new(None) };
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
pub struct WasmLogger {
config: LogConfig,
callback: Option<js_sys::Function>,
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
impl WasmLogger {
pub fn new(config: LogConfig) -> Self {
Self {
config,
callback: None,
}
}
pub fn set_callback(&mut self, callback: js_sys::Function) {
self.callback = Some(callback);
}
pub fn log(&self, level: LogLevel, scope: LogScope, message: &str) {
if !self.config.should_log(scope, level) {
return;
}
if let Some(ref callback) = self.callback {
let log_msg = LogMessage {
level,
scope,
message: message.to_string(),
timestamp: if self.config.timestamps {
Some(js_sys::Date::now())
} else {
None
},
};
if let Ok(js_value) = serde_wasm_bindgen::to_value(&log_msg) {
let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &js_value);
}
} else {
self.fallback_log(level, scope, message);
}
}
fn fallback_log(&self, level: LogLevel, scope: LogScope, message: &str) {
let formatted = format!("[{}] [{}] {}", level, scope, message);
let js_string = wasm_bindgen::JsValue::from_str(&formatted);
match level {
LogLevel::Error => web_sys::console::error_1(&js_string),
LogLevel::Warn => web_sys::console::warn_1(&js_string),
LogLevel::Info => web_sys::console::info_1(&js_string),
LogLevel::Debug => web_sys::console::debug_1(&js_string),
LogLevel::Trace => web_sys::console::log_1(&js_string),
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
pub fn init_logger(config: LogConfig) {
let logger = WasmLogger::new(config);
LOGGER.with(|l| {
*l.borrow_mut() = Some(logger);
});
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
pub fn set_logger_callback(callback: js_sys::Function) {
LOGGER.with(|l| {
if let Some(ref mut logger) = *l.borrow_mut() {
logger.set_callback(callback);
}
});
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
pub fn update_logger_config(config: LogConfig) {
LOGGER.with(|l| {
if let Some(ref mut logger) = *l.borrow_mut() {
logger.config = config;
}
});
}
#[cfg(all(target_arch = "wasm32", feature = "js"))]
pub fn log(level: LogLevel, scope: LogScope, message: &str) {
LOGGER.with(|l| {
if let Some(ref logger) = *l.borrow() {
logger.log(level, scope, message);
}
});
}
#[cfg(not(all(target_arch = "wasm32", feature = "js")))]
pub fn log(level: LogLevel, scope: LogScope, message: &str) {
eprintln!("[{}] [{}] {}", level, scope, message);
}
#[macro_export]
macro_rules! log_trace {
($scope:expr, $($arg:tt)*) => {
$crate::logger::log($crate::logger::LogLevel::Trace, $scope, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_debug {
($scope:expr, $($arg:tt)*) => {
$crate::logger::log($crate::logger::LogLevel::Debug, $scope, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_info {
($scope:expr, $($arg:tt)*) => {
$crate::logger::log($crate::logger::LogLevel::Info, $scope, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_warn {
($scope:expr, $($arg:tt)*) => {
$crate::logger::log($crate::logger::LogLevel::Warn, $scope, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_error {
($scope:expr, $($arg:tt)*) => {
$crate::logger::log($crate::logger::LogLevel::Error, $scope, &format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Trace < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Error);
}
#[test]
fn test_log_config_should_log() {
let config = LogConfig::default();
assert!(!config.should_log(LogScope::Engine, LogLevel::Trace));
assert!(!config.should_log(LogScope::Engine, LogLevel::Debug));
assert!(config.should_log(LogScope::Engine, LogLevel::Info));
assert!(config.should_log(LogScope::Engine, LogLevel::Warn));
assert!(config.should_log(LogScope::Engine, LogLevel::Error));
}
#[test]
fn test_log_config_scopes() {
let config = LogConfig::default().with_scopes(vec![LogScope::Engine, LogScope::Reconciler]);
assert!(config.should_log(LogScope::Engine, LogLevel::Info));
assert!(config.should_log(LogScope::Reconciler, LogLevel::Info));
assert!(!config.should_log(LogScope::Renderer, LogLevel::Info));
}
}