Skip to main content

libsurfer/
logs.rs

1use std::{collections::BTreeMap, sync::Mutex};
2
3use ecolor::Color32;
4use egui::{RichText, TextWrapMode};
5use egui_extras::{Column, TableBuilder, TableRow};
6use eyre::Result;
7use tracing::{
8    Level,
9    field::{Field, Visit},
10};
11use tracing_subscriber::Layer;
12
13use crate::{SystemState, message::Message};
14
15static RECORD_MUTEX: Mutex<Vec<LogMessage>> = Mutex::new(vec![]);
16static LOG_FILTER: Mutex<(bool, bool, bool, bool, bool)> =
17    Mutex::new((true, true, true, true, true));
18#[macro_export]
19macro_rules! try_log_error {
20    ($expr:expr, $what:expr $(,)?) => {
21        if let Err(e) = $expr {
22            error!("{}: {}", $what, e)
23        }
24    };
25}
26
27#[derive(Clone)]
28pub struct LogMessage {
29    pub name: String,
30    pub msg: String,
31    pub level: Level,
32}
33
34struct EguiLogger {}
35
36struct FieldVisitor<'a>(&'a mut BTreeMap<String, String>);
37
38impl Visit for FieldVisitor<'_> {
39    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
40        self.0
41            .insert(field.name().to_string(), format!("{value:?}"));
42    }
43}
44
45impl<S> Layer<S> for EguiLogger
46where
47    S: tracing::Subscriber,
48{
49    fn on_event(
50        &self,
51        event: &tracing::Event<'_>,
52        _ctx: tracing_subscriber::layer::Context<'_, S>,
53    ) {
54        let mut fields = BTreeMap::new();
55        event.record(&mut FieldVisitor(&mut fields));
56
57        RECORD_MUTEX
58            .lock()
59            .expect("Failed to lock logger. Thread poisoned?")
60            .push(LogMessage {
61                name: event.metadata().module_path().unwrap_or("-").to_string(),
62                msg: fields.get("message").cloned().unwrap_or("-".to_string()),
63                level: *event.metadata().level(),
64            });
65    }
66}
67
68impl SystemState {
69    pub fn draw_log_window(&self, ctx: &egui::Context, msgs: &mut Vec<Message>) {
70        let mut open = true;
71        egui::Window::new("Logs")
72            .open(&mut open)
73            .collapsible(true)
74            .resizable(true)
75            .show(ctx, |ui| {
76                {
77                    let mut filters = LOG_FILTER.lock().unwrap();
78                    ui.horizontal(|ui| {
79                        ui.checkbox(&mut filters.0, "Error");
80                        ui.checkbox(&mut filters.1, "Warn");
81                        ui.checkbox(&mut filters.2, "Info");
82                        ui.checkbox(&mut filters.3, "Debug");
83                        ui.checkbox(&mut filters.4, "Trace");
84                    });
85                }
86
87                ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
88
89                egui::ScrollArea::new([true, false]).show(ui, |ui| {
90                    TableBuilder::new(ui)
91                        .column(Column::auto().resizable(true))
92                        .column(Column::auto().resizable(true))
93                        .column(Column::remainder())
94                        .vscroll(true)
95                        .stick_to_bottom(true)
96                        .header(20.0, |mut header| {
97                            header.col(|ui| {
98                                ui.heading("Level");
99                            });
100                            header.col(|ui| {
101                                ui.heading("Source");
102                            });
103                            header.col(|ui| {
104                                ui.heading("Message");
105                            });
106                        })
107                        .body(|body| {
108                            let records = RECORD_MUTEX.lock().unwrap();
109                            let filters = LOG_FILTER.lock().unwrap();
110                            let filtered: Vec<&LogMessage> = records
111                                .iter()
112                                .filter(|record| match record.level {
113                                    Level::ERROR => filters.0,
114                                    Level::WARN => filters.1,
115                                    Level::INFO => filters.2,
116                                    Level::DEBUG => filters.3,
117                                    Level::TRACE => filters.4,
118                                })
119                                .collect();
120
121                            let heights = filtered
122                                .iter()
123                                .map(|record| record.msg.lines().count() as f32 * 15.0)
124                                .collect::<Vec<_>>();
125
126                            body.heterogeneous_rows(heights.into_iter(), |mut row: TableRow| {
127                                let record = filtered[row.index()];
128                                row.col(|ui| {
129                                    let (color, text) = match record.level {
130                                        Level::ERROR => (Color32::RED, "Error"),
131                                        Level::WARN => (Color32::YELLOW, "Warn"),
132                                        Level::INFO => (Color32::GREEN, "Info"),
133                                        Level::DEBUG => (Color32::BLUE, "Debug"),
134                                        Level::TRACE => (Color32::GRAY, "Trace"),
135                                    };
136
137                                    ui.colored_label(color, text);
138                                });
139                                row.col(|ui| {
140                                    ui.label(
141                                        RichText::new(record.name.clone())
142                                            .color(Color32::GRAY)
143                                            .monospace(),
144                                    );
145                                });
146                                row.col(|ui| {
147                                    ui.label(RichText::new(record.msg.clone()).monospace());
148                                });
149                            });
150                        });
151                })
152            });
153        if !open {
154            msgs.push(Message::SetLogsVisible(false));
155        }
156    }
157}
158
159/// Starts the logging and error handling. Can be used by unittests to get more insights.
160#[cfg(not(target_arch = "wasm32"))]
161pub fn start_logging() -> Result<()> {
162    use std::io::stdout;
163
164    use tracing_subscriber::{Registry, fmt, layer::SubscriberExt};
165
166    let filter =
167        tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
168    let subscriber = Registry::default()
169        .with(
170            fmt::layer()
171                .without_time()
172                .with_writer(stdout)
173                .with_filter(filter.clone()),
174        )
175        .with(EguiLogger {}.with_filter(filter));
176
177    tracing::subscriber::set_global_default(subscriber).expect("unable to set global subscriber");
178
179    Ok(())
180}
181
182/// Starts the logging and error handling. Can be used by unittests to get more insights.
183#[cfg(target_arch = "wasm32")]
184pub fn start_logging() -> Result<()> {
185    use tracing_subscriber::{Registry, fmt, layer::SubscriberExt};
186    use wasm_tracing::WasmLayer;
187
188    let filter =
189        tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
190    let subscriber = Registry::default()
191        .with(fmt::layer().without_time().with_filter(filter.clone()))
192        .with(WasmLayer::default())
193        .with(EguiLogger {}.with_filter(filter));
194
195    tracing::subscriber::set_global_default(subscriber).expect("unable to set global subscriber");
196
197    Ok(())
198}