1use std::time::{Duration, Instant};
2use tracing::{debug, error, info, warn};
3
4#[derive(Debug, Clone)]
6pub struct LoggingConfig {
7 pub log_requests: bool,
9 pub log_responses: bool,
11 pub log_errors: bool,
13 pub log_timing: bool,
15 pub max_body_size: usize,
17 pub success_level: LogLevel,
19 pub error_level: LogLevel,
21}
22
23#[derive(Debug, Clone)]
24pub enum LogLevel {
25 Debug,
26 Info,
27 Warn,
28 Error,
29}
30
31impl Default for LoggingConfig {
32 fn default() -> Self {
33 Self {
34 log_requests: true,
35 log_responses: true,
36 log_errors: true,
37 log_timing: true,
38 max_body_size: 1024, success_level: LogLevel::Debug,
40 error_level: LogLevel::Error,
41 }
42 }
43}
44
45impl LoggingConfig {
46 pub fn production() -> Self {
48 Self {
49 log_requests: false,
50 log_responses: false,
51 log_errors: true,
52 log_timing: true,
53 max_body_size: 0,
54 success_level: LogLevel::Debug,
55 error_level: LogLevel::Error,
56 }
57 }
58
59 pub fn development() -> Self {
61 Self {
62 log_requests: true,
63 log_responses: true,
64 log_errors: true,
65 log_timing: true,
66 max_body_size: 4096, success_level: LogLevel::Info,
68 error_level: LogLevel::Error,
69 }
70 }
71
72 pub fn none() -> Self {
74 Self {
75 log_requests: false,
76 log_responses: false,
77 log_errors: false,
78 log_timing: false,
79 max_body_size: 0,
80 success_level: LogLevel::Debug,
81 error_level: LogLevel::Error,
82 }
83 }
84}
85
86pub struct RequestLogger {
88 config: LoggingConfig,
89}
90
91impl RequestLogger {
92 pub fn new(config: LoggingConfig) -> Self {
93 Self { config }
94 }
95
96 pub fn log_request(&self, method: &str, url: &str, headers: &[(&str, &str)], body: &[u8]) {
98 if !self.config.log_requests {
99 return;
100 }
101
102 let safe_headers = self.redact_headers(headers);
103 let safe_body = self.redact_body(body);
104
105 match self.config.success_level {
106 LogLevel::Debug => debug!(
107 method = method,
108 url = url,
109 headers = ?safe_headers,
110 body = safe_body,
111 "Sending request"
112 ),
113 LogLevel::Info => info!(
114 method = method,
115 url = url,
116 "Sending request"
117 ),
118 LogLevel::Warn => warn!(
119 method = method,
120 url = url,
121 "Sending request"
122 ),
123 LogLevel::Error => error!(
124 method = method,
125 url = url,
126 "Sending request"
127 ),
128 }
129 }
130
131 pub fn log_response(&self, status: u16, headers: &[(&str, &str)], body: &[u8], duration: Duration) {
133 if !self.config.log_responses {
134 return;
135 }
136
137 let safe_headers = self.redact_headers(headers);
138 let safe_body = self.redact_body(body);
139 let duration_ms = duration.as_millis();
140
141 let log_level = if status >= 400 {
142 &self.config.error_level
143 } else {
144 &self.config.success_level
145 };
146
147 match log_level {
148 LogLevel::Debug => debug!(
149 status = status,
150 duration_ms = duration_ms,
151 headers = ?safe_headers,
152 body = safe_body,
153 "Received response"
154 ),
155 LogLevel::Info => info!(
156 status = status,
157 duration_ms = duration_ms,
158 "Received response"
159 ),
160 LogLevel::Warn => warn!(
161 status = status,
162 duration_ms = duration_ms,
163 "Received response"
164 ),
165 LogLevel::Error => error!(
166 status = status,
167 duration_ms = duration_ms,
168 "Received response"
169 ),
170 }
171 }
172
173 pub fn log_error(&self, method: &str, url: &str, error: &str, duration: Duration) {
175 if !self.config.log_errors {
176 return;
177 }
178
179 let duration_ms = duration.as_millis();
180
181 match self.config.error_level {
182 LogLevel::Debug => debug!(
183 method = method,
184 url = url,
185 error = error,
186 duration_ms = duration_ms,
187 "Request failed"
188 ),
189 LogLevel::Info => info!(
190 method = method,
191 url = url,
192 error = error,
193 duration_ms = duration_ms,
194 "Request failed"
195 ),
196 LogLevel::Warn => warn!(
197 method = method,
198 url = url,
199 error = error,
200 duration_ms = duration_ms,
201 "Request failed"
202 ),
203 LogLevel::Error => error!(
204 method = method,
205 url = url,
206 error = error,
207 duration_ms = duration_ms,
208 "Request failed"
209 ),
210 }
211 }
212
213 pub fn redact_headers(&self, headers: &[(&str, &str)]) -> Vec<(String, String)> {
215 headers.iter().map(|(name, value)| {
216 let safe_value = if name.to_lowercase().contains("authorization")
217 || name.to_lowercase().contains("cookie")
218 || name.to_lowercase().contains("token") {
219 self.redact_credential(value)
220 } else {
221 value.to_string()
222 };
223 (name.to_string(), safe_value)
224 }).collect()
225 }
226
227 pub fn redact_body(&self, body: &[u8]) -> String {
229 if body.is_empty() {
230 return "".to_string();
231 }
232
233 if body.len() > self.config.max_body_size {
234 return format!("<body too large: {} bytes>", body.len());
235 }
236
237 match std::str::from_utf8(body) {
238 Ok(text) => {
239 let mut result = text.to_string();
241
242 if let Some(start) = result.find(r#""sso_token":""#) {
244 let value_start = start + r#""sso_token":""#.len();
245 if let Some(end) = result[value_start..].find('"') {
246 let value_end = value_start + end;
247 result.replace_range(value_start..value_end, "***");
248 }
249 }
250
251 if let Some(start) = result.find(r#""af_token":""#) {
253 let value_start = start + r#""af_token":""#.len();
254 if let Some(end) = result[value_start..].find('"') {
255 let value_end = value_start + end;
256 result.replace_range(value_start..value_end, "***");
257 }
258 }
259
260 if let Some(start) = result.find(r#""license_key":""#) {
262 let value_start = start + r#""license_key":""#.len();
263 if let Some(end) = result[value_start..].find('"') {
264 let value_end = value_start + end;
265 result.replace_range(value_start..value_end, "***");
266 }
267 }
268
269 result
270 }
271 Err(_) => format!("<binary data: {} bytes>", body.len()),
272 }
273 }
274
275 pub fn redact_credential(&self, value: &str) -> String {
277 if value.len() <= 8 {
278 "***".to_string()
279 } else {
280 if value.starts_with("Bearer ") {
282 format!("{}...{}", &value[..3], &value[value.len()-4..])
283 } else {
284 format!("{}...{}", &value[..3], &value[value.len()-3..])
285 }
286 }
287 }
288}
289
290pub struct RequestTimer {
292 start: Instant,
293}
294
295impl RequestTimer {
296 pub fn new() -> Self {
297 Self {
298 start: Instant::now(),
299 }
300 }
301
302 pub fn elapsed(&self) -> Duration {
303 self.start.elapsed()
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_header_redaction() {
313 let logger = RequestLogger::new(LoggingConfig::development());
314
315 let headers = vec![
316 ("Content-Type", "application/json"),
317 ("Authorization", "Bearer tl_free_secret123456789"),
318 ("X-Custom", "safe-value"),
319 ];
320
321 let redacted = logger.redact_headers(&headers);
322
323 assert_eq!(redacted[0].1, "application/json");
324 assert_eq!(redacted[1].1, "Bea...6789");
325 assert_eq!(redacted[2].1, "safe-value");
326 }
327
328 #[test]
329 fn test_body_redaction() {
330 let logger = RequestLogger::new(LoggingConfig::development());
331
332 let body = r#"{"sso_token":"secret123","other":"safe"}"#.as_bytes();
333 let redacted = logger.redact_body(body);
334
335 assert!(redacted.contains(r#""sso_token":"***""#));
336 assert!(redacted.contains(r#""other":"safe""#));
337 }
338}