bitwarden_logging/
layer.rs1use std::sync::Arc;
4
5use tracing::{Event, Subscriber};
6use tracing_subscriber::{Layer, layer::Context};
7
8use crate::{CircularBuffer, FlightRecorderConfig, FlightRecorderEvent};
9
10pub 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 #[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 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 if metadata.target().starts_with("bitwarden_logging") {
57 return;
58 }
59
60 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 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}