use flate2::write::GzEncoder;
use flate2::Compression;
use reqwest::{Client, StatusCode};
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SendError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("http: {0}")]
Http(#[from] reqwest::Error),
#[error("bad request")]
BadRequest,
#[error("unauthorized")]
Unauthorized,
#[error("payload too large")]
PayloadTooLarge,
#[error("rate limited (retry after {0:?})")]
RateLimited(Option<Duration>),
#[error("server error: {0}")]
Server(u16),
#[error("network / other")]
Other,
}
pub struct HttpSender {
client: Client,
endpoint: String,
}
impl HttpSender {
pub fn new(endpoint: String, app_version: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.user_agent(format!("atomcode-telemetry/{}", app_version))
.build()
.expect("reqwest client build");
Self { client, endpoint }
}
pub async fn send_segment(&self, path: &Path, dropped: u64) -> Result<(), SendError> {
let body = std::fs::read(path)?;
let gz = gzip(&body)?;
let resp = self
.client
.post(&self.endpoint)
.header("content-type", "application/x-ndjson")
.header("content-encoding", "gzip")
.header("x-atomcode-dropped", dropped.to_string())
.header("x-atomcode-schema", crate::SCHEMA_VERSION.to_string())
.body(gz)
.send()
.await?;
map_status(resp.status(), resp.headers())
}
}
fn gzip(buf: &[u8]) -> std::io::Result<Vec<u8>> {
let mut enc = GzEncoder::new(Vec::with_capacity(buf.len() / 2), Compression::fast());
enc.write_all(buf)?;
enc.finish()
}
fn map_status(s: StatusCode, h: &reqwest::header::HeaderMap) -> Result<(), SendError> {
match s.as_u16() {
200 | 202 => Ok(()),
400 => Err(SendError::BadRequest),
401 | 403 => Err(SendError::Unauthorized),
413 => Err(SendError::PayloadTooLarge),
429 => {
let ra = h
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs);
Err(SendError::RateLimited(ra))
}
c if (500..600).contains(&c) => Err(SendError::Server(c)),
_ => Err(SendError::Other),
}
}