jacquardSnapshot

← snapshot

2649 bytes
//! The background task that batches envelopes into NDJSON POSTs.
//!
//! Invariant: this task never propagates a failure to its caller and never
//! logs at error level. A telemetry outage must not become an outage, and
//! logging an ingest failure at error level would feed the very log-shipping
//! path that reports errors, looping the shipper into shipping itself.

use std::time::Duration;

use tokio::sync::mpsc::Receiver;
use tokio::time::interval;

use crate::S10Config;
use crate::envelope::Envelope;

/// Bounded queue capacity: enough to absorb a burst without unbounded
/// memory growth. A full queue drops the newest envelope rather than
/// blocking the caller — `try_send` in `Telemetry::emit`.
pub(crate) const QUEUE_CAP: usize = 2048;

const BATCH_SIZE: usize = 64;
const FLUSH_INTERVAL: Duration = Duration::from_secs(2);

pub(crate) async fn run(config: S10Config, mut rx: Receiver<Envelope>) {
    let client = reqwest::Client::new();
    let endpoint = format!("{}/v2/envelopes", config.url.trim_end_matches('/'));
    let mut ticker = interval(FLUSH_INTERVAL);
    let mut batch = Vec::with_capacity(BATCH_SIZE);

    loop {
        tokio::select! {
            received = rx.recv() => {
                let Some(envelope) = received else {
                    // The last `Telemetry` handle was dropped: flush what's
                    // left and stop.
                    flush(&client, &endpoint, &config.key, &mut batch).await;
                    return;
                };
                batch.push(envelope);
                if batch.len() >= BATCH_SIZE {
                    flush(&client, &endpoint, &config.key, &mut batch).await;
                }
            }
            _ = ticker.tick() => {
                flush(&client, &endpoint, &config.key, &mut batch).await;
            }
        }
    }
}

async fn flush(client: &reqwest::Client, endpoint: &str, key: &str, batch: &mut Vec<Envelope>) {
    if batch.is_empty() {
        return;
    }
    let body = batch
        .iter()
        .map(Envelope::to_ndjson_line)
        .collect::<Vec<_>>()
        .join("\n");
    batch.clear();

    // Best-effort: a dropped or non-2xx response is silently absorbed. The
    // ingest's own contract (a wrong key 401s silently) already means a
    // caller cannot distinguish "delivered" from "misconfigured" without
    // checking the console — telemetry must never gate the product on that.
    let _ = client
        .post(endpoint)
        .header("Authorization", format!("Bearer {key}"))
        .header("Content-Type", "application/x-ndjson")
        .body(body)
        .send()
        .await;
}