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        return;
99    }
100
101    let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
102        Some(state) => state,
103        None => {
104            eprintln!(
105                "[Statsig] Failed to acquire write lock for logger: Failed to lock LOGGER_STATE"
106            );
107            return;
108        }
109    };
110    let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
111    state.level = level.clone();
112
113    if let Some(provider_impl) = provider {
114        provider_impl.initialize();
115        state.provider = Some(provider_impl);
116    } else {
117        let final_level = match level {
118            LogLevel::None => {
119                return;
120            }
121            _ => match level.to_third_party_level() {
122                Some(level) => level,
123                None => return,
124            },
125        };
126
127        match simple_logger::init_with_level(final_level) {
128            Ok(()) => {}
129            Err(_) => {
130                log::set_max_level(final_level.to_level_filter());
131            }
132        }
133    }
134}
135
136pub fn shutdown_output_logger() {
137    let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
138        Some(state) => state,
139        None => {
140            eprintln!(
141                "[Statsig] Failed to acquire write lock for logger: Failed to lock LOGGER_STATE"
142            );
143            return;
144        }
145    };
146
147    if let Some(provider) = &mut state.provider {
148        provider.shutdown();
149    }
150
151    INITIALIZED.store(false, Ordering::SeqCst);
152}
153
154pub fn log_message(tag: &str, level: LogLevel, msg: String) {
155    let truncated_msg = if msg.chars().count() > MAX_CHARS {
156        let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
157        format!(
158            "{}{}",
159            msg.chars().take(visible_chars).collect::<String>(),
160            TRUNCATED_SUFFIX
161        )
162    } else {
163        msg
164    };
165
166    let sanitized_msg = sanitize(&truncated_msg);
167
168    if let Some(state) = LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
169        if let Some(provider) = &state.provider {
170            match level {
171                LogLevel::Debug => provider.debug(tag, sanitized_msg),
172                LogLevel::Info => provider.info(tag, sanitized_msg),
173                LogLevel::Warn => provider.warn(tag, sanitized_msg),
174                LogLevel::Error => provider.error(tag, sanitized_msg),
175                _ => {}
176            }
177            return;
178        }
179    } else {
180        eprintln!("[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE");
181    }
182
183    if let Some(level) = level.to_third_party_level() {
184        let mut target = String::from("Statsig::");
185        target += tag;
186
187        match level {
188            Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
189            Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
190            Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
191            Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
192            _ => {}
193        };
194    }
195}
196
197fn sanitize(input: &str) -> String {
198    input
199        .split("secret-")
200        .enumerate()
201        .map(|(i, part)| {
202            if i == 0 {
203                part.to_string()
204            } else {
205                let (key, rest) =
206                    part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
207                let sanitized_key = if key.len() > 5 {
208                    format!("{}*****{}", &key[..5], rest)
209                } else {
210                    format!("{key}*****{rest}")
211                };
212                format!("secret-{sanitized_key}")
213            }
214        })
215        .collect()
216}
217
218pub fn has_valid_log_level(level: &LogLevel) -> bool {
219    let state = match LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
220        Some(state) => state,
221        None => {
222            eprintln!(
223                "[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE"
224            );
225            return false;
226        }
227    };
228    let current_level = &state.level;
229    level.to_number() <= current_level.to_number()
230}
231
232#[macro_export]
233macro_rules! log_d {
234  ($tag:expr, $($arg:tt)*) => {
235        {
236            let level = $crate::output_logger::LogLevel::Debug;
237            if $crate::output_logger::has_valid_log_level(&level) {
238                $crate::output_logger::log_message($tag, level, format!($($arg)*));
239            }
240        }
241    }
242}
243
244#[macro_export]
245macro_rules! log_i {
246  ($tag:expr, $($arg:tt)*) => {
247        {
248            let level = $crate::output_logger::LogLevel::Info;
249            if $crate::output_logger::has_valid_log_level(&level) {
250                $crate::output_logger::log_message($tag, level, format!($($arg)*));
251            }
252        }
253    }
254}
255
256#[macro_export]
257macro_rules! log_w {
258  ($tag:expr, $($arg:tt)*) => {
259        {
260            let level = $crate::output_logger::LogLevel::Warn;
261            if $crate::output_logger::has_valid_log_level(&level) {
262                $crate::output_logger::log_message($tag, level, format!($($arg)*));
263            }
264        }
265    }
266}
267
268#[macro_export]
269macro_rules! log_e {
270  ($tag:expr, $($arg:tt)*) => {
271        {
272            let level = $crate::output_logger::LogLevel::Error;
273            if $crate::output_logger::has_valid_log_level(&level) {
274                $crate::output_logger::log_message($tag, level, format!($($arg)*));
275            }
276        }
277    }
278}
279
280#[macro_export]
281macro_rules! log_error_to_statsig_and_console {
282    ($ops_stats:expr, $tag:expr, $err:expr) => {
283        let event = ErrorBoundaryEvent {
284            bypass_dedupe: false,
285            exception: $err.name().to_string(),
286            info: serde_json::to_string(&$err).unwrap_or_default(),
287            tag: $tag.to_string(),
288            extra: None,
289            dedupe_key: None,
290        };
291        $ops_stats.log_error(event);
292
293        $crate::output_logger::log_message(
294            &$tag,
295            $crate::output_logger::LogLevel::Error,
296            $err.to_string(),
297        );
298    };
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::collections::HashMap;
305
306    #[test]
307    fn test_sanitize_url_for_logging() {
308        let test_cases = HashMap::from(
309            [
310                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
311                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
312                ("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"),
313            ]
314        );
315        for (before, expected) in test_cases {
316            let sanitized = sanitize(before);
317            assert!(sanitized == expected);
318        }
319    }
320
321    #[test]
322    fn test_multiple_secrets() {
323        let input = "Multiple secrets: secret-key1 and secret-key2";
324        let sanitized = sanitize(input);
325        assert_eq!(
326            sanitized,
327            "Multiple secrets: secret-key1***** and secret-key2*****"
328        );
329    }
330
331    #[test]
332    fn test_short_secret() {
333        let input = "Short secret: secret-a";
334        let sanitized = sanitize(input);
335        assert_eq!(sanitized, "Short secret: secret-a*****");
336    }
337}