Skip to main content

fresh/services/
status_log.rs

1//! Status message log layer for tracing
2//!
3//! This module provides a custom tracing layer that captures status messages
4//! (those with target "status") to a separate file. This allows users to view
5//! the full history of status messages by clicking on the status bar.
6
7use std::fs::File;
8use std::io::Write;
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11use tracing_subscriber::layer::Context;
12use tracing_subscriber::registry::LookupSpan;
13use tracing_subscriber::Layer;
14
15/// A tracing layer that writes status messages to a file
16pub struct StatusLogLayer {
17    file: Arc<Mutex<File>>,
18}
19
20/// Handle returned from setup, containing the log path
21pub struct StatusLogHandle {
22    /// Path to the status log file
23    pub path: PathBuf,
24}
25
26/// Create a status log layer and handle
27///
28/// Returns the layer (to add to tracing subscriber) and a handle (to pass to editor)
29pub fn create() -> std::io::Result<(StatusLogLayer, StatusLogHandle)> {
30    create_with_path(super::log_dirs::status_log_path())
31}
32
33/// Create a status log layer with a specific path (for testing)
34pub fn create_with_path(path: PathBuf) -> std::io::Result<(StatusLogLayer, StatusLogHandle)> {
35    let file = File::create(&path)?;
36
37    let layer = StatusLogLayer {
38        file: Arc::new(Mutex::new(file)),
39    };
40
41    let handle = StatusLogHandle { path };
42
43    Ok((layer, handle))
44}
45
46impl<S> Layer<S> for StatusLogLayer
47where
48    S: tracing::Subscriber + for<'a> LookupSpan<'a>,
49{
50    fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
51        // Only capture events with target "status"
52        let target = event.metadata().target();
53        if target != "status" {
54            return;
55        }
56
57        // Format the event
58        let mut visitor = StringVisitor::default();
59        event.record(&mut visitor);
60
61        // Build the log line with timestamp
62        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
63        let line = format!("{} {}\n", timestamp, visitor.0);
64
65        // Write to file -- best-effort logging, nothing useful to do on failure.
66        #[allow(clippy::let_underscore_must_use)]
67        if let Ok(mut file) = self.file.lock() {
68            let _ = file.write_all(line.as_bytes());
69            let _ = file.flush();
70        }
71    }
72}
73
74/// Simple visitor to extract message from event
75#[derive(Default)]
76struct StringVisitor(String);
77
78impl tracing::field::Visit for StringVisitor {
79    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
80        if field.name() == "message" {
81            self.0 = format!("{:?}", value);
82        }
83    }
84
85    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
86        if field.name() == "message" {
87            self.0 = value.to_string();
88        }
89    }
90}