Skip to main content

litellm_rs/core/audit/
config.rs

1//! Configuration for the Audit Logging system
2
3use crate::config::models::defaults::default_true;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7use super::types::LogLevel;
8
9/// Main configuration for the audit logging system
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditConfig {
12    /// Whether audit logging is enabled
13    #[serde(default)]
14    pub enabled: bool,
15
16    /// Minimum log level
17    #[serde(default)]
18    pub min_level: LogLevel,
19
20    /// Whether to log requests
21    #[serde(default = "default_true")]
22    pub log_requests: bool,
23
24    /// Whether to log responses
25    #[serde(default = "default_true")]
26    pub log_responses: bool,
27
28    /// Whether to log user actions
29    #[serde(default = "default_true")]
30    pub log_user_actions: bool,
31
32    /// Whether to log request bodies
33    #[serde(default)]
34    pub log_request_body: bool,
35
36    /// Whether to log response bodies
37    #[serde(default)]
38    pub log_response_body: bool,
39
40    /// Maximum body size to log (bytes)
41    #[serde(default = "default_max_body_size")]
42    pub max_body_size: usize,
43
44    /// Headers to include in logs (empty = none)
45    #[serde(default)]
46    pub include_headers: Vec<String>,
47
48    /// Headers to exclude from logs
49    #[serde(default = "default_excluded_headers")]
50    pub exclude_headers: Vec<String>,
51
52    /// Paths to exclude from logging
53    #[serde(default = "default_excluded_paths")]
54    pub exclude_paths: Vec<String>,
55
56    /// File output configuration
57    #[serde(default)]
58    pub file_output: Option<FileOutputConfig>,
59
60    /// Buffer size for async logging
61    #[serde(default = "default_buffer_size")]
62    pub buffer_size: usize,
63
64    /// Flush interval in milliseconds
65    #[serde(default = "default_flush_interval")]
66    pub flush_interval_ms: u64,
67
68    /// Retention period in days (0 = no retention)
69    #[serde(default)]
70    pub retention_days: u32,
71
72    /// Whether to redact sensitive data
73    #[serde(default = "default_true")]
74    pub redact_sensitive: bool,
75
76    /// Patterns to redact
77    #[serde(default = "default_redact_patterns")]
78    pub redact_patterns: Vec<String>,
79}
80
81fn default_max_body_size() -> usize {
82    10 * 1024 // 10KB
83}
84
85fn default_excluded_headers() -> Vec<String> {
86    vec![
87        "authorization".to_string(),
88        "x-api-key".to_string(),
89        "cookie".to_string(),
90        "set-cookie".to_string(),
91    ]
92}
93
94fn default_excluded_paths() -> Vec<String> {
95    vec![
96        "/health".to_string(),
97        "/metrics".to_string(),
98        "/ready".to_string(),
99        "/live".to_string(),
100    ]
101}
102
103fn default_buffer_size() -> usize {
104    1000
105}
106
107fn default_flush_interval() -> u64 {
108    1000 // 1 second
109}
110
111fn default_redact_patterns() -> Vec<String> {
112    vec![
113        r#"sk-[a-zA-Z0-9_-]{20,}"#.to_string(),
114        r#"sk-ant-[a-zA-Z0-9_-]{20,}"#.to_string(),
115        r#"gw-[a-zA-Z0-9_-]{20,}"#.to_string(),
116        r#"AKIA[0-9A-Z]{16}"#.to_string(),
117        r#"(?i)bearer\s+[a-z0-9._~+/=-]{10,}"#.to_string(),
118        r#"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"#.to_string(),
119        r#"(?i)authorization["']?\s*[:=]\s*["']?[^"',\s}]+"#.to_string(),
120        r#"api[_-]?key["']?\s*[:=]\s*["']?[a-zA-Z0-9-_]+"#.to_string(),
121    ]
122}
123
124impl Default for AuditConfig {
125    fn default() -> Self {
126        Self {
127            enabled: false,
128            min_level: LogLevel::Info,
129            log_requests: true,
130            log_responses: true,
131            log_user_actions: true,
132            log_request_body: false,
133            log_response_body: false,
134            max_body_size: default_max_body_size(),
135            include_headers: Vec::new(),
136            exclude_headers: default_excluded_headers(),
137            exclude_paths: default_excluded_paths(),
138            file_output: None,
139            buffer_size: default_buffer_size(),
140            flush_interval_ms: default_flush_interval(),
141            retention_days: 0,
142            redact_sensitive: true,
143            redact_patterns: default_redact_patterns(),
144        }
145    }
146}
147
148impl AuditConfig {
149    /// Create a new audit config
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    /// Enable audit logging
155    pub fn enable(mut self) -> Self {
156        self.enabled = true;
157        self
158    }
159
160    /// Set minimum log level
161    pub fn with_min_level(mut self, level: LogLevel) -> Self {
162        self.min_level = level;
163        self
164    }
165
166    /// Enable file output
167    pub fn with_file_output(mut self, path: impl Into<PathBuf>) -> Self {
168        self.file_output = Some(FileOutputConfig {
169            path: path.into(),
170            ..Default::default()
171        });
172        self
173    }
174
175    /// Enable request body logging
176    pub fn with_request_body(mut self, enabled: bool) -> Self {
177        self.log_request_body = enabled;
178        self
179    }
180
181    /// Enable response body logging
182    pub fn with_response_body(mut self, enabled: bool) -> Self {
183        self.log_response_body = enabled;
184        self
185    }
186
187    /// Set max body size
188    pub fn with_max_body_size(mut self, size: usize) -> Self {
189        self.max_body_size = size;
190        self
191    }
192
193    /// Add excluded path
194    pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
195        self.exclude_paths.push(path.into());
196        self
197    }
198
199    /// Set retention days
200    pub fn with_retention_days(mut self, days: u32) -> Self {
201        self.retention_days = days;
202        self
203    }
204
205    /// Check if a path should be excluded
206    pub fn is_path_excluded(&self, path: &str) -> bool {
207        self.exclude_paths.iter().any(|p| path.starts_with(p))
208    }
209
210    /// Check if a header should be excluded
211    pub fn is_header_excluded(&self, header: &str) -> bool {
212        let header_lower = header.to_lowercase();
213        self.exclude_headers
214            .iter()
215            .any(|h| h.to_lowercase() == header_lower)
216    }
217
218    /// Validate the configuration
219    pub fn validate(&self) -> Result<(), String> {
220        if self.buffer_size == 0 {
221            return Err("audit buffer_size must be greater than zero".to_string());
222        }
223        if self.flush_interval_ms == 0 {
224            return Err("audit flush_interval_ms must be greater than zero".to_string());
225        }
226        Ok(())
227    }
228}
229
230/// File output configuration
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct FileOutputConfig {
233    /// Path to the log file
234    pub path: PathBuf,
235
236    /// Whether to rotate logs
237    #[serde(default = "default_true")]
238    pub rotate: bool,
239
240    /// Maximum file size before rotation (bytes)
241    #[serde(default = "default_max_file_size")]
242    pub max_file_size: u64,
243
244    /// Maximum number of backup files
245    #[serde(default = "default_max_backups")]
246    pub max_backups: u32,
247
248    /// Whether to compress rotated files
249    #[serde(default)]
250    pub compress: bool,
251}
252
253fn default_max_file_size() -> u64 {
254    100 * 1024 * 1024 // 100MB
255}
256
257fn default_max_backups() -> u32 {
258    10
259}
260
261impl Default for FileOutputConfig {
262    fn default() -> Self {
263        Self {
264            path: PathBuf::from(r"./logs/audit.log"),
265            rotate: true,
266            max_file_size: default_max_file_size(),
267            max_backups: default_max_backups(),
268            compress: false,
269        }
270    }
271}
272
273impl FileOutputConfig {
274    /// Create a new file output config
275    pub fn new(path: impl Into<PathBuf>) -> Self {
276        Self {
277            path: path.into(),
278            ..Default::default()
279        }
280    }
281
282    /// Set max file size
283    pub fn with_max_size(mut self, size: u64) -> Self {
284        self.max_file_size = size;
285        self
286    }
287
288    /// Set max backups
289    pub fn with_max_backups(mut self, count: u32) -> Self {
290        self.max_backups = count;
291        self
292    }
293
294    /// Enable compression
295    pub fn with_compression(mut self, enabled: bool) -> Self {
296        self.compress = enabled;
297        self
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_audit_config_default() {
307        let config = AuditConfig::default();
308        assert!(!config.enabled);
309        assert!(config.log_requests);
310        assert!(config.log_responses);
311        assert!(!config.log_request_body);
312        assert!(config.redact_sensitive);
313    }
314
315    #[test]
316    fn test_audit_config_builder() {
317        let config = AuditConfig::new()
318            .enable()
319            .with_min_level(LogLevel::Debug)
320            .with_file_output(r"./logs/test.log")
321            .with_request_body(true)
322            .with_response_body(true)
323            .with_max_body_size(1024)
324            .with_retention_days(30)
325            .exclude_path(r"/internal");
326
327        assert!(config.enabled);
328        assert_eq!(config.min_level, LogLevel::Debug);
329        assert!(config.file_output.is_some());
330        assert!(config.log_request_body);
331        assert!(config.log_response_body);
332        assert_eq!(config.max_body_size, 1024);
333        assert_eq!(config.retention_days, 30);
334    }
335
336    #[test]
337    fn test_path_exclusion() {
338        let config = AuditConfig::default();
339        assert!(config.is_path_excluded(r"/health"));
340        assert!(config.is_path_excluded(r"/health/live"));
341        assert!(config.is_path_excluded(r"/metrics"));
342        assert!(!config.is_path_excluded(r"/v1/chat/completions"));
343    }
344
345    #[test]
346    fn test_header_exclusion() {
347        let config = AuditConfig::default();
348        assert!(config.is_header_excluded("Authorization"));
349        assert!(config.is_header_excluded("authorization"));
350        assert!(config.is_header_excluded("X-API-Key"));
351        assert!(!config.is_header_excluded("Content-Type"));
352    }
353
354    #[test]
355    fn test_default_redact_patterns_compile() {
356        for pattern in default_redact_patterns() {
357            regex::Regex::new(&pattern).unwrap_or_else(|error| {
358                panic!("default redact pattern should compile: {pattern}: {error}")
359            });
360        }
361    }
362
363    #[test]
364    fn test_file_output_config() {
365        let config = FileOutputConfig::new(r"./logs/audit.log")
366            .with_max_size(50 * 1024 * 1024)
367            .with_max_backups(5)
368            .with_compression(true);
369
370        assert_eq!(config.path, PathBuf::from(r"./logs/audit.log"));
371        assert_eq!(config.max_file_size, 50 * 1024 * 1024);
372        assert_eq!(config.max_backups, 5);
373        assert!(config.compress);
374    }
375
376    #[test]
377    fn test_config_validation() {
378        let config = AuditConfig::default();
379        assert!(config.validate().is_ok());
380
381        let enabled_config = AuditConfig::new().enable();
382        assert!(enabled_config.validate().is_ok());
383
384        let mut invalid = AuditConfig::new().enable();
385        invalid.buffer_size = 1;
386        assert!(invalid.validate().is_ok());
387        invalid.buffer_size = 0;
388        assert!(invalid.validate().is_err());
389        invalid.buffer_size = 1;
390        invalid.flush_interval_ms = 0;
391        assert!(invalid.validate().is_err());
392    }
393
394    #[test]
395    fn test_config_serialization() {
396        let config = AuditConfig::new()
397            .enable()
398            .with_file_output(r"./logs/test.log");
399
400        let json = serde_json::to_string(&config).unwrap();
401        let deserialized: AuditConfig = serde_json::from_str(&json).unwrap();
402
403        assert_eq!(config.enabled, deserialized.enabled);
404        assert!(deserialized.file_output.is_some());
405    }
406}