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