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