use std::time::Duration;
use async_trait::async_trait;
use secrecy::{ExposeSecret, SecretString};
use url::Url;
use crate::error::TelemetryError;
use crate::event::Event;
use crate::sink::TelemetrySink;
#[derive(Debug, Clone)]
pub struct HttpSinkConfig {
pub endpoint: Url,
pub bearer_token: Option<SecretString>,
pub timeout: Duration,
pub user_agent: String,
pub allow_insecure_endpoint: bool,
}
impl Default for HttpSinkConfig {
fn default() -> Self {
Self {
endpoint: Url::parse("https://telemetry.invalid/").expect("static url"),
bearer_token: None,
timeout: Duration::from_secs(5),
user_agent: "rtb-telemetry/0.2".into(),
allow_insecure_endpoint: false,
}
}
}
#[derive(Debug, Clone)]
pub struct HttpSink {
config: HttpSinkConfig,
client: reqwest::Client,
}
impl HttpSink {
pub fn new(config: HttpSinkConfig) -> Result<Self, TelemetryError> {
Self::validate(&config)?;
let client = reqwest::Client::builder()
.timeout(config.timeout)
.user_agent(config.user_agent.clone())
.build()
.map_err(|e| TelemetryError::Http(format!("client build: {e}")))?;
Ok(Self { config, client })
}
#[must_use]
pub const fn with_client(config: HttpSinkConfig, client: reqwest::Client) -> Self {
Self { config, client }
}
fn validate(config: &HttpSinkConfig) -> Result<(), TelemetryError> {
match config.endpoint.scheme() {
"https" => Ok(()),
"http" if config.allow_insecure_endpoint => Ok(()),
other => Err(TelemetryError::Http(format!(
"endpoint scheme {other:?} not permitted (set allow_insecure_endpoint for tests)"
))),
}
}
}
#[async_trait]
impl TelemetrySink for HttpSink {
async fn emit(&self, event: &Event) -> Result<(), TelemetryError> {
Self::validate(&self.config)?;
let redacted = event.redacted();
let body = WireBody::from(&redacted);
let mut req = self.client.post(self.config.endpoint.clone()).json(&body);
if let Some(tok) = &self.config.bearer_token {
req = req.header("Authorization", format!("Bearer {}", tok.expose_secret()));
}
let resp = req.send().await.map_err(|e| TelemetryError::Http(e.to_string()))?;
if !resp.status().is_success() {
return Err(TelemetryError::Http(format!("non-2xx response: {}", resp.status())));
}
Ok(())
}
}
#[derive(Debug, serde::Serialize)]
struct WireBody<'a> {
#[serde(flatten)]
event: &'a Event,
severity: &'static str,
}
impl<'a> From<&'a Event> for WireBody<'a> {
fn from(event: &'a Event) -> Self {
Self { event, severity: crate::event::severity_of(event) }
}
}