use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_rustls::rustls::crypto::ring;
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
use tracing::{debug, info, warn};
use crate::{CaptureConfig, CapturedRequest};
const MAX_ATTEMPTS: u32 = 3;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone)]
pub struct CaptureSink {
tx: mpsc::Sender<CapturedRequest>,
dropped: Arc<AtomicU64>,
}
impl CaptureSink {
pub(crate) fn channel(capacity: usize) -> (Self, mpsc::Receiver<CapturedRequest>) {
let (tx, rx) = mpsc::channel(capacity.max(1));
(Self { tx, dropped: Arc::new(AtomicU64::new(0)) }, rx)
}
pub fn enqueue(&self, capture: CapturedRequest) {
if self.tx.try_send(capture).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
pub struct CaptureShipper;
impl CaptureShipper {
pub fn spawn(cfg: &CaptureConfig, token: Option<String>) -> Result<CaptureSink, String> {
let client = build_client()?;
let url = format!("{}/ingest/captures", cfg.controlplane_url.trim_end_matches('/'));
let (sink, rx) = CaptureSink::channel(cfg.queue_capacity);
let dropped = Arc::clone(&sink.dropped);
tokio::spawn(ship_loop(rx, client, url, token, dropped));
Ok(sink)
}
}
fn build_client() -> Result<reqwest::Client, String> {
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let tls = ClientConfig::builder_with_provider(Arc::new(ring::default_provider()))
.with_safe_default_protocol_versions()
.map_err(|e| format!("rustls client config: {e}"))?
.with_root_certificates(roots)
.with_no_client_auth();
reqwest::Client::builder()
.use_preconfigured_tls(tls)
.timeout(REQUEST_TIMEOUT)
.build()
.map_err(|e| format!("http client: {e}"))
}
async fn ship_loop(
mut rx: mpsc::Receiver<CapturedRequest>,
client: reqwest::Client,
url: String,
token: Option<String>,
dropped: Arc<AtomicU64>,
) {
let (mut shipped, mut failed) = (0u64, 0u64);
while let Some(capture) = rx.recv().await {
let mut ok = false;
for attempt in 1..=MAX_ATTEMPTS {
let mut rb = client.post(&url).json(&capture);
if let Some(t) = &token {
rb = rb.bearer_auth(t);
}
match rb.send().await {
Ok(resp) if resp.status().is_success() => {
ok = true;
break;
}
Ok(resp) => debug!(status = %resp.status(), attempt, "capture ship: non-2xx"),
Err(e) => debug!(error = %e, attempt, "capture ship: request error"),
}
if attempt < MAX_ATTEMPTS {
tokio::time::sleep(Duration::from_millis(200 * u64::from(attempt))).await;
}
}
if ok {
shipped += 1;
} else {
failed += 1;
warn!(request_id = %capture.request_id, "capture shipping failed after retries; dropping");
}
if (shipped + failed).is_multiple_of(50) {
info!(shipped, failed, dropped = dropped.load(Ordering::Relaxed), "capture shipper stats");
}
}
}