statsig_rust/
output_logger.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, RwLock};
3
4use log::{debug, error, info, warn, Level};
5
6const MAX_CHARS: usize = 400;
7const TRUNCATED_SUFFIX: &str = "...[TRUNCATED]";
8
9const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warn;
10
11lazy_static::lazy_static! {
12    static ref LOGGER_STATE: RwLock<LoggerState> = RwLock::new(LoggerState {
13        level: DEFAULT_LOG_LEVEL,
14        provider: None,
15    });
16}
17
18struct LoggerState {
19    level: LogLevel,
20    provider: Option<Arc<dyn OutputLogProvider>>,
21}
22
23static INITIALIZED: AtomicBool = AtomicBool::new(false);
24
25#[derive(Clone, Debug)]
26pub enum LogLevel {
27    None,
28    Debug,
29    Info,
30    Warn,
31    Error,
32}
33
34impl From<&str> for LogLevel {
35    fn from(level: &str) -> Self {
36        match level.to_lowercase().as_str() {
37            "debug" => LogLevel::Debug,
38            "info" => LogLevel::Info,
39            "warn" => LogLevel::Warn,
40            "error" => LogLevel::Error,
41            "none" => LogLevel::None,
42            _ => DEFAULT_LOG_LEVEL,
43        }
44    }
45}
46
47impl From<u32> for LogLevel {
48    fn from(level: u32) -> Self {
49        match level {
50            0 => LogLevel::None,
51            1 => LogLevel::Error,
52            2 => LogLevel::Warn,
53            3 => LogLevel::Info,
54            4 => LogLevel::Debug,
55            _ => DEFAULT_LOG_LEVEL,
56        }
57    }
58}
59
60impl LogLevel {
61    fn to_third_party_level(&self) -> Option<Level> {
62        match self {
63            LogLevel::Debug => Some(Level::Debug),
64            LogLevel::Info => Some(Level::Info),
65            LogLevel::Warn => Some(Level::Warn),
66            LogLevel::Error => Some(Level::Error),
67            LogLevel::None => None,
68        }
69    }
70
71    fn to_number(&self) -> u32 {
72        match self {
73            LogLevel::Debug => 4,
74            LogLevel::Info => 3,
75            LogLevel::Warn => 2,
76            LogLevel::Error => 1,
77            LogLevel::None => 0,
78        }
79    }
80}
81
82pub trait OutputLogProvider: Send + Sync {
83    fn initialize(&self);
84    fn debug(&self, tag: &str, msg: String);
85    fn info(&self, tag: &str, msg: String);
86    fn warn(&self, tag: &str, msg: String);
87    fn error(&self, tag: &str, msg: String);
88    fn shutdown(&self);
89}
90
91pub fn initialize_output_logger(
92    level: &Option<LogLevel>,
93    provider: Option<Arc<dyn OutputLogProvider>>,
94) {
95    let was_initialized = INITIALIZED.swap(true, Ordering::SeqCst);
96    if was_initialized {
97        return;
98    }
99
100    let mut state = match LOGGER_STATE.write() {
101        Ok(state) => state,
102        Err(e) => {
103            eprintln!("[Statsig] Failed to acquire write lock for logger: {e}");
104            return;
105        }
106    };
107    let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
108    state.level = level.clone();
109
110    if let Some(provider_impl) = provider {
111        provider_impl.initialize();
112        state.provider = Some(provider_impl);
113    } else {
114        let final_level = match level {
115            LogLevel::None => {
116                return;
117            }
118            _ => match level.to_third_party_level() {
119                Some(level) => level,
120                None => return,
121            },
122        };
123
124        match simple_logger::init_with_level(final_level) {
125            Ok(()) => {}
126            Err(_) => {
127                log::set_max_level(final_level.to_level_filter());
128            }
129        }
130    }
131}
132
133pub fn shutdown_output_logger() {
134    let mut state = match LOGGER_STATE.write() {
135        Ok(state) => state,
136        Err(e) => {
137            eprintln!("[Statsig] Failed to acquire write lock for logger: {e}");
138            return;
139        }
140    };
141
142    if let Some(provider) = &mut state.provider {
143        provider.shutdown();
144    }
145
146    INITIALIZED.store(false, Ordering::SeqCst);
147}
148
149pub fn log_message(tag: &str, level: LogLevel, msg: String) {
150    let truncated_msg = if msg.chars().count() > MAX_CHARS {
151        let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
152        format!(
153            "{}{}",
154            msg.chars().take(visible_chars).collect::<String>(),
155            TRUNCATED_SUFFIX
156        )
157    } else {
158        msg
159    };
160
161    let sanitized_msg = sanitize(&truncated_msg);
162
163    if let Ok(state) = LOGGER_STATE.read() {
164        if let Some(provider) = &state.provider {
165            match level {
166                LogLevel::Debug => provider.debug(tag, sanitized_msg),
167                LogLevel::Info => provider.info(tag, sanitized_msg),
168                LogLevel::Warn => provider.warn(tag, sanitized_msg),
169                LogLevel::Error => provider.error(tag, sanitized_msg),
170                _ => {}
171            }
172            return;
173        }
174    }
175
176    if let Some(level) = level.to_third_party_level() {
177        let mut target = String::from("Statsig::");
178        target += tag;
179
180        match level {
181            Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
182            Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
183            Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
184            Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
185            _ => {}
186        };
187    }
188}
189
190fn sanitize(input: &str) -> String {
191    input
192        .split("secret-")
193        .enumerate()
194        .map(|(i, part)| {
195            if i == 0 {
196                part.to_string()
197            } else {
198                let (key, rest) =
199                    part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
200                let sanitized_key = if key.len() > 5 {
201                    format!("{}*****{}", &key[..5], rest)
202                } else {
203                    format!("{key}*****{rest}")
204                };
205                format!("secret-{sanitized_key}")
206            }
207        })
208        .collect()
209}
210
211pub fn has_valid_log_level(level: &LogLevel) -> bool {
212    let state = match LOGGER_STATE.read() {
213        Ok(state) => state,
214        Err(e) => {
215            eprintln!("[Statsig] Failed to acquire read lock for logger: {e}");
216            return false;
217        }
218    };
219    let current_level = &state.level;
220    level.to_number() <= current_level.to_number()
221}
222
223#[macro_export]
224macro_rules! log_d {
225  ($tag:expr, $($arg:tt)*) => {
226        {
227            let level = $crate::output_logger::LogLevel::Debug;
228            if $crate::output_logger::has_valid_log_level(&level) {
229                $crate::output_logger::log_message($tag, level, format!($($arg)*));
230            }
231        }
232    }
233}
234
235#[macro_export]
236macro_rules! log_i {
237  ($tag:expr, $($arg:tt)*) => {
238        {
239            let level = $crate::output_logger::LogLevel::Info;
240            if $crate::output_logger::has_valid_log_level(&level) {
241                $crate::output_logger::log_message($tag, level, format!($($arg)*));
242            }
243        }
244    }
245}
246
247#[macro_export]
248macro_rules! log_w {
249  ($tag:expr, $($arg:tt)*) => {
250        {
251            let level = $crate::output_logger::LogLevel::Warn;
252            if $crate::output_logger::has_valid_log_level(&level) {
253                $crate::output_logger::log_message($tag, level, format!($($arg)*));
254            }
255        }
256    }
257}
258
259#[macro_export]
260macro_rules! log_e {
261  ($tag:expr, $($arg:tt)*) => {
262        {
263            let level = $crate::output_logger::LogLevel::Error;
264            if $crate::output_logger::has_valid_log_level(&level) {
265                $crate::output_logger::log_message($tag, level, format!($($arg)*));
266            }
267        }
268    }
269}
270
271#[macro_export]
272macro_rules! log_error_to_statsig_and_console {
273    ($ops_stats:expr, $tag:expr, $err:expr) => {
274        let event = ErrorBoundaryEvent {
275            bypass_dedupe: false,
276            exception: $err.name().to_string(),
277            info: serde_json::to_string(&$err).unwrap_or_default(),
278            tag: $tag.to_string(),
279            extra: None,
280            dedupe_key: None,
281        };
282        $ops_stats.log_error(event);
283
284        $crate::output_logger::log_message(
285            &$tag,
286            $crate::output_logger::LogLevel::Error,
287            $err.to_string(),
288        );
289    };
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use std::collections::HashMap;
296
297    #[test]
298    fn test_sanitize_url_for_logging() {
299        let test_cases = HashMap::from(
300            [
301                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
302                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
303                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json?sinceTime=1", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json?sinceTime=1"),
304            ]
305        );
306        for (before, expected) in test_cases {
307            let sanitized = sanitize(before);
308            assert!(sanitized == expected);
309        }
310    }
311
312    #[test]
313    fn test_multiple_secrets() {
314        let input = "Multiple secrets: secret-key1 and secret-key2";
315        let sanitized = sanitize(input);
316        assert_eq!(
317            sanitized,
318            "Multiple secrets: secret-key1***** and secret-key2*****"
319        );
320    }
321
322    #[test]
323    fn test_short_secret() {
324        let input = "Short secret: secret-a";
325        let sanitized = sanitize(input);
326        assert_eq!(sanitized, "Short secret: secret-a*****");
327    }
328}