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