use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use parking_lot::Mutex;
use reqwest::Client;
use tracing::{error, warn};
use super::audit::{AuditEntry, AuditError, AuditExporter, WebhookExportConfig};
use crate::http::client::build_ssrf_safe_client;
const MAX_RETRIES: u32 = 3;
const BASE_RETRY_DELAY: Duration = Duration::from_millis(500);
const WEBHOOK_TIMEOUT: Duration = Duration::from_secs(10);
pub struct WebhookAuditExporter {
client: Client,
config: WebhookExportConfig,
pub(crate) buffer: Arc<Mutex<Vec<AuditEntry>>>,
}
impl WebhookAuditExporter {
pub fn new(config: &WebhookExportConfig) -> Result<Self, AuditError> {
let client = build_ssrf_safe_client(WEBHOOK_TIMEOUT)
.map_err(|e| AuditError::Export(format!("webhook client init: {e}")))?;
Ok(Self {
client,
config: config.clone(),
buffer: Arc::new(Mutex::new(Vec::with_capacity(config.batch_size))),
})
}
async fn send_batch(&self, entries: &[AuditEntry]) -> Result<(), AuditError> {
if entries.is_empty() {
return Ok(());
}
let mut request = self.client.post(&self.config.url).json(entries);
for (key, value) in &self.config.headers {
request = request.header(key.as_str(), value.as_str());
}
let mut last_err = None;
for attempt in 0..MAX_RETRIES {
if attempt > 0 {
let delay = BASE_RETRY_DELAY * 2u32.saturating_pow(attempt - 1);
tokio::time::sleep(delay).await;
warn!(attempt, "Retrying webhook audit export");
}
match request.try_clone() {
Some(req) => match req.send().await {
Ok(resp) if resp.status().is_success() => return Ok(()),
Ok(resp) => {
let status = resp.status();
last_err = Some(format!("HTTP {status}"));
},
Err(e) => {
last_err = Some(e.to_string());
},
},
None => {
return Err(AuditError::Export(
"webhook request could not be cloned for retry".to_string(),
));
},
}
}
Err(AuditError::Export(format!(
"webhook export failed after {MAX_RETRIES} attempts: {}",
last_err.unwrap_or_default()
)))
}
}
#[async_trait]
impl AuditExporter for WebhookAuditExporter {
async fn export(&self, entry: &AuditEntry) -> Result<(), AuditError> {
let should_flush = {
let mut buf = self.buffer.lock();
buf.push(entry.clone());
buf.len() >= self.config.batch_size
};
if should_flush {
self.flush().await?;
}
Ok(())
}
async fn flush(&self) -> Result<(), AuditError> {
let entries: Vec<AuditEntry> = {
let mut buf = self.buffer.lock();
std::mem::take(&mut *buf)
};
if entries.is_empty() {
return Ok(());
}
if let Err(e) = self.send_batch(&entries).await {
error!(error = %e, count = entries.len(), "Failed to flush webhook audit batch");
return Err(e);
}
Ok(())
}
}