1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, RwLock};
3
4use log::{debug, error, info, warn, Level};
5
6const MAX_CHARS: usize = 400;
7const TRUNCATED_SUFFIX: &str = "...[TRUNCATED]";
8
9const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warn;
10
11lazy_static::lazy_static! {
12 static ref LOGGER_STATE: RwLock<LoggerState> = RwLock::new(LoggerState {
13 level: DEFAULT_LOG_LEVEL,
14 provider: None,
15 });
16}
17
18struct LoggerState {
19 level: LogLevel,
20 provider: Option<Arc<dyn OutputLogProvider>>,
21}
22
23static INITIALIZED: AtomicBool = AtomicBool::new(false);
24
25#[derive(Clone, Debug)]
26pub enum LogLevel {
27 None,
28 Debug,
29 Info,
30 Warn,
31 Error,
32}
33
34impl From<&str> for LogLevel {
35 fn from(level: &str) -> Self {
36 match level.to_lowercase().as_str() {
37 "debug" => LogLevel::Debug,
38 "info" => LogLevel::Info,
39 "warn" => LogLevel::Warn,
40 "error" => LogLevel::Error,
41 "none" => LogLevel::None,
42 _ => DEFAULT_LOG_LEVEL,
43 }
44 }
45}
46
47impl From<u32> for LogLevel {
48 fn from(level: u32) -> Self {
49 match level {
50 0 => LogLevel::None,
51 1 => LogLevel::Error,
52 2 => LogLevel::Warn,
53 3 => LogLevel::Info,
54 4 => LogLevel::Debug,
55 _ => DEFAULT_LOG_LEVEL,
56 }
57 }
58}
59
60impl LogLevel {
61 fn to_third_party_level(&self) -> Option<Level> {
62 match self {
63 LogLevel::Debug => Some(Level::Debug),
64 LogLevel::Info => Some(Level::Info),
65 LogLevel::Warn => Some(Level::Warn),
66 LogLevel::Error => Some(Level::Error),
67 LogLevel::None => None,
68 }
69 }
70
71 fn to_number(&self) -> u32 {
72 match self {
73 LogLevel::Debug => 4,
74 LogLevel::Info => 3,
75 LogLevel::Warn => 2,
76 LogLevel::Error => 1,
77 LogLevel::None => 0,
78 }
79 }
80}
81
82pub trait OutputLogProvider: Send + Sync {
83 fn initialize(&self);
84 fn debug(&self, tag: &str, msg: String);
85 fn info(&self, tag: &str, msg: String);
86 fn warn(&self, tag: &str, msg: String);
87 fn error(&self, tag: &str, msg: String);
88 fn shutdown(&self);
89}
90
91pub fn initialize_output_logger(
92 level: &Option<LogLevel>,
93 provider: Option<Arc<dyn OutputLogProvider>>,
94) {
95 let was_initialized = INITIALIZED.swap(true, Ordering::SeqCst);
96 if was_initialized {
97 return;
98 }
99
100 let mut state = match LOGGER_STATE.write() {
101 Ok(state) => state,
102 Err(e) => {
103 eprintln!("[Statsig] Failed to acquire write lock for logger: {e}");
104 return;
105 }
106 };
107 let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
108 state.level = level.clone();
109
110 if let Some(provider_impl) = provider {
111 provider_impl.initialize();
112 state.provider = Some(provider_impl);
113 } else {
114 let final_level = match level {
115 LogLevel::None => {
116 return;
117 }
118 _ => match level.to_third_party_level() {
119 Some(level) => level,
120 None => return,
121 },
122 };
123
124 match simple_logger::init_with_level(final_level) {
125 Ok(()) => {}
126 Err(_) => {
127 log::set_max_level(final_level.to_level_filter());
128 }
129 }
130 }
131}
132
133pub fn shutdown_output_logger() {
134 let mut state = match LOGGER_STATE.write() {
135 Ok(state) => state,
136 Err(e) => {
137 eprintln!("[Statsig] Failed to acquire write lock for logger: {e}");
138 return;
139 }
140 };
141
142 if let Some(provider) = &mut state.provider {
143 provider.shutdown();
144 }
145
146 INITIALIZED.store(false, Ordering::SeqCst);
147}
148
149pub fn log_message(tag: &str, level: LogLevel, msg: String) {
150 let truncated_msg = if msg.chars().count() > MAX_CHARS {
151 let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
152 format!(
153 "{}{}",
154 msg.chars().take(visible_chars).collect::<String>(),
155 TRUNCATED_SUFFIX
156 )
157 } else {
158 msg
159 };
160
161 let sanitized_msg = sanitize(&truncated_msg);
162
163 if let Ok(state) = LOGGER_STATE.read() {
164 if let Some(provider) = &state.provider {
165 match level {
166 LogLevel::Debug => provider.debug(tag, sanitized_msg),
167 LogLevel::Info => provider.info(tag, sanitized_msg),
168 LogLevel::Warn => provider.warn(tag, sanitized_msg),
169 LogLevel::Error => provider.error(tag, sanitized_msg),
170 _ => {}
171 }
172 return;
173 }
174 }
175
176 if let Some(level) = level.to_third_party_level() {
177 let mut target = String::from("Statsig::");
178 target += tag;
179
180 match level {
181 Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
182 Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
183 Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
184 Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
185 _ => {}
186 };
187 }
188}
189
190fn sanitize(input: &str) -> String {
191 input
192 .split("secret-")
193 .enumerate()
194 .map(|(i, part)| {
195 if i == 0 {
196 part.to_string()
197 } else {
198 let (key, rest) =
199 part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
200 let sanitized_key = if key.len() > 5 {
201 format!("{}*****{}", &key[..5], rest)
202 } else {
203 format!("{key}*****{rest}")
204 };
205 format!("secret-{sanitized_key}")
206 }
207 })
208 .collect()
209}
210
211pub fn has_valid_log_level(level: &LogLevel) -> bool {
212 let state = match LOGGER_STATE.read() {
213 Ok(state) => state,
214 Err(e) => {
215 eprintln!("[Statsig] Failed to acquire read lock for logger: {e}");
216 return false;
217 }
218 };
219 let current_level = &state.level;
220 level.to_number() <= current_level.to_number()
221}
222
223#[macro_export]
224macro_rules! log_d {
225 ($tag:expr, $($arg:tt)*) => {
226 {
227 let level = $crate::output_logger::LogLevel::Debug;
228 if $crate::output_logger::has_valid_log_level(&level) {
229 $crate::output_logger::log_message($tag, level, format!($($arg)*));
230 }
231 }
232 }
233}
234
235#[macro_export]
236macro_rules! log_i {
237 ($tag:expr, $($arg:tt)*) => {
238 {
239 let level = $crate::output_logger::LogLevel::Info;
240 if $crate::output_logger::has_valid_log_level(&level) {
241 $crate::output_logger::log_message($tag, level, format!($($arg)*));
242 }
243 }
244 }
245}
246
247#[macro_export]
248macro_rules! log_w {
249 ($tag:expr, $($arg:tt)*) => {
250 {
251 let level = $crate::output_logger::LogLevel::Warn;
252 if $crate::output_logger::has_valid_log_level(&level) {
253 $crate::output_logger::log_message($tag, level, format!($($arg)*));
254 }
255 }
256 }
257}
258
259#[macro_export]
260macro_rules! log_e {
261 ($tag:expr, $($arg:tt)*) => {
262 {
263 let level = $crate::output_logger::LogLevel::Error;
264 if $crate::output_logger::has_valid_log_level(&level) {
265 $crate::output_logger::log_message($tag, level, format!($($arg)*));
266 }
267 }
268 }
269}
270
271#[macro_export]
272macro_rules! log_error_to_statsig_and_console {
273 ($ops_stats:expr, $tag:expr, $err:expr) => {
274 let event = ErrorBoundaryEvent {
275 bypass_dedupe: false,
276 exception: $err.name().to_string(),
277 info: serde_json::to_string(&$err).unwrap_or_default(),
278 tag: $tag.to_string(),
279 extra: None,
280 dedupe_key: None,
281 };
282 $ops_stats.log_error(event);
283
284 $crate::output_logger::log_message(
285 &$tag,
286 $crate::output_logger::LogLevel::Error,
287 $err.to_string(),
288 );
289 };
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use std::collections::HashMap;
296
297 #[test]
298 fn test_sanitize_url_for_logging() {
299 let test_cases = HashMap::from(
300 [
301 ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
302 ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
303 ("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"),
304 ]
305 );
306 for (before, expected) in test_cases {
307 let sanitized = sanitize(before);
308 assert!(sanitized == expected);
309 }
310 }
311
312 #[test]
313 fn test_multiple_secrets() {
314 let input = "Multiple secrets: secret-key1 and secret-key2";
315 let sanitized = sanitize(input);
316 assert_eq!(
317 sanitized,
318 "Multiple secrets: secret-key1***** and secret-key2*****"
319 );
320 }
321
322 #[test]
323 fn test_short_secret() {
324 let input = "Short secret: secret-a";
325 let sanitized = sanitize(input);
326 assert_eq!(sanitized, "Short secret: secret-a*****");
327 }
328}