use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime};
use parking_lot::RwLock;
use serde::Serialize;
use super::types::{CacheStatus, MissReason};
#[derive(Debug, Clone, Serialize)]
pub struct AuditEntry {
pub request_id: u64,
pub timestamp: u64,
pub query: String,
pub top_k: Option<usize>,
pub cache_status: CacheStatus,
pub latency_ms: u64,
pub result_count: usize,
pub success: bool,
pub error: Option<String>,
pub client_id: Option<String>,
pub upstream_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub miss_reason: Option<MissReason>,
}
impl AuditEntry {
pub fn new(request_id: u64, query: &str, top_k: Option<usize>) -> Self {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let query = if query.len() > 200 {
format!("{}...", &query[..200])
} else {
query.to_string()
};
Self {
request_id,
timestamp,
query,
top_k,
cache_status: CacheStatus::Miss,
latency_ms: 0,
result_count: 0,
success: false,
error: None,
client_id: None,
upstream_id: None,
miss_reason: None,
}
}
pub fn with_success(mut self, result_count: usize) -> Self {
self.success = true;
self.result_count = result_count;
self
}
pub fn with_error(mut self, error: &str) -> Self {
self.success = false;
self.error = Some(error.to_string());
self
}
pub fn with_cache_status(mut self, status: CacheStatus) -> Self {
self.cache_status = status;
self
}
pub fn with_latency(mut self, latency: Duration) -> Self {
self.latency_ms = latency.as_millis() as u64;
self
}
pub fn with_client_id(mut self, client_id: &str) -> Self {
self.client_id = Some(client_id.to_string());
self
}
pub fn with_upstream_id(mut self, upstream_id: &str) -> Self {
self.upstream_id = Some(upstream_id.to_string());
self
}
pub fn with_miss_reason(mut self, reason: MissReason) -> Self {
self.miss_reason = Some(reason);
self
}
}
pub struct AuditBuilder {
entry: AuditEntry,
start: Instant,
}
impl AuditBuilder {
pub fn new(request_id: u64, query: &str, top_k: Option<usize>) -> Self {
Self {
entry: AuditEntry::new(request_id, query, top_k),
start: Instant::now(),
}
}
pub fn cache_status(mut self, status: CacheStatus) -> Self {
self.entry.cache_status = status;
self
}
pub fn client_id(mut self, client_id: &str) -> Self {
self.entry.client_id = Some(client_id.to_string());
self
}
pub fn upstream_id(mut self, upstream_id: &str) -> Self {
self.entry.upstream_id = Some(upstream_id.to_string());
self
}
pub fn success(mut self, result_count: usize) -> AuditEntry {
self.entry.latency_ms = self.start.elapsed().as_millis() as u64;
self.entry.success = true;
self.entry.result_count = result_count;
self.entry
}
pub fn error(mut self, error: &str) -> AuditEntry {
self.entry.latency_ms = self.start.elapsed().as_millis() as u64;
self.entry.success = false;
self.entry.error = Some(error.to_string());
self.entry
}
}
pub struct AuditLog {
entries: RwLock<Vec<AuditEntry>>,
capacity: usize,
write_pos: AtomicU64,
total_logged: AtomicU64,
next_request_id: AtomicU64,
}
impl AuditLog {
pub fn new(capacity: usize) -> Self {
Self {
entries: RwLock::new(Vec::with_capacity(capacity)),
capacity,
write_pos: AtomicU64::new(0),
total_logged: AtomicU64::new(0),
next_request_id: AtomicU64::new(1),
}
}
pub fn next_request_id(&self) -> u64 {
self.next_request_id.fetch_add(1, Ordering::Relaxed)
}
pub fn start(&self, query: &str, top_k: Option<usize>) -> AuditBuilder {
AuditBuilder::new(self.next_request_id(), query, top_k)
}
pub fn log(&self, entry: AuditEntry) {
let mut entries = self.entries.write();
let pos = self.write_pos.fetch_add(1, Ordering::Relaxed) as usize;
if entries.len() < self.capacity {
entries.push(entry);
} else {
#[allow(clippy::arithmetic_side_effects)]
let idx = pos % self.capacity;
if let Some(slot) = entries.get_mut(idx) {
*slot = entry;
}
}
self.total_logged.fetch_add(1, Ordering::Relaxed);
}
pub fn recent(&self, limit: usize) -> Vec<AuditEntry> {
let entries = self.entries.read();
let len = entries.len().min(limit);
let mut result = Vec::with_capacity(len);
let total = self.write_pos.load(Ordering::Relaxed) as usize;
for i in 0..len {
#[allow(clippy::arithmetic_side_effects)]
let idx = if total > 0 && total >= entries.len() {
total.saturating_sub(1).saturating_sub(i) % entries.len()
} else {
entries.len().saturating_sub(1_usize.saturating_add(i))
};
if let Some(entry) = entries.get(idx) {
result.push(entry.clone());
}
}
result
}
pub fn filter<F>(&self, predicate: F, limit: usize) -> Vec<AuditEntry>
where
F: Fn(&AuditEntry) -> bool,
{
let entries = self.entries.read();
entries
.iter()
.filter(|e| predicate(e))
.take(limit)
.cloned()
.collect()
}
pub fn for_client(&self, client_id: &str, limit: usize) -> Vec<AuditEntry> {
self.filter(|e| e.client_id.as_deref() == Some(client_id), limit)
}
pub fn failures(&self, limit: usize) -> Vec<AuditEntry> {
self.filter(|e| !e.success, limit)
}
pub fn slow(&self, threshold_ms: u64, limit: usize) -> Vec<AuditEntry> {
self.filter(|e| e.latency_ms > threshold_ms, limit)
}
pub fn stats(&self) -> AuditStats {
let entries = self.entries.read();
let total = entries.len();
if total == 0 {
return AuditStats::default();
}
let mut success_count = 0u64;
let mut cache_hit_count = 0u64;
let mut total_latency = 0u64;
for entry in entries.iter() {
if entry.success {
success_count = success_count.saturating_add(1);
}
if entry.cache_status == CacheStatus::Hit {
cache_hit_count = cache_hit_count.saturating_add(1);
}
total_latency = total_latency.saturating_add(entry.latency_ms);
}
let total_u64 = total as u64;
let avg_latency_ms = total_latency.checked_div(total_u64).unwrap_or(0);
AuditStats {
total_logged: self.total_logged.load(Ordering::Relaxed),
entries_in_buffer: total,
success_rate: success_count as f64 / total as f64,
cache_hit_rate: cache_hit_count as f64 / total as f64,
avg_latency_ms,
}
}
pub fn clear(&self) {
let mut entries = self.entries.write();
entries.clear();
self.write_pos.store(0, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct AuditStats {
pub total_logged: u64,
pub entries_in_buffer: usize,
pub success_rate: f64,
pub cache_hit_rate: f64,
pub avg_latency_ms: u64,
}
#[cfg(test)]
#[path = "tests/audit_tests.rs"]
mod tests;