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}