|
| 1 | +// UI Logger - Custom tracing subscriber that sends logs to the UI |
| 2 | +// |
| 3 | +// This layer captures tracing events and forwards them to a channel |
| 4 | +// that the UI can consume to display logs in real-time. |
| 5 | + |
| 6 | +use std::sync::Arc; |
| 7 | + |
| 8 | +use chrono::Local; |
| 9 | +use tokio::sync::mpsc; |
| 10 | +use tracing::{Event, Level, Subscriber}; |
| 11 | +use tracing_subscriber::{layer::Context, Layer}; |
| 12 | + |
| 13 | +use crate::models::LogEntry; |
| 14 | + |
| 15 | +/// A tracing layer that sends log entries to the UI via a channel |
| 16 | +pub struct UiLogLayer { |
| 17 | + sender: Arc<mpsc::UnboundedSender<LogEntry>>, |
| 18 | +} |
| 19 | + |
| 20 | +impl UiLogLayer { |
| 21 | + /// Create a new UI log layer |
| 22 | + /// Returns the layer and a receiver for consuming log entries |
| 23 | + pub fn new() -> (Self, mpsc::UnboundedReceiver<LogEntry>) { |
| 24 | + let (sender, receiver) = mpsc::unbounded_channel(); |
| 25 | + let layer = Self { |
| 26 | + sender: Arc::new(sender), |
| 27 | + }; |
| 28 | + (layer, receiver) |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +impl<S> Layer<S> for UiLogLayer |
| 33 | +where |
| 34 | + S: Subscriber, |
| 35 | +{ |
| 36 | + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { |
| 37 | + // Extract log level |
| 38 | + let level = match *event.metadata().level() { |
| 39 | + Level::TRACE => "TRACE", |
| 40 | + Level::DEBUG => "DEBUG", |
| 41 | + Level::INFO => "INFO", |
| 42 | + Level::WARN => "WARN", |
| 43 | + Level::ERROR => "ERROR", |
| 44 | + }; |
| 45 | + |
| 46 | + // Extract target (module path) |
| 47 | + let target = event.metadata().target(); |
| 48 | + |
| 49 | + // Format timestamp |
| 50 | + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); |
| 51 | + |
| 52 | + // Extract message from event |
| 53 | + // Note: This is a simplified approach. For production, you'd want to |
| 54 | + // use a visitor pattern to properly extract all fields. |
| 55 | + let mut message = String::new(); |
| 56 | + event.record(&mut MessageVisitor { |
| 57 | + message: &mut message, |
| 58 | + }); |
| 59 | + |
| 60 | + // Create log entry |
| 61 | + let log_entry = LogEntry { |
| 62 | + timestamp, |
| 63 | + level: level.to_string(), |
| 64 | + target: target.to_string(), |
| 65 | + message, |
| 66 | + }; |
| 67 | + |
| 68 | + // Send to UI (ignore errors if receiver is dropped) |
| 69 | + let _ = self.sender.send(log_entry); |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +/// Visitor for extracting the message from a tracing event |
| 74 | +struct MessageVisitor<'a> { |
| 75 | + message: &'a mut String, |
| 76 | +} |
| 77 | + |
| 78 | +impl<'a> tracing::field::Visit for MessageVisitor<'a> { |
| 79 | + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { |
| 80 | + if field.name() == "message" { |
| 81 | + *self.message = format!("{:?}", value); |
| 82 | + // Remove quotes added by Debug formatting |
| 83 | + if self.message.starts_with('"') && self.message.ends_with('"') { |
| 84 | + *self.message = self.message[1..self.message.len() - 1].to_string(); |
| 85 | + } |
| 86 | + } else { |
| 87 | + // Append other fields to the message |
| 88 | + if !self.message.is_empty() { |
| 89 | + self.message.push_str(", "); |
| 90 | + } |
| 91 | + self.message.push_str(&format!("{}={:?}", field.name(), value)); |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/// Set up logging with UI layer |
| 97 | +/// Returns guards for file and console loggers, plus a receiver for UI logs |
| 98 | +pub fn setup_with_ui() -> anyhow::Result<( |
| 99 | + tracing_appender::non_blocking::WorkerGuard, |
| 100 | + tracing_appender::non_blocking::WorkerGuard, |
| 101 | + mpsc::UnboundedReceiver<LogEntry>, |
| 102 | +)> { |
| 103 | + use std::fs; |
| 104 | + |
| 105 | + use directories::ProjectDirs; |
| 106 | + use tracing_appender::non_blocking; |
| 107 | + use tracing_subscriber::{EnvFilter, fmt, prelude::*}; |
| 108 | + |
| 109 | + // Get platform-specific directories |
| 110 | + let proj_dirs = ProjectDirs::from("org", "xdsec", "wsrx-desktop-gpui") |
| 111 | + .ok_or_else(|| anyhow::anyhow!("Failed to get project directories"))?; |
| 112 | + |
| 113 | + let log_dir = proj_dirs.cache_dir(); |
| 114 | + fs::create_dir_all(log_dir)?; |
| 115 | + |
| 116 | + // Console logger |
| 117 | + let (console_non_blocking, console_guard) = non_blocking(std::io::stderr()); |
| 118 | + |
| 119 | + // File logger |
| 120 | + let file_appender = tracing_appender::rolling::daily(log_dir, "wsrx-desktop-gpui.log"); |
| 121 | + let (file_non_blocking, file_guard) = non_blocking(file_appender); |
| 122 | + |
| 123 | + // UI logger |
| 124 | + let (ui_layer, ui_receiver) = UiLogLayer::new(); |
| 125 | + |
| 126 | + // Set up the subscriber with console, file, and UI output |
| 127 | + tracing_subscriber::registry() |
| 128 | + .with( |
| 129 | + fmt::layer() |
| 130 | + .with_writer(console_non_blocking) |
| 131 | + .with_filter(EnvFilter::from_default_env()), |
| 132 | + ) |
| 133 | + .with( |
| 134 | + fmt::layer() |
| 135 | + .json() |
| 136 | + .with_writer(file_non_blocking) |
| 137 | + .with_filter(EnvFilter::from_default_env()), |
| 138 | + ) |
| 139 | + .with(ui_layer) |
| 140 | + .init(); |
| 141 | + |
| 142 | + tracing::info!("Logging initialized for wsrx-desktop-gpui with UI layer"); |
| 143 | + |
| 144 | + Ok((console_guard, file_guard, ui_receiver)) |
| 145 | +} |
0 commit comments