use crate::config::models::defaults::default_true;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::types::LogLevel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub min_level: LogLevel,
#[serde(default = "default_true")]
pub log_requests: bool,
#[serde(default = "default_true")]
pub log_responses: bool,
#[serde(default = "default_true")]
pub log_user_actions: bool,
#[serde(default)]
pub log_request_body: bool,
#[serde(default)]
pub log_response_body: bool,
#[serde(default = "default_max_body_size")]
pub max_body_size: usize,
#[serde(default)]
pub include_headers: Vec<String>,
#[serde(default = "default_excluded_headers")]
pub exclude_headers: Vec<String>,
#[serde(default = "default_excluded_paths")]
pub exclude_paths: Vec<String>,
#[serde(default)]
pub file_output: Option<FileOutputConfig>,
#[serde(default = "default_buffer_size")]
pub buffer_size: usize,
#[serde(default = "default_flush_interval")]
pub flush_interval_ms: u64,
#[serde(default)]
pub retention_days: u32,
#[serde(default = "default_true")]
pub redact_sensitive: bool,
#[serde(default = "default_redact_patterns")]
pub redact_patterns: Vec<String>,
}
fn default_max_body_size() -> usize {
10 * 1024 }
fn default_excluded_headers() -> Vec<String> {
vec![
"authorization".to_string(),
"x-api-key".to_string(),
"cookie".to_string(),
"set-cookie".to_string(),
]
}
fn default_excluded_paths() -> Vec<String> {
vec![
"/health".to_string(),
"/metrics".to_string(),
"/ready".to_string(),
"/live".to_string(),
]
}
fn default_buffer_size() -> usize {
1000
}
fn default_flush_interval() -> u64 {
1000 }
fn default_redact_patterns() -> Vec<String> {
vec![
r#"sk-[a-zA-Z0-9]{20,}"#.to_string(),
r#"api[_-]?key["']?\s*[:=]\s*["']?[a-zA-Z0-9-_]+"#.to_string(),
]
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
enabled: false,
min_level: LogLevel::Info,
log_requests: true,
log_responses: true,
log_user_actions: true,
log_request_body: false,
log_response_body: false,
max_body_size: default_max_body_size(),
include_headers: Vec::new(),
exclude_headers: default_excluded_headers(),
exclude_paths: default_excluded_paths(),
file_output: None,
buffer_size: default_buffer_size(),
flush_interval_ms: default_flush_interval(),
retention_days: 0,
redact_sensitive: true,
redact_patterns: default_redact_patterns(),
}
}
}
impl AuditConfig {
pub fn new() -> Self {
Self::default()
}
pub fn enable(mut self) -> Self {
self.enabled = true;
self
}
pub fn with_min_level(mut self, level: LogLevel) -> Self {
self.min_level = level;
self
}
pub fn with_file_output(mut self, path: impl Into<PathBuf>) -> Self {
self.file_output = Some(FileOutputConfig {
path: path.into(),
..Default::default()
});
self
}
pub fn with_request_body(mut self, enabled: bool) -> Self {
self.log_request_body = enabled;
self
}
pub fn with_response_body(mut self, enabled: bool) -> Self {
self.log_response_body = enabled;
self
}
pub fn with_max_body_size(mut self, size: usize) -> Self {
self.max_body_size = size;
self
}
pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
self.exclude_paths.push(path.into());
self
}
pub fn with_retention_days(mut self, days: u32) -> Self {
self.retention_days = days;
self
}
pub fn is_path_excluded(&self, path: &str) -> bool {
self.exclude_paths.iter().any(|p| path.starts_with(p))
}
pub fn is_header_excluded(&self, header: &str) -> bool {
let header_lower = header.to_lowercase();
self.exclude_headers
.iter()
.any(|h| h.to_lowercase() == header_lower)
}
pub fn validate(&self) -> Result<(), String> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileOutputConfig {
pub path: PathBuf,
#[serde(default = "default_true")]
pub rotate: bool,
#[serde(default = "default_max_file_size")]
pub max_file_size: u64,
#[serde(default = "default_max_backups")]
pub max_backups: u32,
#[serde(default)]
pub compress: bool,
}
fn default_max_file_size() -> u64 {
100 * 1024 * 1024 }
fn default_max_backups() -> u32 {
10
}
impl Default for FileOutputConfig {
fn default() -> Self {
Self {
path: PathBuf::from(r"./logs/audit.log"),
rotate: true,
max_file_size: default_max_file_size(),
max_backups: default_max_backups(),
compress: false,
}
}
}
impl FileOutputConfig {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
..Default::default()
}
}
pub fn with_max_size(mut self, size: u64) -> Self {
self.max_file_size = size;
self
}
pub fn with_max_backups(mut self, count: u32) -> Self {
self.max_backups = count;
self
}
pub fn with_compression(mut self, enabled: bool) -> Self {
self.compress = enabled;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_config_default() {
let config = AuditConfig::default();
assert!(!config.enabled);
assert!(config.log_requests);
assert!(config.log_responses);
assert!(!config.log_request_body);
assert!(config.redact_sensitive);
}
#[test]
fn test_audit_config_builder() {
let config = AuditConfig::new()
.enable()
.with_min_level(LogLevel::Debug)
.with_file_output(r"./logs/test.log")
.with_request_body(true)
.with_response_body(true)
.with_max_body_size(1024)
.with_retention_days(30)
.exclude_path(r"/internal");
assert!(config.enabled);
assert_eq!(config.min_level, LogLevel::Debug);
assert!(config.file_output.is_some());
assert!(config.log_request_body);
assert!(config.log_response_body);
assert_eq!(config.max_body_size, 1024);
assert_eq!(config.retention_days, 30);
}
#[test]
fn test_path_exclusion() {
let config = AuditConfig::default();
assert!(config.is_path_excluded(r"/health"));
assert!(config.is_path_excluded(r"/health/live"));
assert!(config.is_path_excluded(r"/metrics"));
assert!(!config.is_path_excluded(r"/v1/chat/completions"));
}
#[test]
fn test_header_exclusion() {
let config = AuditConfig::default();
assert!(config.is_header_excluded("Authorization"));
assert!(config.is_header_excluded("authorization"));
assert!(config.is_header_excluded("X-API-Key"));
assert!(!config.is_header_excluded("Content-Type"));
}
#[test]
fn test_file_output_config() {
let config = FileOutputConfig::new(r"./logs/audit.log")
.with_max_size(50 * 1024 * 1024)
.with_max_backups(5)
.with_compression(true);
assert_eq!(config.path, PathBuf::from(r"./logs/audit.log"));
assert_eq!(config.max_file_size, 50 * 1024 * 1024);
assert_eq!(config.max_backups, 5);
assert!(config.compress);
}
#[test]
fn test_config_validation() {
let config = AuditConfig::default();
assert!(config.validate().is_ok());
let enabled_config = AuditConfig::new().enable();
assert!(enabled_config.validate().is_ok());
}
#[test]
fn test_config_serialization() {
let config = AuditConfig::new()
.enable()
.with_file_output(r"./logs/test.log");
let json = serde_json::to_string(&config).unwrap();
let deserialized: AuditConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.enabled, deserialized.enabled);
assert!(deserialized.file_output.is_some());
}
}