Skip to main content

statsig_rust/observability/
ops_stats.rs

1use super::{
2    observability_client_adapter::{MetricType, ObservabilityEvent},
3    sdk_errors_observer::ErrorBoundaryEvent,
4    DiagnosticsEvent,
5};
6use crate::user::StatsigUserLoggable;
7use crate::{log_e, log_w, StatsigRuntime};
8use crate::{
9    observability::console_capture_observer::ConsoleCaptureEvent,
10    sdk_diagnostics::{
11        diagnostics::ContextType,
12        marker::{KeyType, Marker},
13    },
14};
15use async_trait::async_trait;
16use lazy_static::lazy_static;
17use parking_lot::RwLock;
18use std::{
19    collections::HashMap,
20    sync::{Arc, Weak},
21};
22use tokio::sync::broadcast::{self, Sender};
23use tokio::sync::Notify;
24
25const TAG: &str = stringify!(OpsStats);
26
27/* Ideally we don't need to pass OpsStats around, but right now I could find a good way to do it to support multiple instances*/
28lazy_static! {
29    pub static ref OPS_STATS: OpsStats = OpsStats::new();
30}
31
32pub struct OpsStats {
33    instances_map: RwLock<HashMap<String, Weak<OpsStatsForInstance>>>, // key is sdk key
34}
35
36impl Default for OpsStats {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl OpsStats {
43    pub fn new() -> Self {
44        OpsStats {
45            instances_map: HashMap::new().into(),
46        }
47    }
48
49    pub fn get_for_instance(&self, sdk_key: &str) -> Arc<OpsStatsForInstance> {
50        match self
51            .instances_map
52            .try_read_for(std::time::Duration::from_secs(5))
53        {
54            Some(read_guard) => {
55                if let Some(instance) = read_guard.get(sdk_key) {
56                    if let Some(instance) = instance.upgrade() {
57                        return instance.clone();
58                    }
59                }
60            }
61            None => {
62                log_e!(
63                    TAG,
64                    "Failed to get read guard: Failed to lock instances_map"
65                );
66            }
67        }
68
69        let instance = Arc::new(OpsStatsForInstance::new());
70        match self
71            .instances_map
72            .try_write_for(std::time::Duration::from_secs(5))
73        {
74            Some(mut write_guard) => {
75                write_guard.insert(sdk_key.into(), Arc::downgrade(&instance));
76            }
77            None => {
78                log_e!(
79                    TAG,
80                    "Failed to get write guard: Failed to lock instances_map"
81                );
82            }
83        }
84
85        instance
86    }
87}
88
89#[derive(Clone)]
90pub enum OpsStatsEvent {
91    Observability(ObservabilityEvent),
92    SDKError(ErrorBoundaryEvent),
93    Diagnostics(DiagnosticsEvent),
94    ConsoleCapture(ConsoleCaptureEvent),
95}
96
97pub struct OpsStatsForInstance {
98    sender: Sender<OpsStatsEvent>,
99    shutdown_notify: Arc<Notify>,
100}
101
102// The class used to handle all observability events including diagnostics, error, event logging, and external metric sharing
103impl Default for OpsStatsForInstance {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl OpsStatsForInstance {
110    pub fn new() -> Self {
111        let (tx, _) = broadcast::channel(1000);
112        OpsStatsForInstance {
113            sender: tx,
114            shutdown_notify: Arc::new(Notify::new()),
115        }
116    }
117
118    #[cfg(test)]
119    pub(crate) fn subscribe_for_test(&self) -> broadcast::Receiver<OpsStatsEvent> {
120        self.sender.subscribe()
121    }
122
123    pub fn log(&self, event: OpsStatsEvent) {
124        match self.sender.send(event) {
125            Ok(_) => {}
126            Err(e) => {
127                log_w!(
128                    "OpsStats Message Queue",
129                    "Dropping ops stats event {}",
130                    e.to_string()
131                );
132            }
133        }
134    }
135
136    pub fn log_error(&self, error: ErrorBoundaryEvent) {
137        self.log(OpsStatsEvent::SDKError(error));
138    }
139
140    pub fn log_checksum_validation_result(&self, is_success: bool) {
141        self.log(ObservabilityEvent::new_event(
142            MetricType::Increment,
143            "deltas_checksum_validation.count".to_string(),
144            1.0,
145            Some(HashMap::from([(
146                "result".to_string(),
147                if is_success { "success" } else { "failure" }.to_string(),
148            )])),
149        ));
150    }
151
152    pub fn add_marker(&self, marker: Marker, context: Option<ContextType>) {
153        self.log(OpsStatsEvent::Diagnostics(DiagnosticsEvent {
154            marker: Some(marker),
155            context,
156            key: None,
157            should_enqueue: false,
158        }));
159    }
160
161    pub fn set_diagnostics_context(&self, context: ContextType) {
162        self.log(OpsStatsEvent::Diagnostics(DiagnosticsEvent {
163            marker: None,
164            context: Some(context),
165            key: None,
166            should_enqueue: false,
167        }));
168    }
169
170    pub fn enqueue_diagnostics_event(&self, key: Option<KeyType>, context: Option<ContextType>) {
171        self.log(OpsStatsEvent::Diagnostics(DiagnosticsEvent {
172            marker: None,
173            context,
174            key,
175            should_enqueue: true,
176        }));
177    }
178
179    pub fn enqueue_console_capture_event(
180        &self,
181        level: String,
182        payload: Vec<String>,
183        timestamp: u64,
184        user: StatsigUserLoggable,
185        stack_trace: Option<String>,
186    ) {
187        self.log(OpsStatsEvent::ConsoleCapture(ConsoleCaptureEvent {
188            level,
189            payload,
190            timestamp,
191            user,
192            stack_trace,
193        }));
194    }
195
196    pub fn subscribe(
197        &self,
198        runtime: Arc<StatsigRuntime>,
199        observer: Weak<dyn OpsStatsEventObserver>,
200    ) {
201        let mut rx = self.sender.subscribe();
202        let shutdown_notify = self.shutdown_notify.clone();
203        let _ = runtime.spawn("opts_stats_listen_for", |rt_shutdown_notify| async move {
204            loop {
205                tokio::select! {
206                    event = rx.recv() => {
207                        let observer = match observer.upgrade() {
208                            Some(observer) => observer,
209                            None => break,
210                        };
211
212                        if let Ok(event) = event {
213                            observer.handle_event(event).await;
214                        }
215                    }
216                    () = rt_shutdown_notify.notified() => {
217                        break;
218                    }
219                    () = shutdown_notify.notified() => {
220                        break;
221                    }
222                }
223            }
224        });
225    }
226}
227
228impl Drop for OpsStatsForInstance {
229    fn drop(&mut self) {
230        self.shutdown_notify.notify_waiters();
231    }
232}
233
234#[async_trait]
235pub trait OpsStatsEventObserver: Send + Sync + 'static {
236    async fn handle_event(&self, event: OpsStatsEvent);
237}