1use std::sync::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 LOG_LEVEL: RwLock<LogLevel> = RwLock::new(DEFAULT_LOG_LEVEL);
12}
13
14#[derive(Clone)]
15pub enum LogLevel {
16 None,
17 Debug,
18 Info,
19 Warn,
20 Error,
21}
22
23impl From<&str> for LogLevel {
24 fn from(level: &str) -> Self {
25 match level.to_lowercase().as_str() {
26 "debug" => LogLevel::Debug,
27 "info" => LogLevel::Info,
28 "warn" => LogLevel::Warn,
29 "error" => LogLevel::Error,
30 "none" => LogLevel::None,
31 _ => DEFAULT_LOG_LEVEL,
32 }
33 }
34}
35
36impl From<u32> for LogLevel {
37 fn from(level: u32) -> Self {
38 match level {
39 0 => LogLevel::None,
40 1 => LogLevel::Error,
41 2 => LogLevel::Warn,
42 3 => LogLevel::Info,
43 4 => LogLevel::Debug,
44 _ => DEFAULT_LOG_LEVEL,
45 }
46 }
47}
48
49impl LogLevel {
50 fn to_third_party_level(&self) -> Option<Level> {
51 match self {
52 LogLevel::Debug => Some(Level::Debug),
53 LogLevel::Info => Some(Level::Info),
54 LogLevel::Warn => Some(Level::Warn),
55 LogLevel::Error => Some(Level::Error),
56 LogLevel::None => None,
57 }
58 }
59
60 fn to_number(&self) -> u32 {
61 match self {
62 LogLevel::Debug => 4,
63 LogLevel::Info => 3,
64 LogLevel::Warn => 2,
65 LogLevel::Error => 1,
66 LogLevel::None => 0,
67 }
68 }
69}
70
71pub fn initialize_simple_output_logger(level: &Option<LogLevel>) {
72 let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
73
74 if let Ok(mut lock) = LOG_LEVEL.write() {
75 *lock = level.clone();
76 }
77
78 let final_level = match level {
79 LogLevel::None => {
80 return;
81 }
82 _ => match level.to_third_party_level() {
83 Some(level) => level,
84 None => return,
85 },
86 };
87
88 match simple_logger::init_with_level(final_level) {
89 Ok(()) => {}
90 Err(_) => {
91 log::set_max_level(final_level.to_level_filter());
92 }
93 }
94}
95
96pub fn log_message(tag: &str, level: LogLevel, msg: String) {
97 let truncated_msg = if msg.chars().count() > MAX_CHARS {
98 let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
99 format!(
100 "{}{}",
101 msg.chars().take(visible_chars).collect::<String>(),
102 TRUNCATED_SUFFIX
103 )
104 } else {
105 msg
106 };
107
108 let sanitized_msg = sanitize(&truncated_msg);
109
110 if let Some(level) = level.to_third_party_level() {
111 let mut target = String::from("Statsig::");
112 target += tag;
113
114 match level {
115 Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
116 Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
117 Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
118 Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
119 _ => {}
120 };
121 }
122}
123
124fn sanitize(input: &str) -> String {
125 input
126 .split("secret-")
127 .enumerate()
128 .map(|(i, part)| {
129 if i == 0 {
130 part.to_string()
131 } else {
132 let (key, rest) =
133 part.split_at(part.chars().take_while(|c| c.is_alphanumeric()).count());
134 let sanitized_key = if key.len() > 5 {
135 format!("{}*****{}", &key[..5], rest)
136 } else {
137 format!("{}*****{}", key, rest)
138 };
139 format!("secret-{}", sanitized_key)
140 }
141 })
142 .collect()
143}
144
145pub fn has_valid_log_level(level: &LogLevel) -> bool {
146 let current_level = match LOG_LEVEL.read() {
147 Ok(lock) => lock,
148 Err(_) => return false,
149 };
150
151 level.to_number() <= current_level.to_number()
152}
153
154#[macro_export]
155macro_rules! log_d {
156 ($tag:expr, $($arg:tt)*) => {
157 {
158 let level = $crate::output_logger::LogLevel::Debug;
159 if $crate::output_logger::has_valid_log_level(&level) {
160 $crate::output_logger::log_message($tag, level, format!($($arg)*));
161 }
162 }
163 }
164}
165
166#[macro_export]
167macro_rules! log_i {
168 ($tag:expr, $($arg:tt)*) => {
169 {
170 let level = $crate::output_logger::LogLevel::Info;
171 if $crate::output_logger::has_valid_log_level(&level) {
172 $crate::output_logger::log_message($tag, level, format!($($arg)*));
173 }
174 }
175 }
176}
177
178#[macro_export]
179macro_rules! log_w {
180 ($tag:expr, $($arg:tt)*) => {
181 {
182 let level = $crate::output_logger::LogLevel::Warn;
183 if $crate::output_logger::has_valid_log_level(&level) {
184 $crate::output_logger::log_message($tag, level, format!($($arg)*));
185 }
186 }
187 }
188}
189
190#[macro_export]
191macro_rules! log_e {
192 ($tag:expr, $($arg:tt)*) => {
193 {
194 let level = $crate::output_logger::LogLevel::Error;
195 if $crate::output_logger::has_valid_log_level(&level) {
196 $crate::output_logger::log_message($tag, level, format!($($arg)*));
197 }
198 }
199 }
200}
201
202#[macro_export]
203macro_rules! log_error_to_statsig_and_console {
204 ($ops_stats:expr, $tag:expr, $err:expr) => {
205 let event = ErrorBoundaryEvent {
206 bypass_dedupe: false,
207 info: $err.clone(),
208 tag: $tag.to_string(),
209 extra: None,
210 dedupe_key: None,
211 };
212 $ops_stats.log_error(event);
213
214 $crate::output_logger::log_message(
215 &$tag,
216 $crate::output_logger::LogLevel::Error,
217 $err.to_string(),
218 );
219 };
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use std::collections::HashMap;
226
227 #[test]
228 fn test_sanitize_url_for_logging() {
229 let test_cases = HashMap::from(
230 [
231 ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
232 ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
233 ("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"),
234 ]
235 );
236 for (before, expected) in test_cases {
237 let sanitized = sanitize(before);
238 assert!(sanitized == expected);
239 }
240 }
241
242 #[test]
243 fn test_multiple_secrets() {
244 let input = "Multiple secrets: secret-key1 and secret-key2";
245 let sanitized = sanitize(input);
246 assert_eq!(
247 sanitized,
248 "Multiple secrets: secret-key1***** and secret-key2*****"
249 );
250 }
251
252 #[test]
253 fn test_short_secret() {
254 let input = "Short secret: secret-a";
255 let sanitized = sanitize(input);
256 assert_eq!(sanitized, "Short secret: secret-a*****");
257 }
258}