statsig_rust/observability/
ops_stats.rs1use 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
27lazy_static! {
29 pub static ref OPS_STATS: OpsStats = OpsStats::new();
30}
31
32pub struct OpsStats {
33 instances_map: RwLock<HashMap<String, Weak<OpsStatsForInstance>>>, }
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
102impl 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}