p2p_chat/logging/
collector.rs

1//! This module provides a `tracing` layer for collecting log entries and sending
2//! them to the TUI.
3use super::LogBuffer;
4use crate::ui::LogEntry;
5use chrono::Utc;
6use std::sync::Arc;
7use tracing::{Event, Subscriber};
8use tracing_subscriber::{
9    layer::{Context, SubscriberExt},
10    registry::LookupSpan,
11    Layer,
12};
13
14/// A `tracing` layer that collects log entries and sends them to a `LogBuffer`.
15pub struct TUILogCollector {
16    buffer: Arc<LogBuffer>,
17}
18
19impl TUILogCollector {
20    /// Creates a new `TUILogCollector`.
21    ///
22    /// # Arguments
23    ///
24    /// * `buffer` - The `LogBuffer` to which log entries will be sent.
25    pub fn new(buffer: Arc<LogBuffer>) -> Self {
26        Self { buffer }
27    }
28
29    /// Initializes the `tracing` subscriber with the `TUILogCollector`.
30    ///
31    /// This sets up the global default subscriber for the application.
32    ///
33    /// # Arguments
34    ///
35    /// * `buffer` - The `LogBuffer` to use for collecting logs.
36    ///
37    /// # Errors
38    ///
39    /// This function will return an error if the global default subscriber cannot
40    /// be set.
41    pub fn init_subscriber(
42        buffer: Arc<LogBuffer>,
43    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
44        let collector = TUILogCollector::new(buffer);
45
46        // Create a layered subscriber with only TUI output (no console output).
47        let subscriber = tracing_subscriber::registry().with(collector);
48
49        tracing::subscriber::set_global_default(subscriber)?;
50        Ok(())
51    }
52}
53
54impl<S> Layer<S> for TUILogCollector
55where
56    S: Subscriber + for<'a> LookupSpan<'a>,
57{
58    /// Handles a `tracing` event.
59    ///
60    /// This function is called by the `tracing` subscriber whenever a new event
61    /// is recorded. It extracts the relevant information from the event, creates
62    /// a `LogEntry`, and adds it to the `LogBuffer`.
63    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
64        let metadata = event.metadata();
65
66        // Extract the message from the event.
67        let mut message = String::new();
68        let mut visitor = MessageVisitor(&mut message);
69        event.record(&mut visitor);
70
71        // Get the module path.
72        let target = metadata.target();
73        let module = if let Some(module_path) = metadata.module_path() {
74            // Extract the last component for cleaner display.
75            module_path
76                .split("::")
77                .last()
78                .unwrap_or(module_path)
79                .to_string()
80        } else {
81            target.to_string()
82        };
83
84        // Create log entry.
85        let entry = LogEntry {
86            timestamp: Utc::now(),
87            level: *metadata.level(),
88            module,
89            message,
90        };
91
92        // Add to buffer.
93        self.buffer.add_entry(entry);
94    }
95}
96
97/// A `tracing::field::Visit` implementation for extracting the message from an event.
98struct MessageVisitor<'a>(&'a mut String);
99
100impl<'a> tracing::field::Visit for MessageVisitor<'a> {
101    /// Records a debug-formatted value.
102    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
103        if field.name() == "message" {
104            *self.0 = format!("{:?}", value);
105        } else {
106            if !self.0.is_empty() {
107                self.0.push(' ');
108            }
109            self.0.push_str(&format!("{}={:?}", field.name(), value));
110        }
111    }
112
113    /// Records a string value.
114    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
115        if field.name() == "message" {
116            *self.0 = value.to_string();
117        } else {
118            if !self.0.is_empty() {
119                self.0.push(' ');
120            }
121            self.0.push_str(&format!("{}={}", field.name(), value));
122        }
123    }
124
125    /// Records an `i64` value.
126    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
127        if !self.0.is_empty() {
128            self.0.push(' ');
129        }
130        self.0.push_str(&format!("{}={}", field.name(), value));
131    }
132
133    /// Records a `u64` value.
134    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
135        if !self.0.is_empty() {
136            self.0.push(' ');
137        }
138        self.0.push_str(&format!("{}={}", field.name(), value));
139    }
140
141    /// Records a `bool` value.
142    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
143        if !self.0.is_empty() {
144            self.0.push(' ');
145        }
146        self.0.push_str(&format!("{}={}", field.name(), value));
147    }
148}