netget/client/prometheus_remote_write/
transport.rs1use crate::server::prometheus_remote_write::codec::WriteBatch;
2use anyhow::{ensure, Context, Result};
3use bytes::Bytes;
4use http_body_util::{BodyExt, Full, Limited};
5use hyper::{
6 client::conn::http1,
7 header::{AUTHORIZATION, CONNECTION, CONTENT_ENCODING, CONTENT_TYPE, HOST, USER_AGENT},
8 Request, Uri,
9};
10use hyper_util::rt::TokioIo;
11use serde::Serialize;
12use std::time::Duration;
13pub const IO_TIMEOUT: Duration = Duration::from_secs(10);
14pub const MIN_BACKOFF: Duration = Duration::from_millis(100);
15pub const MAX_BACKOFF: Duration = Duration::from_secs(5);
16pub const MAX_RESPONSE_BYTES: usize = 64 * 1024;
17pub const MAX_RESPONSE_HEADERS: usize = 64;
18pub const MAX_RESPONSE_HEADER_BYTES: usize = 32 * 1024;
19#[derive(Clone)]
20pub struct Origin {
21 pub authority: String,
22 pub connect_addr: String,
23}
24impl Origin {
25 pub fn parse(value: &str) -> Result<Self> {
26 ensure!(
27 !value.contains(['#', '@']),
28 "HTTP origin cannot contain credentials/fragment"
29 );
30 let value = if value.starts_with("http://") {
31 value.to_owned()
32 } else {
33 format!("http://{value}")
34 };
35 let uri: Uri = value.parse().context("invalid HTTP origin")?;
36 ensure!(
37 uri.scheme_str() == Some("http")
38 && uri.query().is_none()
39 && matches!(uri.path(), "" | "/"),
40 "cleartext HTTP origin only, without path/query"
41 );
42 let authority = uri.authority().context("HTTP host required")?;
43 ensure!(authority.as_str().len() <= 1024, "HTTP origin byte limit");
44 Ok(Self {
45 authority: authority.as_str().into(),
46 connect_addr: format!(
47 "{}:{}",
48 authority.host(),
49 authority.port_u16().unwrap_or(9090)
50 ),
51 })
52 }
53}
54#[derive(Clone)]
55pub struct Config {
56 pub origin: Origin,
57 pub token: Option<String>,
58 pub path: String,
59 pub retry_429: bool,
60}
61#[derive(Clone, Debug, Serialize)]
62pub struct WriteResponse {
63 pub series_count: usize,
64 pub sample_count: usize,
65 pub status: u16,
66 pub accepted: bool,
67 pub attempts: u64,
68 pub durable_storage_confirmed: bool,
69}
70pub async fn write(config: Config, batch: WriteBatch, body: Vec<u8>) -> Result<WriteResponse> {
71 let mut attempts = 0u64;
72 let mut backoff = MIN_BACKOFF;
73 loop {
74 attempts = attempts.saturating_add(1);
75 match write_once(&config, &body).await {
76 Ok(status) if (500..600).contains(&status) || (status == 429 && config.retry_429) => {}
77 Ok(status) => {
78 return Ok(WriteResponse {
79 series_count: batch.series.len(),
80 sample_count: batch.series.iter().map(|s| s.samples.len()).sum(),
81 status,
82 accepted: (200..300).contains(&status),
83 attempts,
84 durable_storage_confirmed: false,
85 })
86 }
87 Err(_) => {}
90 }
91 tokio::time::sleep(backoff).await;
94 backoff = backoff.saturating_mul(2).min(MAX_BACKOFF);
95 }
96}
97pub async fn write_once(config: &Config, body: &[u8]) -> Result<u16> {
98 tokio::time::timeout(IO_TIMEOUT, async {
99 let socket = tokio::net::TcpStream::connect(&config.origin.connect_addr).await?;
100 let (mut sender, connection) = http1::Builder::new()
101 .max_headers(MAX_RESPONSE_HEADERS)
102 .max_buf_size(MAX_RESPONSE_HEADER_BYTES)
103 .handshake(TokioIo::new(socket))
104 .await?;
105 let mut request = Request::builder()
106 .method("POST")
107 .uri(&config.path)
108 .header(HOST, &config.origin.authority)
109 .header(CONNECTION, "close")
110 .header(CONTENT_TYPE, "application/x-protobuf")
111 .header(CONTENT_ENCODING, "snappy")
112 .header(USER_AGENT, concat!("netget/", env!("CARGO_PKG_VERSION")))
113 .header("X-Prometheus-Remote-Write-Version", "0.1.0");
114 if let Some(token) = &config.token {
115 request = request.header(AUTHORIZATION, format!("Bearer {token}"));
116 }
117 let request = request.body(Full::new(Bytes::copy_from_slice(body)))?;
118 let exchange = async {
119 let response = sender.send_request(request).await?;
120 let (parts, body) = response.into_parts();
121 ensure!(
122 parts.headers.len() <= MAX_RESPONSE_HEADERS
123 && parts
124 .headers
125 .iter()
126 .map(|(k, v)| k.as_str().len() + v.as_bytes().len() + 4)
127 .sum::<usize>()
128 <= MAX_RESPONSE_HEADER_BYTES,
129 "response header count/byte limit"
130 );
131 Limited::new(body, MAX_RESPONSE_BYTES)
135 .collect()
136 .await
137 .map_err(|_| anyhow::anyhow!("invalid/oversized response body"))?;
138 Ok::<_, anyhow::Error>(parts.status.as_u16())
139 };
140 tokio::pin!(connection);
141 tokio::pin!(exchange);
142 tokio::select! {
143 result = &mut exchange => result,
144 result = &mut connection => { result.context("HTTP driver failed")?; exchange.await }
145 }
146 })
147 .await
148 .context("remote write exchange deadline exceeded")?
149}