| |
| |
|
|
| use parking_lot::RwLock; |
| use serde::Serialize; |
| use std::collections::VecDeque; |
| use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
| use std::sync::{Arc, OnceLock}; |
| use tauri::Emitter; |
| use tracing::field::{Field, Visit}; |
| use tracing::{Event, Level, Subscriber}; |
| use tracing_subscriber::layer::Context; |
| use tracing_subscriber::Layer; |
|
|
| |
| const MAX_BUFFER_SIZE: usize = 5000; |
|
|
| |
| static LOG_BRIDGE_ENABLED: AtomicBool = AtomicBool::new(false); |
|
|
| |
| static LOG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); |
|
|
| |
| static APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new(); |
|
|
| |
| static LOG_BUFFER: OnceLock<Arc<RwLock<VecDeque<LogEntry>>>> = OnceLock::new(); |
|
|
| fn get_log_buffer() -> &'static Arc<RwLock<VecDeque<LogEntry>>> { |
| LOG_BUFFER.get_or_init(|| Arc::new(RwLock::new(VecDeque::with_capacity(MAX_BUFFER_SIZE)))) |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct LogEntry { |
| pub id: u64, |
| pub timestamp: i64, |
| pub level: String, |
| pub target: String, |
| pub message: String, |
| pub fields: std::collections::HashMap<String, String>, |
| } |
|
|
| |
| pub fn init_log_bridge(app_handle: tauri::AppHandle) { |
| let _ = APP_HANDLE.set(app_handle); |
| tracing::debug!("[LogBridge] Initialized with app handle"); |
| } |
|
|
| |
| pub fn enable_log_bridge() { |
| LOG_BRIDGE_ENABLED.store(true, Ordering::SeqCst); |
|
|
| |
| if let Some(handle) = APP_HANDLE.get() { |
| let buffer = get_log_buffer().read(); |
| for entry in buffer.iter() { |
| let _ = handle.emit("log-event", entry.clone()); |
| } |
| } |
|
|
| tracing::info!("[LogBridge] Debug console enabled"); |
| } |
|
|
| |
| pub fn disable_log_bridge() { |
| LOG_BRIDGE_ENABLED.store(false, Ordering::SeqCst); |
| tracing::info!("[LogBridge] Debug console disabled"); |
| } |
|
|
| |
| pub fn is_log_bridge_enabled() -> bool { |
| LOG_BRIDGE_ENABLED.load(Ordering::SeqCst) |
| } |
|
|
| |
| pub fn get_buffered_logs() -> Vec<LogEntry> { |
| get_log_buffer().read().iter().cloned().collect() |
| } |
|
|
| |
| pub fn clear_log_buffer() { |
| get_log_buffer().write().clear(); |
| } |
|
|
| |
| |
| pub fn emit_accounts_refreshed() { |
| if let Some(handle) = APP_HANDLE.get() { |
| let _ = handle.emit("accounts://refreshed", ()); |
| tracing::debug!("[LogBridge] Emitted accounts://refreshed event to frontend"); |
| } |
| } |
|
|
| |
| struct FieldVisitor { |
| message: Option<String>, |
| fields: std::collections::HashMap<String, String>, |
| } |
|
|
| impl FieldVisitor { |
| fn new() -> Self { |
| Self { |
| message: None, |
| fields: std::collections::HashMap::new(), |
| } |
| } |
| } |
|
|
| impl Visit for FieldVisitor { |
| fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { |
| let value_str = format!("{:?}", value); |
| if field.name() == "message" { |
| self.message = Some(value_str.trim_matches('"').to_string()); |
| } else { |
| self.fields.insert(field.name().to_string(), value_str); |
| } |
| } |
|
|
| fn record_str(&mut self, field: &Field, value: &str) { |
| if field.name() == "message" { |
| self.message = Some(value.to_string()); |
| } else { |
| self.fields |
| .insert(field.name().to_string(), value.to_string()); |
| } |
| } |
|
|
| fn record_i64(&mut self, field: &Field, value: i64) { |
| self.fields |
| .insert(field.name().to_string(), value.to_string()); |
| } |
|
|
| fn record_u64(&mut self, field: &Field, value: u64) { |
| self.fields |
| .insert(field.name().to_string(), value.to_string()); |
| } |
|
|
| fn record_bool(&mut self, field: &Field, value: bool) { |
| self.fields |
| .insert(field.name().to_string(), value.to_string()); |
| } |
| } |
|
|
| |
| pub struct TauriLogBridgeLayer; |
|
|
| impl TauriLogBridgeLayer { |
| pub fn new() -> Self { |
| Self |
| } |
| } |
|
|
| impl Default for TauriLogBridgeLayer { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
|
|
| impl<S> Layer<S> for TauriLogBridgeLayer |
| where |
| S: Subscriber, |
| { |
| fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { |
| |
| if !LOG_BRIDGE_ENABLED.load(Ordering::Relaxed) { |
| return; |
| } |
|
|
| |
| let metadata = event.metadata(); |
| let level = match *metadata.level() { |
| Level::ERROR => "ERROR", |
| Level::WARN => "WARN", |
| Level::INFO => "INFO", |
| Level::DEBUG => "DEBUG", |
| Level::TRACE => "TRACE", |
| }; |
|
|
| |
| let mut visitor = FieldVisitor::new(); |
| event.record(&mut visitor); |
|
|
| |
| let message = visitor.message.unwrap_or_default(); |
|
|
| |
| if message.is_empty() && visitor.fields.is_empty() { |
| return; |
| } |
|
|
| |
| let entry = LogEntry { |
| id: LOG_ID_COUNTER.fetch_add(1, Ordering::SeqCst), |
| timestamp: chrono::Utc::now().timestamp_millis(), |
| level: level.to_string(), |
| target: metadata.target().to_string(), |
| message, |
| fields: visitor.fields, |
| }; |
|
|
| |
| { |
| let mut buffer = get_log_buffer().write(); |
| if buffer.len() >= MAX_BUFFER_SIZE { |
| buffer.pop_front(); |
| } |
| buffer.push_back(entry.clone()); |
| } |
|
|
| |
| if let Some(handle) = APP_HANDLE.get() { |
| let _ = handle.emit("log-event", entry); |
| } |
| } |
| } |
|
|
| |
| |
| |
|
|
| #[tauri::command] |
| pub fn enable_debug_console() { |
| enable_log_bridge(); |
| } |
|
|
| #[tauri::command] |
| pub fn disable_debug_console() { |
| disable_log_bridge(); |
| } |
|
|
| #[tauri::command] |
| pub fn is_debug_console_enabled() -> bool { |
| is_log_bridge_enabled() |
| } |
|
|
| #[tauri::command] |
| pub fn get_debug_console_logs() -> Vec<LogEntry> { |
| get_buffered_logs() |
| } |
|
|
| #[tauri::command] |
| pub fn clear_debug_console_logs() { |
| clear_log_buffer(); |
| } |
|
|