Skip to main content

bitwarden_logging/
layer.rs

1//! Tracing subscriber layer for Flight Recorder.
2
3use std::sync::Arc;
4
5use tracing::{Event, Subscriber};
6use tracing_subscriber::{Layer, layer::Context};
7
8use crate::{CircularBuffer, FlightRecorderConfig, FlightRecorderEvent};
9
10/// A tracing subscriber layer that captures log events into a circular buffer.
11///
12/// This layer intercepts tracing events and stores them in a thread-safe
13/// buffer for later export. Events from the `bitwarden_logging` target
14/// are filtered out to prevent infinite recursion.
15pub struct FlightRecorderLayer {
16    buffer: Arc<CircularBuffer<FlightRecorderEvent>>,
17    level: tracing::Level,
18}
19
20impl std::fmt::Debug for FlightRecorderLayer {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("FlightRecorderLayer")
23            .field("level", &self.level)
24            .field("buffer", &self.buffer)
25            .finish()
26    }
27}
28
29impl FlightRecorderLayer {
30    /// Create a new FlightRecorderLayer with the given configuration.
31    #[must_use]
32    pub fn new(config: FlightRecorderConfig) -> Self {
33        let buffer = Arc::new(CircularBuffer::new(config.buffer_size));
34        Self {
35            buffer,
36            level: config.level,
37        }
38    }
39
40    /// Get a reference to the underlying buffer.
41    ///
42    /// This can be used to access the buffer for reading captured events.
43    pub fn buffer(&self) -> Arc<CircularBuffer<FlightRecorderEvent>> {
44        Arc::clone(&self.buffer)
45    }
46}
47
48impl<S> Layer<S> for FlightRecorderLayer
49where
50    S: Subscriber,
51{
52    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
53        let metadata = event.metadata();
54
55        // Filter out our own logging to prevent recursion
56        if metadata.target().starts_with("bitwarden_logging") {
57            return;
58        }
59
60        // Skip events more verbose than configured level
61        if *metadata.level() > self.level {
62            return;
63        }
64
65        self.buffer.push(event.into());
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use std::num::NonZeroUsize;
72
73    use tracing_subscriber::layer::SubscriberExt;
74
75    use super::*;
76
77    #[test]
78    fn test_layer_creation() {
79        let config = FlightRecorderConfig::default();
80        let layer = FlightRecorderLayer::new(config);
81        let buffer = layer.buffer();
82
83        assert!(buffer.is_empty());
84        assert_eq!(buffer.len(), 0);
85    }
86
87    #[test]
88    fn test_layer_captures_events() {
89        let config = FlightRecorderConfig::default();
90        let layer = FlightRecorderLayer::new(config);
91        let buffer = layer.buffer();
92
93        let subscriber = tracing_subscriber::registry().with(layer);
94        tracing::subscriber::with_default(subscriber, || {
95            tracing::info!(target: "test::module", "hello from test");
96        });
97
98        let events = buffer.read();
99        assert_eq!(events.len(), 1);
100        assert_eq!(events[0].message, "hello from test");
101        assert_eq!(events[0].level, "INFO");
102    }
103
104    #[test]
105    fn test_layer_filters_own_crate() {
106        let config = FlightRecorderConfig::default();
107        let layer = FlightRecorderLayer::new(config);
108        let buffer = layer.buffer();
109
110        let subscriber = tracing_subscriber::registry().with(layer);
111        tracing::subscriber::with_default(subscriber, || {
112            tracing::info!(target: "bitwarden_logging::internal", "should be skipped");
113            tracing::info!(target: "bitwarden_logging", "should be skipped");
114            tracing::info!("should be skipped");
115        });
116
117        assert!(buffer.is_empty());
118    }
119
120    #[test]
121    fn test_layer_filters_by_level() {
122        let config = FlightRecorderConfig::new(
123            NonZeroUsize::new(100).expect("non-zero"),
124            tracing::Level::INFO,
125        );
126        let layer = FlightRecorderLayer::new(config);
127        let buffer = layer.buffer();
128
129        let subscriber = tracing_subscriber::registry().with(layer);
130        tracing::subscriber::with_default(subscriber, || {
131            tracing::debug!(target: "test::module", "should be skipped");
132            tracing::info!(target: "test::module", "should be captured");
133            tracing::warn!(target: "test::module", "should also be captured");
134        });
135
136        let events = buffer.read();
137        assert_eq!(events.len(), 2);
138        assert_eq!(events[0].level, "INFO");
139        assert_eq!(events[1].level, "WARN");
140    }
141
142    #[test]
143    fn test_buffer_shared_via_arc() {
144        let config = FlightRecorderConfig::default();
145        let layer = FlightRecorderLayer::new(config);
146        let buffer1 = layer.buffer();
147        let buffer2 = layer.buffer();
148
149        assert!(Arc::ptr_eq(&buffer1, &buffer2));
150
151        let subscriber = tracing_subscriber::registry().with(layer);
152        tracing::subscriber::with_default(subscriber, || {
153            tracing::warn!(target: "test::module", "shared buffer test");
154        });
155
156        // Both handles see the same events
157        assert_eq!(buffer1.read().len(), 1);
158        assert_eq!(buffer2.read().len(), 1);
159    }
160
161    #[test]
162    fn test_layer_captures_structured_fields() {
163        let config = FlightRecorderConfig::default();
164        let layer = FlightRecorderLayer::new(config);
165        let buffer = layer.buffer();
166
167        let subscriber = tracing_subscriber::registry().with(layer);
168        tracing::subscriber::with_default(subscriber, || {
169            tracing::info!(target: "test::module", user_id = "abc-123", "login attempt");
170        });
171
172        let events = buffer.read();
173        assert_eq!(events.len(), 1);
174        assert_eq!(events[0].message, "login attempt");
175        assert_eq!(events[0].target, "test::module");
176        assert_eq!(
177            events[0].fields.get("user_id"),
178            Some(&"abc-123".to_string())
179        );
180    }
181}