Skip to main content

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
7use crate::logging_utils::sanitize_secret_key;
8const MAX_CHARS: usize = 400;
9const TRUNCATED_SUFFIX: &str = "...[TRUNCATED]";
10
11const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warn;
12
13lazy_static::lazy_static! {
14    static ref LOGGER_STATE: RwLock<LoggerState> = RwLock::new(LoggerState {
15        level: DEFAULT_LOG_LEVEL,
16        provider: None,
17    });
18}
19
20struct LoggerState {
21    level: LogLevel,
22    provider: Option<Arc<dyn OutputLogProvider>>,
23}
24
25static INITIALIZED: AtomicBool = AtomicBool::new(false);
26
27#[derive(Clone, Debug)]
28pub enum LogLevel {
29    None,
30    Debug,
31    Info,
32    Warn,
33    Error,
34}
35
36impl From<&str> for LogLevel {
37    fn from(level: &str) -> Self {
38        match level.to_lowercase().as_str() {
39            "debug" => LogLevel::Debug,
40            "info" => LogLevel::Info,
41            "warn" => LogLevel::Warn,
42            "error" => LogLevel::Error,
43            "none" => LogLevel::None,
44            _ => DEFAULT_LOG_LEVEL,
45        }
46    }
47}
48
49impl From<u32> for LogLevel {
50    fn from(level: u32) -> Self {
51        match level {
52            0 => LogLevel::None,
53            1 => LogLevel::Error,
54            2 => LogLevel::Warn,
55            3 => LogLevel::Info,
56            4 => LogLevel::Debug,
57            _ => DEFAULT_LOG_LEVEL,
58        }
59    }
60}
61
62impl LogLevel {
63    fn to_third_party_level(&self) -> Option<Level> {
64        match self {
65            LogLevel::Debug => Some(Level::Debug),
66            LogLevel::Info => Some(Level::Info),
67            LogLevel::Warn => Some(Level::Warn),
68            LogLevel::Error => Some(Level::Error),
69            LogLevel::None => None,
70        }
71    }
72
73    fn to_number(&self) -> u32 {
74        match self {
75            LogLevel::Debug => 4,
76            LogLevel::Info => 3,
77            LogLevel::Warn => 2,
78            LogLevel::Error => 1,
79            LogLevel::None => 0,
80        }
81    }
82}
83
84pub trait OutputLogProvider: Send + Sync {
85    fn initialize(&self);
86    fn debug(&self, tag: &str, msg: String);
87    fn info(&self, tag: &str, msg: String);
88    fn warn(&self, tag: &str, msg: String);
89    fn error(&self, tag: &str, msg: String);
90    fn shutdown(&self);
91}
92
93pub fn initialize_output_logger(
94    level: &Option<LogLevel>,
95    provider: Option<Arc<dyn OutputLogProvider>>,
96) {
97    let was_initialized = INITIALIZED.swap(true, Ordering::SeqCst);
98    if was_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_secret_key(&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
198pub fn has_valid_log_level(level: &LogLevel) -> bool {
199    let state = match LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
200        Some(state) => state,
201        None => {
202            eprintln!(
203                "[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE"
204            );
205            return false;
206        }
207    };
208    let current_level = &state.level;
209    level.to_number() <= current_level.to_number()
210}
211
212#[macro_export]
213macro_rules! log_d {
214  ($tag:expr, $($arg:tt)*) => {
215        {
216            let level = $crate::output_logger::LogLevel::Debug;
217            if $crate::output_logger::has_valid_log_level(&level) {
218                $crate::output_logger::log_message($tag, level, format!($($arg)*));
219            }
220        }
221    }
222}
223
224#[macro_export]
225macro_rules! log_i {
226  ($tag:expr, $($arg:tt)*) => {
227        {
228            let level = $crate::output_logger::LogLevel::Info;
229            if $crate::output_logger::has_valid_log_level(&level) {
230                $crate::output_logger::log_message($tag, level, format!($($arg)*));
231            }
232        }
233    }
234}
235
236#[macro_export]
237macro_rules! log_w {
238  ($tag:expr, $($arg:tt)*) => {
239        {
240            let level = $crate::output_logger::LogLevel::Warn;
241            if $crate::output_logger::has_valid_log_level(&level) {
242                $crate::output_logger::log_message($tag, level, format!($($arg)*));
243            }
244        }
245    }
246}
247
248#[macro_export]
249macro_rules! log_e {
250  ($tag:expr, $($arg:tt)*) => {
251        {
252            let level = $crate::output_logger::LogLevel::Error;
253            if $crate::output_logger::has_valid_log_level(&level) {
254                $crate::output_logger::log_message($tag, level, format!($($arg)*));
255            }
256        }
257    }
258}
259
260#[macro_export]
261macro_rules! log_error_to_statsig_and_console {
262    ($ops_stats:expr, $tag:expr, $err:expr) => {
263        let event = ErrorBoundaryEvent {
264            bypass_dedupe: false,
265            exception: $err.name().to_string(),
266            info: serde_json::to_string(&$err).unwrap_or_default(),
267            tag: $tag.to_string(),
268            extra: None,
269            dedupe_key: None,
270        };
271        $ops_stats.log_error(event);
272
273        $crate::output_logger::log_message(
274            &$tag,
275            $crate::output_logger::LogLevel::Error,
276            $err.to_string(),
277        );
278    };
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use std::collections::HashMap;
285
286    #[test]
287    fn test_sanitize_url_for_logging() {
288        let test_cases = HashMap::from(
289            [
290                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
291                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
292                ("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"),
293            ]
294        );
295        for (before, expected) in test_cases {
296            let sanitized = sanitize_secret_key(before);
297            assert!(sanitized == expected);
298        }
299    }
300
301    #[test]
302    fn test_multiple_secrets() {
303        let input = "Multiple secrets: secret-key1 and secret-key2";
304        let sanitized = sanitize_secret_key(input);
305        assert_eq!(
306            sanitized,
307            "Multiple secrets: secret-key1***** and secret-key2*****"
308        );
309    }
310
311    #[test]
312    fn test_short_secret() {
313        let input = "Short secret: secret-a";
314        let sanitized = sanitize_secret_key(input);
315        assert_eq!(sanitized, "Short secret: secret-a*****");
316    }
317}