statsig_rust/
output_logger.rs

1use std::sync::{Arc, Once, RwLock};
2
3use log::{debug, error, info, warn, Level};
4
5const MAX_CHARS: usize = 400;
6const TRUNCATED_SUFFIX: &str = "...[TRUNCATED]";
7
8const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warn;
9
10lazy_static::lazy_static! {
11    static ref LOGGER_STATE: RwLock<LoggerState> = RwLock::new(LoggerState {
12        level: DEFAULT_LOG_LEVEL,
13        provider: None,
14        initialized: false,
15    });
16}
17
18struct LoggerState {
19    level: LogLevel,
20    provider: Option<Arc<dyn LogProvider>>,
21    initialized: bool,
22}
23
24static INIT: Once = Once::new();
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 LogProvider: 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(level: &Option<LogLevel>, provider: Option<Arc<dyn LogProvider>>) {
93    INIT.call_once(|| {
94        let mut state = LOGGER_STATE.write().unwrap();
95        let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
96        state.level = level.clone();
97
98        if let Some(provider_impl) = provider {
99            provider_impl.initialize();
100            state.provider = Some(provider_impl);
101        } else {
102            let final_level = match level {
103                LogLevel::None => {
104                    return;
105                }
106                _ => match level.to_third_party_level() {
107                    Some(level) => level,
108                    None => return,
109                },
110            };
111
112            match simple_logger::init_with_level(final_level) {
113                Ok(()) => {}
114                Err(_) => {
115                    log::set_max_level(final_level.to_level_filter());
116                }
117            }
118        }
119
120        state.initialized = true;
121    });
122}
123
124pub fn shutdown_output_logger() {
125    let mut state = LOGGER_STATE.write().unwrap();
126    if let Some(provider) = &mut state.provider {
127        provider.shutdown();
128    }
129}
130
131pub fn log_message(tag: &str, level: LogLevel, msg: String) {
132    let truncated_msg = if msg.chars().count() > MAX_CHARS {
133        let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
134        format!(
135            "{}{}",
136            msg.chars().take(visible_chars).collect::<String>(),
137            TRUNCATED_SUFFIX
138        )
139    } else {
140        msg
141    };
142
143    let sanitized_msg = sanitize(&truncated_msg);
144
145    if let Ok(state) = LOGGER_STATE.read() {
146        if let Some(provider) = &state.provider {
147            match level {
148                LogLevel::Debug => provider.debug(tag, sanitized_msg),
149                LogLevel::Info => provider.info(tag, sanitized_msg),
150                LogLevel::Warn => provider.warn(tag, sanitized_msg),
151                LogLevel::Error => provider.error(tag, sanitized_msg),
152                LogLevel::None => {}
153            }
154            return;
155        }
156    }
157
158    if let Some(level) = level.to_third_party_level() {
159        let mut target = String::from("Statsig::");
160        target += tag;
161
162        match level {
163            Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
164            Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
165            Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
166            Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
167            _ => {}
168        };
169    }
170}
171
172fn sanitize(input: &str) -> String {
173    input
174        .split("secret-")
175        .enumerate()
176        .map(|(i, part)| {
177            if i == 0 {
178                part.to_string()
179            } else {
180                let (key, rest) =
181                    part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
182                let sanitized_key = if key.len() > 5 {
183                    format!("{}*****{}", &key[..5], rest)
184                } else {
185                    format!("{}*****{}", key, rest)
186                };
187                format!("secret-{}", sanitized_key)
188            }
189        })
190        .collect()
191}
192
193pub fn has_valid_log_level(level: &LogLevel) -> bool {
194    let state = LOGGER_STATE.read().unwrap();
195    let current_level = &state.level;
196    level.to_number() <= current_level.to_number()
197}
198
199#[macro_export]
200macro_rules! log_d {
201  ($tag:expr, $($arg:tt)*) => {
202        {
203            let level = $crate::output_logger::LogLevel::Debug;
204            if $crate::output_logger::has_valid_log_level(&level) {
205                $crate::output_logger::log_message($tag, level, format!($($arg)*));
206            }
207        }
208    }
209}
210
211#[macro_export]
212macro_rules! log_i {
213  ($tag:expr, $($arg:tt)*) => {
214        {
215            let level = $crate::output_logger::LogLevel::Info;
216            if $crate::output_logger::has_valid_log_level(&level) {
217                $crate::output_logger::log_message($tag, level, format!($($arg)*));
218            }
219        }
220    }
221}
222
223#[macro_export]
224macro_rules! log_w {
225  ($tag:expr, $($arg:tt)*) => {
226        {
227            let level = $crate::output_logger::LogLevel::Warn;
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_e {
237  ($tag:expr, $($arg:tt)*) => {
238        {
239            let level = $crate::output_logger::LogLevel::Error;
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_error_to_statsig_and_console {
249    ($ops_stats:expr, $tag:expr, $err:expr) => {
250        let event = ErrorBoundaryEvent {
251            bypass_dedupe: false,
252            info: $err.clone(),
253            tag: $tag.to_string(),
254            extra: None,
255            dedupe_key: None,
256        };
257        $ops_stats.log_error(event);
258
259        $crate::output_logger::log_message(
260            &$tag,
261            $crate::output_logger::LogLevel::Error,
262            $err.to_string(),
263        );
264    };
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use std::collections::HashMap;
271
272    #[test]
273    fn test_sanitize_url_for_logging() {
274        let test_cases = HashMap::from(
275            [
276                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
277                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
278                ("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"),
279            ]
280        );
281        for (before, expected) in test_cases {
282            let sanitized = sanitize(before);
283            assert!(sanitized == expected);
284        }
285    }
286
287    #[test]
288    fn test_multiple_secrets() {
289        let input = "Multiple secrets: secret-key1 and secret-key2";
290        let sanitized = sanitize(input);
291        assert_eq!(
292            sanitized,
293            "Multiple secrets: secret-key1***** and secret-key2*****"
294        );
295    }
296
297    #[test]
298    fn test_short_secret() {
299        let input = "Short secret: secret-a";
300        let sanitized = sanitize(input);
301        assert_eq!(sanitized, "Short secret: secret-a*****");
302    }
303}