statsig_rust/
output_logger.rs

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