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