Skip to main content

libsurfer/
logs.rs

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