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