Skip to main content

iroh_services/
alerts.rs

1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use tokio::sync::mpsc;
5use tracing::field::{Field, Visit};
6use tracing_subscriber::Layer;
7
8use crate::protocol::{AlertInfo, LogEntry};
9
10const CONTEXT_BUFFER_SIZE: usize = 200;
11
12/// A [`tracing_subscriber::Layer`] that captures ERROR-level log events from
13/// the `iroh` crate and forwards them to the n0des cloud via the client actor.
14///
15/// All log events (any level, any target) are recorded into a 200-entry ring
16/// buffer. When an ERROR from the `iroh` crate fires, the buffered context is
17/// drained and sent alongside the alert.
18///
19/// Returned by [`Client::enable_alerts`]. The caller must install this layer
20/// into their tracing subscriber stack for alerts to fire.
21///
22/// [`Client::enable_alerts`]: crate::Client::enable_alerts
23#[derive(Debug)]
24pub struct LogMonitor {
25    tx: mpsc::Sender<AlertInfo>,
26    context_buffer: Mutex<VecDeque<LogEntry>>,
27}
28
29impl LogMonitor {
30    pub(crate) fn new(tx: mpsc::Sender<AlertInfo>) -> Self {
31        Self {
32            tx,
33            context_buffer: Mutex::new(VecDeque::with_capacity(CONTEXT_BUFFER_SIZE)),
34        }
35    }
36}
37
38impl<S: tracing::Subscriber> Layer<S> for LogMonitor {
39    fn on_event(
40        &self,
41        event: &tracing::Event<'_>,
42        _ctx: tracing_subscriber::layer::Context<'_, S>,
43    ) {
44        let meta = event.metadata();
45        let level = *meta.level();
46
47        let mut visitor = MessageVisitor::default();
48        event.record(&mut visitor);
49        let message = visitor.message.unwrap_or_default();
50
51        let timestamp_ms = std::time::SystemTime::now()
52            .duration_since(std::time::UNIX_EPOCH)
53            .unwrap_or_default()
54            .as_millis() as u64;
55
56        // Record every event into the ring buffer for context.
57        let entry = LogEntry {
58            level: level.to_string(),
59            target: meta.target().to_string(),
60            message: message.clone(),
61            timestamp_ms,
62        };
63
64        let mut buf = self
65            .context_buffer
66            .lock()
67            .unwrap_or_else(|e| e.into_inner());
68        if buf.len() >= CONTEXT_BUFFER_SIZE {
69            buf.pop_front();
70        }
71        buf.push_back(entry);
72
73        // Only fire an alert for ERROR-level events from iroh targets.
74        if level != tracing::Level::ERROR || !meta.target().starts_with("iroh") {
75            return;
76        }
77
78        let context: Vec<LogEntry> = buf.drain(..).collect();
79
80        let alert = AlertInfo {
81            target: meta.target().to_string(),
82            message,
83            file: meta.file().map(String::from),
84            line: meta.line(),
85            timestamp_ms,
86            iroh_version: crate::IROH_VERSION.to_string(),
87            iroh_n0des_version: crate::IROH_N0DES_VERSION.to_string(),
88            context,
89        };
90
91        // Non-blocking send. If the channel is full the alert is dropped —
92        // alerting is best-effort and must never block the caller's thread.
93        let _ = self.tx.try_send(alert);
94    }
95}
96
97#[derive(Default)]
98struct MessageVisitor {
99    message: Option<String>,
100}
101
102impl Visit for MessageVisitor {
103    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
104        if field.name() == "message" {
105            self.message = Some(format!("{:?}", value));
106        }
107    }
108
109    fn record_str(&mut self, field: &Field, value: &str) {
110        if field.name() == "message" {
111            self.message = Some(value.to_string());
112        }
113    }
114}