Skip to main content

ravenrs_movie/
errors.rs

1//! The last errors of the app, to go along with a bug report.
2
3use std::collections::VecDeque;
4use std::fmt::{self, Write as _};
5use std::sync::{Arc, Mutex};
6
7use tracing::field::{Field, Visit};
8use tracing::{Event, Level, Subscriber};
9use tracing_subscriber::layer::{Context, Layer};
10
11/// How many it keeps: what led to a bug is in the last ones.
12const KEPT: usize = 30;
13
14/// Keeps the last warnings and errors logged with `tracing`, in memory only, to give
15/// to [`BugReporter::errors`](crate::BugReporter::errors), where the person sees them
16/// before anything goes. Add it to the app's subscriber as a layer.
17///
18/// ```
19/// use ravenrs_movie::RecentErrors;
20/// use tracing_subscriber::layer::SubscriberExt as _;
21///
22/// let errors = RecentErrors::default();
23/// let subscriber = tracing_subscriber::registry().with(errors.clone());
24/// tracing::subscriber::with_default(subscriber, || tracing::error!(code = 7, "could not save"));
25/// assert_eq!(errors.lines(), ["could not save code=7"]);
26/// ```
27#[derive(Clone, Debug, Default)]
28#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
29pub struct RecentErrors(Arc<Mutex<VecDeque<String>>>);
30
31#[cfg_attr(feature = "live", ravenrs_live::expose)]
32impl RecentErrors {
33    /// What it has, oldest first, with the person's home folder written as `~`, so
34    /// that a path does not say whose computer this is.
35    pub fn lines(&self) -> Vec<String> {
36        let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap_or_default();
37        let Ok(lines) = self.0.lock() else {
38            return Vec::new();
39        };
40        lines
41            .iter()
42            .map(|line| if home.is_empty() { line.clone() } else { line.replace(&home, "~") })
43            .collect()
44    }
45}
46
47/// The message first, then the fields.
48#[derive(Default)]
49struct Line {
50    message: String,
51    fields: String,
52}
53
54impl Visit for Line {
55    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
56        if field.name() == "message" {
57            let _ = write!(self.message, "{value:?}");
58        } else {
59            let _ = write!(self.fields, " {}={value:?}", field.name());
60        }
61    }
62
63    fn record_str(&mut self, field: &Field, value: &str) {
64        if field.name() == "message" {
65            self.message.push_str(value);
66        } else {
67            let _ = write!(self.fields, " {}={value}", field.name());
68        }
69    }
70}
71
72impl<S: Subscriber> Layer<S> for RecentErrors {
73    fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
74        if *event.metadata().level() > Level::WARN {
75            return;
76        }
77        let mut line = Line::default();
78        event.record(&mut line);
79        // Logging from a thread that panicked holding it is not worth a second panic.
80        if let Ok(mut lines) = self.0.lock() {
81            if lines.len() == KEPT {
82                lines.pop_front();
83            }
84            lines.push_back(line.message + &line.fields);
85        }
86    }
87}