statsig_rust/console_capture/
console_capture_instances.rs1use 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 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}