Skip to content

Commit 6ecdd40

Browse files
CopilotGZTimeWalker
andcommitted
Add UI logger with tracing subscriber for real-time log streaming
Co-authored-by: GZTimeWalker <[email protected]>
1 parent 6ba213b commit 6ecdd40

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

crates/wsrx-desktop-gpui/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod icons;
1313
mod logging;
1414
mod models;
1515
mod styles;
16+
mod ui_logger;
1617
mod views;
1718

1819
// Initialize i18n at crate root with TOML locale files
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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

Comments
 (0)