statsig_rust/
output_logger.rs

1use std::sync::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 LOG_LEVEL: RwLock<LogLevel> = RwLock::new(DEFAULT_LOG_LEVEL);
12}
13
14#[derive(Clone)]
15pub enum LogLevel {
16    None,
17    Debug,
18    Info,
19    Warn,
20    Error,
21}
22
23impl From<&str> for LogLevel {
24    fn from(level: &str) -> Self {
25        match level.to_lowercase().as_str() {
26            "debug" => LogLevel::Debug,
27            "info" => LogLevel::Info,
28            "warn" => LogLevel::Warn,
29            "error" => LogLevel::Error,
30            "none" => LogLevel::None,
31            _ => DEFAULT_LOG_LEVEL,
32        }
33    }
34}
35
36impl From<u32> for LogLevel {
37    fn from(level: u32) -> Self {
38        match level {
39            0 => LogLevel::None,
40            1 => LogLevel::Error,
41            2 => LogLevel::Warn,
42            3 => LogLevel::Info,
43            4 => LogLevel::Debug,
44            _ => DEFAULT_LOG_LEVEL,
45        }
46    }
47}
48
49impl LogLevel {
50    fn to_third_party_level(&self) -> Option<Level> {
51        match self {
52            LogLevel::Debug => Some(Level::Debug),
53            LogLevel::Info => Some(Level::Info),
54            LogLevel::Warn => Some(Level::Warn),
55            LogLevel::Error => Some(Level::Error),
56            LogLevel::None => None,
57        }
58    }
59
60    fn to_number(&self) -> u32 {
61        match self {
62            LogLevel::Debug => 4,
63            LogLevel::Info => 3,
64            LogLevel::Warn => 2,
65            LogLevel::Error => 1,
66            LogLevel::None => 0,
67        }
68    }
69}
70
71pub fn initialize_simple_output_logger(level: &Option<LogLevel>) {
72    let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
73
74    if let Ok(mut lock) = LOG_LEVEL.write() {
75        *lock = level.clone();
76    }
77
78    let final_level = match level {
79        LogLevel::None => {
80            return;
81        }
82        _ => match level.to_third_party_level() {
83            Some(level) => level,
84            None => return,
85        },
86    };
87
88    match simple_logger::init_with_level(final_level) {
89        Ok(()) => {}
90        Err(_) => {
91            log::set_max_level(final_level.to_level_filter());
92        }
93    }
94}
95
96pub fn log_message(tag: &str, level: LogLevel, msg: String) {
97    let truncated_msg = if msg.chars().count() > MAX_CHARS {
98        let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
99        format!(
100            "{}{}",
101            msg.chars().take(visible_chars).collect::<String>(),
102            TRUNCATED_SUFFIX
103        )
104    } else {
105        msg
106    };
107
108    let sanitized_msg = sanitize(&truncated_msg);
109
110    if let Some(level) = level.to_third_party_level() {
111        let mut target = String::from("Statsig::");
112        target += tag;
113
114        match level {
115            Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
116            Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
117            Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
118            Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
119            _ => {}
120        };
121    }
122}
123
124fn sanitize(input: &str) -> String {
125    input
126        .split("secret-")
127        .enumerate()
128        .map(|(i, part)| {
129            if i == 0 {
130                part.to_string()
131            } else {
132                let (key, rest) =
133                    part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
134                let sanitized_key = if key.len() > 5 {
135                    format!("{}*****{}", &key[..5], rest)
136                } else {
137                    format!("{}*****{}", key, rest)
138                };
139                format!("secret-{}", sanitized_key)
140            }
141        })
142        .collect()
143}
144
145pub fn has_valid_log_level(level: &LogLevel) -> bool {
146    let current_level = match LOG_LEVEL.read() {
147        Ok(lock) => lock,
148        Err(_) => return false,
149    };
150
151    level.to_number() <= current_level.to_number()
152}
153
154#[macro_export]
155macro_rules! log_d {
156  ($tag:expr, $($arg:tt)*) => {
157        {
158            let level = $crate::output_logger::LogLevel::Debug;
159            if $crate::output_logger::has_valid_log_level(&level) {
160                $crate::output_logger::log_message($tag, level, format!($($arg)*));
161            }
162        }
163    }
164}
165
166#[macro_export]
167macro_rules! log_i {
168  ($tag:expr, $($arg:tt)*) => {
169        {
170            let level = $crate::output_logger::LogLevel::Info;
171            if $crate::output_logger::has_valid_log_level(&level) {
172                $crate::output_logger::log_message($tag, level, format!($($arg)*));
173            }
174        }
175    }
176}
177
178#[macro_export]
179macro_rules! log_w {
180  ($tag:expr, $($arg:tt)*) => {
181        {
182            let level = $crate::output_logger::LogLevel::Warn;
183            if $crate::output_logger::has_valid_log_level(&level) {
184                $crate::output_logger::log_message($tag, level, format!($($arg)*));
185            }
186        }
187    }
188}
189
190#[macro_export]
191macro_rules! log_e {
192  ($tag:expr, $($arg:tt)*) => {
193        {
194            let level = $crate::output_logger::LogLevel::Error;
195            if $crate::output_logger::has_valid_log_level(&level) {
196                $crate::output_logger::log_message($tag, level, format!($($arg)*));
197            }
198        }
199    }
200}
201
202#[macro_export]
203macro_rules! log_error_to_statsig_and_console {
204    ($ops_stats:expr, $tag:expr, $err:expr) => {
205        let event = ErrorBoundaryEvent {
206            bypass_dedupe: false,
207            info: $err.clone(),
208            tag: $tag.to_string(),
209            extra: None,
210            dedupe_key: None,
211        };
212        $ops_stats.log_error(event);
213
214        $crate::output_logger::log_message(
215            &$tag,
216            $crate::output_logger::LogLevel::Error,
217            $err.to_string(),
218        );
219    };
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use std::collections::HashMap;
226
227    #[test]
228    fn test_sanitize_url_for_logging() {
229        let test_cases = HashMap::from(
230            [
231                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
232                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
233                ("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"),
234            ]
235        );
236        for (before, expected) in test_cases {
237            let sanitized = sanitize(before);
238            assert!(sanitized == expected);
239        }
240    }
241
242    #[test]
243    fn test_multiple_secrets() {
244        let input = "Multiple secrets: secret-key1 and secret-key2";
245        let sanitized = sanitize(input);
246        assert_eq!(
247            sanitized,
248            "Multiple secrets: secret-key1***** and secret-key2*****"
249        );
250    }
251
252    #[test]
253    fn test_short_secret() {
254        let input = "Short secret: secret-a";
255        let sanitized = sanitize(input);
256        assert_eq!(sanitized, "Short secret: secret-a*****");
257    }
258}