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