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        match level {
112            Level::Debug => debug!("[Statsig.{}] {}", tag, sanitized_msg),
113            Level::Info => info!("[Statsig.{}] {}", tag, sanitized_msg),
114            Level::Warn => warn!("[Statsig.{}] {}", tag, sanitized_msg),
115            Level::Error => error!("[Statsig.{}] {}", tag, sanitized_msg),
116            _ => {}
117        };
118    }
119}
120
121fn sanitize(input: &str) -> String {
122    input
123        .split("secret-")
124        .enumerate()
125        .map(|(i, part)| {
126            if i == 0 {
127                part.to_string()
128            } else {
129                let (key, rest) =
130                    part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
131                let sanitized_key = if key.len() > 5 {
132                    format!("{}*****{}", &key[..5], rest)
133                } else {
134                    format!("{}*****{}", key, rest)
135                };
136                format!("secret-{}", sanitized_key)
137            }
138        })
139        .collect()
140}
141
142pub fn has_valid_log_level(level: &LogLevel) -> bool {
143    let current_level = match LOG_LEVEL.read() {
144        Ok(lock) => lock,
145        Err(_) => return false,
146    };
147
148    level.to_number() <= current_level.to_number()
149}
150
151#[macro_export]
152macro_rules! log_d {
153  ($tag:expr, $($arg:tt)*) => {
154        {
155            let level = $crate::output_logger::LogLevel::Debug;
156            if $crate::output_logger::has_valid_log_level(&level) {
157                $crate::output_logger::log_message($tag, level, format!($($arg)*));
158            }
159        }
160    }
161}
162
163#[macro_export]
164macro_rules! log_i {
165  ($tag:expr, $($arg:tt)*) => {
166        {
167            let level = $crate::output_logger::LogLevel::Info;
168            if $crate::output_logger::has_valid_log_level(&level) {
169                $crate::output_logger::log_message($tag, level, format!($($arg)*));
170            }
171        }
172    }
173}
174
175#[macro_export]
176macro_rules! log_w {
177  ($tag:expr, $($arg:tt)*) => {
178        {
179            let level = $crate::output_logger::LogLevel::Warn;
180            if $crate::output_logger::has_valid_log_level(&level) {
181                $crate::output_logger::log_message($tag, level, format!($($arg)*));
182            }
183        }
184    }
185}
186
187#[macro_export]
188macro_rules! log_e {
189  ($tag:expr, $($arg:tt)*) => {
190        {
191            let level = $crate::output_logger::LogLevel::Error;
192            if $crate::output_logger::has_valid_log_level(&level) {
193                $crate::output_logger::log_message($tag, level, format!($($arg)*));
194            }
195        }
196    }
197}
198
199#[macro_export]
200macro_rules! log_error_to_statsig_and_console {
201    ($ops_stats:expr, $tag:expr, $err:expr) => {
202        let event = ErrorBoundaryEvent {
203            bypass_dedupe: false,
204            info: $err.clone(),
205            tag: $tag.to_string(),
206            extra: None,
207            dedupe_key: None,
208        };
209        $ops_stats.log_error(event);
210
211        $crate::output_logger::log_message(
212            &$tag,
213            $crate::output_logger::LogLevel::Error,
214            $err.to_string(),
215        );
216    };
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use std::collections::HashMap;
223
224    #[test]
225    fn test_sanitize_url_for_logging() {
226        let test_cases = HashMap::from(
227            [
228                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
229                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
230                ("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"),
231            ]
232        );
233        for (before, expected) in test_cases {
234            let sanitized = sanitize(before);
235            assert!(sanitized == expected);
236        }
237    }
238
239    #[test]
240    fn test_multiple_secrets() {
241        let input = "Multiple secrets: secret-key1 and secret-key2";
242        let sanitized = sanitize(input);
243        assert_eq!(
244            sanitized,
245            "Multiple secrets: secret-key1***** and secret-key2*****"
246        );
247    }
248
249    #[test]
250    fn test_short_secret() {
251        let input = "Short secret: secret-a";
252        let sanitized = sanitize(input);
253        assert_eq!(sanitized, "Short secret: secret-a*****");
254    }
255}