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