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 return;
99 }
100
101 let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
102 Some(state) => state,
103 None => {
104 eprintln!(
105 "[Statsig] Failed to acquire write lock for logger: Failed to lock LOGGER_STATE"
106 );
107 return;
108 }
109 };
110 let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
111 state.level = level.clone();
112
113 if let Some(provider_impl) = provider {
114 provider_impl.initialize();
115 state.provider = Some(provider_impl);
116 } else {
117 let final_level = match level {
118 LogLevel::None => {
119 return;
120 }
121 _ => match level.to_third_party_level() {
122 Some(level) => level,
123 None => return,
124 },
125 };
126
127 match simple_logger::init_with_level(final_level) {
128 Ok(()) => {}
129 Err(_) => {
130 log::set_max_level(final_level.to_level_filter());
131 }
132 }
133 }
134}
135
136pub fn shutdown_output_logger() {
137 let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
138 Some(state) => state,
139 None => {
140 eprintln!(
141 "[Statsig] Failed to acquire write lock for logger: Failed to lock LOGGER_STATE"
142 );
143 return;
144 }
145 };
146
147 if let Some(provider) = &mut state.provider {
148 provider.shutdown();
149 }
150
151 INITIALIZED.store(false, Ordering::SeqCst);
152}
153
154pub fn log_message(tag: &str, level: LogLevel, msg: String) {
155 let truncated_msg = if msg.chars().count() > MAX_CHARS {
156 let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
157 format!(
158 "{}{}",
159 msg.chars().take(visible_chars).collect::<String>(),
160 TRUNCATED_SUFFIX
161 )
162 } else {
163 msg
164 };
165
166 let sanitized_msg = sanitize(&truncated_msg);
167
168 if let Some(state) = LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
169 if let Some(provider) = &state.provider {
170 match level {
171 LogLevel::Debug => provider.debug(tag, sanitized_msg),
172 LogLevel::Info => provider.info(tag, sanitized_msg),
173 LogLevel::Warn => provider.warn(tag, sanitized_msg),
174 LogLevel::Error => provider.error(tag, sanitized_msg),
175 _ => {}
176 }
177 return;
178 }
179 } else {
180 eprintln!("[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE");
181 }
182
183 if let Some(level) = level.to_third_party_level() {
184 let mut target = String::from("Statsig::");
185 target += tag;
186
187 match level {
188 Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
189 Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
190 Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
191 Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
192 _ => {}
193 };
194 }
195}
196
197fn sanitize(input: &str) -> String {
198 input
199 .split("secret-")
200 .enumerate()
201 .map(|(i, part)| {
202 if i == 0 {
203 part.to_string()
204 } else {
205 let (key, rest) =
206 part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
207 let sanitized_key = if key.len() > 5 {
208 format!("{}*****{}", &key[..5], rest)
209 } else {
210 format!("{key}*****{rest}")
211 };
212 format!("secret-{sanitized_key}")
213 }
214 })
215 .collect()
216}
217
218pub fn has_valid_log_level(level: &LogLevel) -> bool {
219 let state = match LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
220 Some(state) => state,
221 None => {
222 eprintln!(
223 "[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE"
224 );
225 return false;
226 }
227 };
228 let current_level = &state.level;
229 level.to_number() <= current_level.to_number()
230}
231
232#[macro_export]
233macro_rules! log_d {
234 ($tag:expr, $($arg:tt)*) => {
235 {
236 let level = $crate::output_logger::LogLevel::Debug;
237 if $crate::output_logger::has_valid_log_level(&level) {
238 $crate::output_logger::log_message($tag, level, format!($($arg)*));
239 }
240 }
241 }
242}
243
244#[macro_export]
245macro_rules! log_i {
246 ($tag:expr, $($arg:tt)*) => {
247 {
248 let level = $crate::output_logger::LogLevel::Info;
249 if $crate::output_logger::has_valid_log_level(&level) {
250 $crate::output_logger::log_message($tag, level, format!($($arg)*));
251 }
252 }
253 }
254}
255
256#[macro_export]
257macro_rules! log_w {
258 ($tag:expr, $($arg:tt)*) => {
259 {
260 let level = $crate::output_logger::LogLevel::Warn;
261 if $crate::output_logger::has_valid_log_level(&level) {
262 $crate::output_logger::log_message($tag, level, format!($($arg)*));
263 }
264 }
265 }
266}
267
268#[macro_export]
269macro_rules! log_e {
270 ($tag:expr, $($arg:tt)*) => {
271 {
272 let level = $crate::output_logger::LogLevel::Error;
273 if $crate::output_logger::has_valid_log_level(&level) {
274 $crate::output_logger::log_message($tag, level, format!($($arg)*));
275 }
276 }
277 }
278}
279
280#[macro_export]
281macro_rules! log_error_to_statsig_and_console {
282 ($ops_stats:expr, $tag:expr, $err:expr) => {
283 let event = ErrorBoundaryEvent {
284 bypass_dedupe: false,
285 exception: $err.name().to_string(),
286 info: serde_json::to_string(&$err).unwrap_or_default(),
287 tag: $tag.to_string(),
288 extra: None,
289 dedupe_key: None,
290 };
291 $ops_stats.log_error(event);
292
293 $crate::output_logger::log_message(
294 &$tag,
295 $crate::output_logger::LogLevel::Error,
296 $err.to_string(),
297 );
298 };
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::collections::HashMap;
305
306 #[test]
307 fn test_sanitize_url_for_logging() {
308 let test_cases = HashMap::from(
309 [
310 ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
311 ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
312 ("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"),
313 ]
314 );
315 for (before, expected) in test_cases {
316 let sanitized = sanitize(before);
317 assert!(sanitized == expected);
318 }
319 }
320
321 #[test]
322 fn test_multiple_secrets() {
323 let input = "Multiple secrets: secret-key1 and secret-key2";
324 let sanitized = sanitize(input);
325 assert_eq!(
326 sanitized,
327 "Multiple secrets: secret-key1***** and secret-key2*****"
328 );
329 }
330
331 #[test]
332 fn test_short_secret() {
333 let input = "Short secret: secret-a";
334 let sanitized = sanitize(input);
335 assert_eq!(sanitized, "Short secret: secret-a*****");
336 }
337}