truthlinked_sdk/
logging.rs

1use std::time::{Duration, Instant};
2use tracing::{debug, error, info, warn};
3
4/// Logging configuration for requests and responses
5#[derive(Debug, Clone)]
6pub struct LoggingConfig {
7    /// Enable request logging
8    pub log_requests: bool,
9    /// Enable response logging
10    pub log_responses: bool,
11    /// Enable error logging
12    pub log_errors: bool,
13    /// Enable timing information
14    pub log_timing: bool,
15    /// Maximum body size to log (bytes)
16    pub max_body_size: usize,
17    /// Log level for successful requests
18    pub success_level: LogLevel,
19    /// Log level for failed requests
20    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, // 1KB
39            success_level: LogLevel::Debug,
40            error_level: LogLevel::Error,
41        }
42    }
43}
44
45impl LoggingConfig {
46    /// Production logging config (minimal logging)
47    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    /// Development logging config (verbose logging)
60    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, // 4KB
67            success_level: LogLevel::Info,
68            error_level: LogLevel::Error,
69        }
70    }
71    
72    /// Disable all logging
73    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
86/// Request/response logger with credential redaction
87pub struct RequestLogger {
88    config: LoggingConfig,
89}
90
91impl RequestLogger {
92    pub fn new(config: LoggingConfig) -> Self {
93        Self { config }
94    }
95    
96    /// Log outgoing request
97    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    /// Log incoming response
132    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    /// Log request error with timing information
174    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    /// Redact sensitive headers
214    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    /// Redact request/response body
228    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                // Simple credential redaction for common patterns
240                let mut result = text.to_string();
241                
242                // Redact sso_token values
243                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                // Redact af_token values
252                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                // Redact license_key values
261                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    /// Redact credential values
276    pub fn redact_credential(&self, value: &str) -> String {
277        if value.len() <= 8 {
278            "***".to_string()
279        } else {
280            // Special case for Bearer tokens - use 4 chars at end
281            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
290/// Request timing tracker
291pub 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}