1pub mod actions;
3pub mod codec;
4use crate::protocol::{Event, SpawnContext};
5use crate::server::{
6 accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS},
7 connection::ConnectionId,
8};
9use crate::state::{
10 server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo},
11 AccessLogOwner,
12};
13use crate::{console_error, console_info};
14use anyhow::Result;
15use bytes::Bytes;
16use http_body_util::{BodyExt, Full, Limited};
17use hyper::{
18 body::Incoming,
19 header::{HeaderValue, ALLOW, AUTHORIZATION, CONTENT_TYPE, RETRY_AFTER},
20 server::conn::http1,
21 service::service_fn,
22 Method, Request, Response, StatusCode,
23};
24use hyper_util::rt::{TokioIo, TokioTimer};
25use serde_json::json;
26use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration};
27use tokio::sync::Notify;
28
29pub const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
30pub const BODY_TIMEOUT: Duration = Duration::from_secs(30);
31pub const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
32pub const MAX_HEADER_BYTES: usize = 32 * 1024;
33pub const MAX_HEADERS: usize = 64;
34const CAP_REFUSAL:&[u8]=b"HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 19\r\nConnection: close\r\nRetry-After: 5\r\n\r\nconnection capacity";
35#[derive(Clone)]
36struct Config {
37 token: Option<String>,
38 llm_fallback: bool,
39 path: String,
40}
41pub struct PrometheusRemoteWriteServer;
42impl PrometheusRemoteWriteServer {
43 pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
44 let token = ctx
45 .startup_params
46 .as_ref()
47 .map(|p| p.get_optional_string("auth_token"))
48 .transpose()?
49 .flatten();
50 if let Some(token) = &token {
51 codec::validate_token(token)?;
52 }
53 let llm_fallback = ctx
54 .startup_params
55 .as_ref()
56 .map(|p| p.get_optional_bool("llm_fallback"))
57 .transpose()?
58 .flatten()
59 .unwrap_or(codec::DEFAULT_LLM_FALLBACK);
60 let path = ctx
61 .startup_params
62 .as_ref()
63 .map(|p| p.get_optional_string("path"))
64 .transpose()?
65 .flatten()
66 .unwrap_or_else(|| codec::DEFAULT_PATH.into());
67 codec::validate_path(&path)?;
68 let config = Arc::new(Config {
69 token,
70 llm_fallback,
71 path,
72 });
73 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
74 let local = listener.local_addr()?;
75 console_info!(
76 ctx.status_tx,
77 "PrometheusRemoteWrite push listening on {} (auth_required={},llm_fallback={})",
78 local,
79 config.token.is_some(),
80 llm_fallback
81 );
82 let owner = ctx.state.clone();
83 let server_id = ctx.server_id;
84 owner.spawn_server_task(server_id, async move {
85 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
86 loop {
87 let (stream, peer, permit) = match accept_bounded(&listener, &limiter, CAP_REFUSAL, "PrometheusRemoteWrite", Some(&ctx.status_tx)).await {
88 Ok(value) => value,
89 Err(error) => {
90 console_error!(ctx.status_tx, "PrometheusRemoteWrite decision=fail_closed_accept_error error={}", error);
91 break;
92 }
93 };
94 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
95 let now = crate::utils::clock::Instant::now();
96 ctx.state.add_connection_to_server(server_id, ConnectionState {
97 id, remote_addr: peer, local_addr: local, bytes_sent: 0,
98 bytes_received: 0, packets_sent: 0, packets_received: 0,
99 last_activity: now, status: ConnectionStatus::Active,
100 status_changed_at: now, protocol_info: ProtocolConnectionInfo::empty(),
101 }).await;
102 let child_ctx = ctx.clone();
103 let config = config.clone();
104 ctx.state.spawn_server_task(server_id, async move {
105 let _permit = permit;
106 let write_ready = Arc::new(Notify::new());
107 let service_ready = write_ready.clone();
108 let request_ctx = child_ctx.clone();
109 let service = service_fn(move |request| {
110 let ctx = request_ctx.clone();
111 let config = config.clone();
112 let ready = service_ready.clone();
113 async move {
114 let response = handle_request(request, peer, id, &ctx, &config).await;
115 ready.notify_one();
116 Ok::<_, Infallible>(response)
117 }
118 });
119 let mut builder = http1::Builder::new();
120 builder.keep_alive(false).timer(TokioTimer::new())
121 .header_read_timeout(HEADER_TIMEOUT).max_headers(MAX_HEADERS)
122 .max_buf_size(MAX_HEADER_BYTES);
123 let connection = builder.serve_connection(TokioIo::new(stream), service);
124 tokio::pin!(connection);
125 tokio::select! {
126 result = &mut connection => {
127 if let Err(error) = result {
128 console_error!(child_ctx.status_tx,
129 "PrometheusRemoteWrite decision=fail_closed_http_framing error={}", error);
130 }
131 }
132 _ = async {
133 write_ready.notified().await;
134 tokio::time::sleep(WRITE_TIMEOUT).await;
135 } => {
136 console_error!(child_ctx.status_tx,
137 "PrometheusRemoteWrite decision=fail_closed_response_write_timeout");
138 }
139 }
140 child_ctx.state.remove_connection_from_server(server_id, id).await;
141 let _ = child_ctx.status_tx.send("__UPDATE_UI__".into());
142 }).await;
143 }
144 }).await;
145 Ok(local)
146 }
147}
148fn response(status: u16, message: &str, retry: Option<u16>) -> Response<Full<Bytes>> {
149 let bytes = if status == 204 {
150 Vec::new()
151 } else {
152 message.as_bytes().to_vec()
153 };
154 let mut r = Response::new(Full::new(Bytes::from(bytes)));
155 *r.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
156 r.headers_mut().insert(
157 CONTENT_TYPE,
158 HeaderValue::from_static("text/plain; charset=utf-8"),
159 );
160 if let Some(n) = retry {
161 if let Ok(v) = HeaderValue::from_str(&n.to_string()) {
162 r.headers_mut().insert(RETRY_AFTER, v);
163 }
164 }
165 r.headers_mut().insert(
166 "X-Prometheus-Remote-Write-Version",
167 HeaderValue::from_static("0.1.0"),
168 );
169 r
170}
171fn error(status: u16, message: &str) -> Response<Full<Bytes>> {
172 response(status, message, None)
173}
174fn authorized(headers: &hyper::HeaderMap, expected: Option<&str>) -> bool {
175 let Some(expected) = expected else {
176 return true;
177 };
178 let values = headers.get_all(AUTHORIZATION).iter().collect::<Vec<_>>();
179 if values.len() != 1 {
180 return false;
181 }
182 let Some((scheme, token)) = values[0].to_str().ok().and_then(|v| v.split_once(' ')) else {
183 return false;
184 };
185 let token = token.trim_start_matches(' ');
186 if !scheme.eq_ignore_ascii_case("Bearer") || codec::validate_token(token).is_err() {
187 return false;
188 }
189 let mut difference = expected.len() ^ token.len();
190 for at in 0..expected.len().max(token.len()) {
191 difference |= usize::from(
192 expected.as_bytes().get(at).copied().unwrap_or(0)
193 ^ token.as_bytes().get(at).copied().unwrap_or(0),
194 );
195 }
196 difference == 0
197}
198fn request_encoding(content_type: &str) -> bool {
201 let mut parts = content_type.split(';');
202 if !parts
203 .next()
204 .is_some_and(|v| v.trim().eq_ignore_ascii_case("application/x-protobuf"))
205 {
206 return false;
207 }
208 let mut proto_seen = false;
209 for parameter in parts {
210 let Some((name, value)) = parameter.trim().split_once('=') else {
211 return false;
212 };
213 if proto_seen || !name.trim().eq_ignore_ascii_case("proto") {
214 return false;
215 }
216 let value = value.trim();
217 let value = if value.starts_with('"') {
218 match value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
219 Some(v) => v,
220 None => return false,
221 }
222 } else {
223 value
224 };
225 if value != "prometheus.WriteRequest" {
226 return false;
227 }
228 proto_seen = true;
229 }
230 true
231}
232async fn handle_request(
233 request: Request<Incoming>,
234 peer: SocketAddr,
235 id: ConnectionId,
236 ctx: &SpawnContext,
237 config: &Config,
238) -> Response<Full<Bytes>> {
239 let response = process_request(request, peer, id, ctx, config).await;
240 if response.status().is_success() {
241 console_info!(
242 ctx.status_tx,
243 "PrometheusRemoteWrite decision=accept status={}",
244 response.status().as_u16()
245 );
246 } else {
247 console_error!(
248 ctx.status_tx,
249 "PrometheusRemoteWrite decision=fail_closed_http_reject status={}",
250 response.status().as_u16()
251 );
252 }
253 response
254}
255async fn process_request(
256 request: Request<Incoming>,
257 peer: SocketAddr,
258 id: ConnectionId,
259 ctx: &SpawnContext,
260 config: &Config,
261) -> Response<Full<Bytes>> {
262 let (parts, body) = request.into_parts();
263 if parts.headers.len() > MAX_HEADERS
264 || parts
265 .headers
266 .iter()
267 .map(|(k, v)| k.as_str().len() + v.as_bytes().len() + 4)
268 .sum::<usize>()
269 > MAX_HEADER_BYTES
270 {
271 return error(413, "Request header byte/count limit");
272 }
273 let bytes = match tokio::time::timeout(
274 BODY_TIMEOUT,
275 Limited::new(body, codec::MAX_BODY_BYTES).collect(),
276 )
277 .await
278 {
279 Err(_) => return error(408, "Request body deadline exceeded"),
280 Ok(Err(error)) => {
281 return if error
282 .downcast_ref::<http_body_util::LengthLimitError>()
283 .is_some()
284 {
285 self::error(413, "Request body limit exceeded")
286 } else {
287 self::error(400, "Incomplete HTTP body")
288 };
289 }
290 Ok(Ok(body)) => body.to_bytes(),
291 };
292 ctx.state
293 .update_connection_stats(
294 ctx.server_id,
295 id,
296 Some(bytes.len() as u64),
297 None,
298 Some(1),
299 None,
300 )
301 .await;
302 if parts.uri.path() != config.path {
303 return error(404, "Only the configured write endpoint is implemented");
304 }
305 if parts.method != Method::POST {
306 let mut r = error(405, "Only POST is supported");
307 r.headers_mut()
308 .insert(ALLOW, HeaderValue::from_static("POST"));
309 return r;
310 }
311 if parts.uri.query().is_some() {
312 return error(400, "Write query parameters unsupported");
313 }
314 if !authorized(&parts.headers, config.token.as_deref()) {
315 return error(401, "Unauthorized bearer credential");
316 }
317 for name in [
318 "content-encoding",
319 "content-type",
320 "user-agent",
321 "x-prometheus-remote-write-version",
322 ] {
323 if parts.headers.get_all(name).iter().count() != 1 {
324 return error(400, "One required protocol header expected");
325 }
326 }
327 let text = |name| {
328 parts
329 .headers
330 .get(name)
331 .and_then(|v| v.to_str().ok())
332 .unwrap_or("")
333 };
334 if !text("content-encoding")
335 .trim()
336 .eq_ignore_ascii_case("snappy")
337 || !request_encoding(text("content-type"))
338 {
339 return error(
340 415,
341 "Expected Snappy block and prometheus.WriteRequest protobuf",
342 );
343 }
344 if text("x-prometheus-remote-write-version") != "0.1.0" {
345 return error(
346 415,
347 "Only published remote write 1.0 is supported (wire header0.1.0)",
348 );
349 }
350 if text("user-agent").is_empty() || text("user-agent").len() > 512 {
351 return error(400, "User-Agent required/byte limit");
352 }
353 let decoded = match codec::decode_batch(&bytes) {
354 Ok(v) => v,
355 Err(_) => return error(400, "Malformed, invalid or over-limit remote write samples"),
356 };
357 let event = Event::new(
358 &actions::REMOTE_WRITE_EVENT,
359 json!({
360 "series": decoded.series,
361 "ignored_fields": decoded.ignored_fields,
362 "authenticated": true,
363 "auth_required": config.token.is_some(),
364 "source_addr": peer.to_string(),
365 "version": "1.0",
366 "series_count": decoded.series.len(),
367 "sample_count": decoded.series.iter().map(|s|s.samples.len()).sum::<usize>(),
368 "durable_storage": false,
369 }),
370 );
371 let configured = ctx
372 .state
373 .get_event_handler_config(ctx.server_id)
374 .await
375 .is_some_and(|c| c.find_handler("remote_write_request").is_some());
376 let decision = if configured || config.llm_fallback {
377 match crate::llm::action_helper::call_llm(
378 &ctx.llm_client,
379 &ctx.state,
380 ctx.server_id,
381 Some(id),
382 &event,
383 &actions::PrometheusRemoteWriteProtocol::new(),
384 )
385 .await
386 {
387 Ok(result) if result.failures.is_empty() => {
388 let decisions = result
389 .protocol_results
390 .iter()
391 .filter_map(|r| match r {
392 crate::llm::ActionResult::Custom { name, data }
393 if name == "remote_write_decision" =>
394 {
395 Some(data.clone())
396 }
397 _ => None,
398 })
399 .collect::<Vec<_>>();
400 if decisions.len() > 1 {
401 return failed(ctx, id, event.data, "fail_closed_multiple_decisions").await;
402 }
403 match decisions
404 .first()
405 .map(|d| serde_json::from_value::<actions::Decision>(d.clone()))
406 .transpose()
407 {
408 Ok(Some(d)) => d,
409 Ok(None) => actions::Decision::Accept,
410 Err(_) => {
411 return failed(ctx, id, event.data, "fail_closed_invalid_decision").await
412 }
413 }
414 }
415 Ok(_) => return failed(ctx, id, event.data, "fail_closed_handler_action_error").await,
416 Err(_) => return failed(ctx, id, event.data, "fail_closed_handler_error").await,
417 }
418 } else {
419 ctx.state
420 .record_access_log(
421 AccessLogOwner::Server(ctx.server_id.as_u32()),
422 "PrometheusRemoteWrite",
423 Some(id.as_u32()),
424 "remote_write_request",
425 event.data.clone(),
426 vec![json!({"type":"accept_remote_write_samples"})],
427 )
428 .await;
429 actions::Decision::Accept
430 };
431 match decision {
432 actions::Decision::Accept => response(204, "", None),
433 actions::Decision::Reject {
434 status,
435 message,
436 retry_after_seconds,
437 } => response(status, &message, retry_after_seconds),
438 }
439}
440async fn failed(
441 ctx: &SpawnContext,
442 id: ConnectionId,
443 data: serde_json::Value,
444 decision: &str,
445) -> Response<Full<Bytes>> {
446 console_error!(ctx.status_tx, "PrometheusRemoteWrite decision={}", decision);
447 ctx.state
448 .record_access_log(
449 AccessLogOwner::Server(ctx.server_id.as_u32()),
450 "PrometheusRemoteWrite",
451 Some(id.as_u32()),
452 "remote_write_handler_failed",
453 data,
454 vec![json!({"decision":decision})],
455 )
456 .await;
457 error(503, "Write handler failed; no acceptance confirmed")
458}