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