Skip to main content

bitwarden_logging/
global.rs

1//! Global Flight Recorder buffer and convenience accessors.
2
3use std::sync::{Arc, OnceLock};
4
5use crate::{CircularBuffer, FlightRecorderConfig, FlightRecorderEvent, FlightRecorderLayer};
6
7/// Global Flight Recorder buffer, initialized during `init_sdk()`.
8static FLIGHT_RECORDER_BUFFER: OnceLock<Arc<CircularBuffer<FlightRecorderEvent>>> = OnceLock::new();
9
10/// Configured level floor for the global Flight Recorder, initialized during
11/// `init_sdk()`. Mirrors [`FlightRecorderLayer`]'s level so that direct writes
12/// via [`write_flight_recorder`] honor the same filter as the tracing layer.
13static FLIGHT_RECORDER_LEVEL: OnceLock<tracing::Level> = OnceLock::new();
14
15/// Initialize the global Flight Recorder.
16///
17/// Creates a [`FlightRecorderLayer`] and stores the buffer in a global
18/// [`OnceLock`] so it can be read from anywhere via [`read_flight_recorder`].
19/// Returns the layer to add to a tracing subscriber.
20///
21/// If called more than once, the second call's buffer is **not** stored
22/// globally (the `OnceLock` is already set), but the returned layer is
23/// still independently functional.
24#[must_use]
25pub fn init_flight_recorder(config: FlightRecorderConfig) -> FlightRecorderLayer {
26    let _ = FLIGHT_RECORDER_LEVEL.set(config.level);
27    let layer = FlightRecorderLayer::new(config);
28    let _ = FLIGHT_RECORDER_BUFFER.set(layer.buffer());
29    layer
30}
31
32/// Write a single externally-sourced event (e.g. from TypeScript) directly into
33/// the global buffer, honoring the configured level floor.
34///
35/// This mirrors [`FlightRecorderLayer`]'s filter so that events routed around
36/// the tracing pipeline are subject to the same level check. Events more verbose
37/// than the configured floor are dropped. No-op if [`init_flight_recorder`] has
38/// not been called.
39pub fn write_flight_recorder(event: FlightRecorderEvent, level: tracing::Level) {
40    // Mirrors the layer's check: skip events more verbose than the configured level.
41    if let Some(floor) = FLIGHT_RECORDER_LEVEL.get()
42        && level > *floor
43    {
44        return;
45    }
46    if let Some(buffer) = get_flight_recorder_buffer() {
47        buffer.push(event);
48    }
49}
50
51/// Get the global Flight Recorder buffer.
52///
53/// Returns `None` if [`init_flight_recorder`] has not been called.
54pub fn get_flight_recorder_buffer() -> Option<Arc<CircularBuffer<FlightRecorderEvent>>> {
55    FLIGHT_RECORDER_BUFFER.get().cloned()
56}
57
58/// Read all events from the global Flight Recorder buffer.
59///
60/// Returns an empty `Vec` if [`init_flight_recorder`] has not been called.
61#[must_use]
62pub fn read_flight_recorder() -> Vec<FlightRecorderEvent> {
63    get_flight_recorder_buffer()
64        .map(|buffer| buffer.read())
65        .unwrap_or_default()
66}
67
68/// Get the current event count without reading event contents.
69///
70/// Returns `0` if [`init_flight_recorder`] has not been called.
71#[must_use]
72pub fn flight_recorder_count() -> usize {
73    get_flight_recorder_buffer()
74        .map(|buffer| buffer.len())
75        .unwrap_or(0)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_config_default_values() {
84        let config = FlightRecorderConfig::default();
85        assert_eq!(config.buffer_size.get(), 1000);
86        assert_eq!(config.level, tracing::Level::DEBUG);
87    }
88
89    #[test]
90    fn test_read_before_init_returns_empty() {
91        // A fresh OnceLock (not the global one, which may already be set
92        // by other tests) would return None. We can at least verify the
93        // convenience functions don't panic.
94        let events = read_flight_recorder();
95        // Either empty (not initialized) or non-empty (another test initialized it)
96        let _ = events;
97    }
98
99    fn event(marker: &str, level: &str) -> FlightRecorderEvent {
100        FlightRecorderEvent {
101            timestamp: 0,
102            level: level.to_string(),
103            target: "test::module".to_string(),
104            message: marker.to_string(),
105            fields: std::collections::HashMap::new(),
106        }
107    }
108
109    #[test]
110    fn test_write_flight_recorder_respects_level_floor() {
111        // This is the only test in the crate that initializes the global
112        // recorder, so it controls the shared level/buffer for this binary.
113        let config = FlightRecorderConfig::new(
114            std::num::NonZeroUsize::new(100).expect("non-zero"),
115            tracing::Level::INFO,
116        );
117        let _ = init_flight_recorder(config);
118
119        // Below the floor (more verbose than INFO) is dropped.
120        write_flight_recorder(event("fr-drop-debug", "DEBUG"), tracing::Level::DEBUG);
121        // At or above the floor is captured.
122        write_flight_recorder(event("fr-keep-info", "INFO"), tracing::Level::INFO);
123        write_flight_recorder(event("fr-keep-error", "ERROR"), tracing::Level::ERROR);
124
125        let messages: Vec<String> = read_flight_recorder()
126            .into_iter()
127            .map(|e| e.message)
128            .collect();
129        assert!(messages.contains(&"fr-keep-info".to_string()));
130        assert!(messages.contains(&"fr-keep-error".to_string()));
131        assert!(!messages.contains(&"fr-drop-debug".to_string()));
132    }
133}