Skip to main content

statsig_rust/console_capture/
console_capture_instances.rs

1use lazy_static::lazy_static;
2use std::{
3    collections::HashMap,
4    sync::{Arc, Weak},
5};
6
7use crate::console_capture::console_log_line_levels::StatsigLogLineLevel;
8use parking_lot::RwLock;
9
10use crate::{
11    log_e, observability::ops_stats::OpsStatsForInstance, user::user_data::UserDataMap,
12    user::StatsigUserLoggable, DynamicValue, StatsigOptions, OPS_STATS,
13};
14
15const TAG: &str = stringify!(ConsoleCaptureRegistry);
16
17lazy_static! {
18    pub static ref CONSOLE_CAPTURE_REGISTRY: ConsoleCaptureRegistry = ConsoleCaptureRegistry {
19        instances_map: RwLock::new(HashMap::new())
20    };
21}
22
23pub struct ConsoleCaptureInstance {
24    enabled: bool,
25    allowed_log_levels: Vec<StatsigLogLineLevel>,
26    ops_stats_instance: Arc<OpsStatsForInstance>,
27    console_capture_user: StatsigUserLoggable,
28}
29
30pub struct ConsoleCaptureRegistry {
31    instances_map: RwLock<HashMap<String, Weak<ConsoleCaptureInstance>>>,
32}
33
34impl ConsoleCaptureInstance {
35    pub fn new(
36        sdk_key: &str,
37        statsig_options: &StatsigOptions,
38        environment: &Option<HashMap<String, DynamicValue>>,
39    ) -> Self {
40        let console_capture_options = statsig_options
41            .console_capture_options
42            .clone()
43            .unwrap_or_default();
44        let enabled = console_capture_options.enabled;
45        let ops_stats_instance = OPS_STATS.get_for_instance(sdk_key);
46        let allowed_log_levels = console_capture_options
47            .log_levels
48            .filter(|levels| !levels.is_empty())
49            .unwrap_or(vec![StatsigLogLineLevel::Warn, StatsigLogLineLevel::Error]);
50
51        let loggable_user = if let Some(console_capture_user) = console_capture_options.user {
52            StatsigUserLoggable::new(
53                &console_capture_user.data,
54                to_user_data_map(environment),
55                statsig_options.global_custom_fields.clone(),
56            )
57        } else {
58            StatsigUserLoggable::default_console_capture_user(
59                to_user_data_map(environment),
60                statsig_options.global_custom_fields.clone(),
61            )
62        };
63
64        Self {
65            enabled,
66            ops_stats_instance,
67            allowed_log_levels,
68            console_capture_user: loggable_user,
69        }
70    }
71
72    pub fn is_enabled(&self) -> bool {
73        self.enabled
74    }
75}
76
77fn to_user_data_map(environment: &Option<HashMap<String, DynamicValue>>) -> Option<UserDataMap> {
78    environment.as_ref().map(|environment| {
79        environment
80            .iter()
81            .map(|(key, value)| (key.clone(), value.clone()))
82            .collect()
83    })
84}
85
86impl ConsoleCaptureRegistry {
87    pub fn get_for_instance(
88        &self,
89        sdk_key: &str,
90        options: &StatsigOptions,
91        environment: &Option<HashMap<String, DynamicValue>>,
92    ) -> Arc<ConsoleCaptureInstance> {
93        match self
94            .instances_map
95            .try_read_for(std::time::Duration::from_secs(5))
96        {
97            Some(read_guard) => {
98                if let Some(instance) = read_guard.get(sdk_key) {
99                    if let Some(instance) = instance.upgrade() {
100                        return instance.clone();
101                    }
102                }
103            }
104            None => {
105                log_e!(
106                    TAG,
107                    "Failed to get read guard: Failed to lock instances_map"
108                );
109            }
110        }
111
112        let instance = Arc::new(ConsoleCaptureInstance::new(sdk_key, options, environment));
113        match self
114            .instances_map
115            .try_write_for(std::time::Duration::from_secs(5))
116        {
117            Some(mut write_guard) => {
118                write_guard.insert(sdk_key.into(), Arc::downgrade(&instance));
119            }
120            None => {
121                log_e!(
122                    TAG,
123                    "Failed to get write guard: Failed to lock instances_map"
124                );
125            }
126        }
127
128        instance
129    }
130
131    /// Helper method to get a strong reference to an instance
132    fn get_and_upgrade_instance_for_key(
133        &self,
134        sdk_key: &str,
135    ) -> Option<Arc<ConsoleCaptureInstance>> {
136        match self
137            .instances_map
138            .try_read_for(std::time::Duration::from_secs(5))
139        {
140            Some(read_guard) => read_guard.get(sdk_key).cloned()?.upgrade(),
141            None => None,
142        }
143    }
144
145    pub fn is_enabled(&self, sdk_key: &str) -> bool {
146        let instance = self.get_and_upgrade_instance_for_key(sdk_key);
147
148        instance
149            .map(|instance| instance.is_enabled())
150            .unwrap_or(false)
151    }
152
153    pub fn enqueue_console_capture_event(
154        &self,
155        sdk_key: &str,
156        level: String,
157        payload: Vec<String>,
158        timestamp: u64,
159        stack_trace: Option<String>,
160    ) {
161        if !self.is_enabled(sdk_key) {
162            return;
163        }
164
165        if is_internal_log(&payload) {
166            return;
167        }
168
169        let Some(log_level) = StatsigLogLineLevel::from_string(&level) else {
170            log_e!(TAG, "Failed to parse log level: {}", level);
171            return;
172        };
173
174        let Some(instance) = self.get_and_upgrade_instance_for_key(sdk_key) else {
175            log_e!(
176                TAG,
177                "Failed to get and upgrade instance for key: {}",
178                sdk_key
179            );
180            return;
181        };
182
183        if !instance.allowed_log_levels.contains(&log_level) {
184            log_e!(TAG, "Log level not allowed: {:?}", log_level);
185            return;
186        }
187
188        let user = instance.console_capture_user.clone();
189
190        instance.ops_stats_instance.enqueue_console_capture_event(
191            level,
192            payload,
193            timestamp,
194            user,
195            stack_trace,
196        );
197    }
198}
199
200fn is_internal_log(payload: &[String]) -> bool {
201    payload
202        .iter()
203        .any(|p| p.contains("Statsig::") || p.contains("[Statsig]"))
204}