dynomite/stats/rest.rs
1//! Hand-rolled HTTP/1.1 server exposing the stats snapshot as JSON.
2//!
3//! This module implements a minimal stats query surface: a `GET /`
4//! (and `GET /info`) request that returns the latest snapshot. Every
5//! other request returns an empty `200 OK` with body `OK\r\n`.
6
7#![allow(clippy::needless_continue)]
8
9use std::io;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Duration;
13
14use parking_lot::Mutex;
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::{TcpListener, TcpStream};
17use tracing::Instrument as _;
18
19use crate::admin::cluster_info::{format_text, ClusterInfoSnapshot, RingSnapshot};
20use crate::stats::prometheus::render_prometheus;
21use crate::stats::snapshot::Snapshot;
22
23/// Type alias for a closure that produces a fresh
24/// [`ClusterInfoSnapshot`] every time the `/cluster-info.txt`
25/// route is hit.
26///
27/// The closure must be `Send + Sync` so a clone can be moved
28/// into each accept-handler task. The runtime owns the closure;
29/// embedders set it via
30/// [`StatsServer::with_cluster_info_provider`].
31pub type ClusterInfoProvider = Arc<dyn Fn() -> ClusterInfoSnapshot + Send + Sync>;
32
33/// Type alias for a closure that produces a fresh
34/// [`RingSnapshot`] every time the `/ring` route is hit.
35///
36/// The closure must be `Send + Sync` so a clone can be moved
37/// into each accept-handler task. Embedders set it via
38/// [`StatsServer::with_ring_provider`]; when unset the `/ring`
39/// route returns `503 Service Unavailable`.
40pub type RingProvider = Arc<dyn Fn() -> RingSnapshot + Send + Sync>;
41
42/// Maximum number of bytes the server will read for an HTTP request
43/// line plus headers. Requests larger than this are rejected.
44///
45/// # Examples
46///
47/// ```
48/// assert!(dynomite::stats::MAX_REQUEST_BYTES >= 1024);
49/// ```
50pub const MAX_REQUEST_BYTES: usize = 8 * 1024;
51
52/// Maximum number of headers parsed in a single request.
53///
54/// # Examples
55///
56/// ```
57/// assert!(dynomite::stats::MAX_HEADERS > 0);
58/// ```
59pub const MAX_HEADERS: usize = 32;
60
61/// Maximum time the server waits for a single read from a connected
62/// peer before closing the socket. Protects tokio tasks from
63/// slow-loris clients.
64const READ_TIMEOUT: Duration = Duration::from_secs(5);
65
66/// A bound TCP listener serving the stats endpoint.
67///
68/// Construct via [`StatsServer::bind`] then call
69/// [`StatsServer::run`] to accept connections in a loop, or
70/// [`StatsServer::accept_one`] for one-shot tests.
71///
72/// # Examples
73///
74/// ```no_run
75/// use std::sync::Arc;
76/// use dynomite::stats::{Snapshot, StatsServer};
77/// use parking_lot::Mutex;
78///
79/// # async fn _example() -> std::io::Result<()> {
80/// let sink = Arc::new(Mutex::new(Snapshot::default()));
81/// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink).await?;
82/// let _addr = server.local_addr()?;
83/// # Ok(())
84/// # }
85/// ```
86pub struct StatsServer {
87 listener: TcpListener,
88 source: Arc<Mutex<Snapshot>>,
89 cluster_info: Option<ClusterInfoProvider>,
90 ring: Option<RingProvider>,
91}
92
93impl StatsServer {
94 /// Bind a listener at `addr`. Returns the bound server alongside
95 /// its actual local address (useful when binding to port 0).
96 ///
97 /// # Examples
98 ///
99 /// ```no_run
100 /// use std::sync::Arc;
101 /// use dynomite::stats::{Snapshot, StatsServer};
102 /// use parking_lot::Mutex;
103 ///
104 /// # async fn _example() -> std::io::Result<()> {
105 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
106 /// let _server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink).await?;
107 /// # Ok(())
108 /// # }
109 /// ```
110 pub async fn bind(addr: SocketAddr, source: Arc<Mutex<Snapshot>>) -> io::Result<Self> {
111 let listener = TcpListener::bind(addr).await?;
112 Ok(Self {
113 listener,
114 source,
115 cluster_info: None,
116 ring: None,
117 })
118 }
119
120 /// Attach a [`ClusterInfoProvider`] so the server answers
121 /// `GET /cluster-info.txt` with a freshly assembled
122 /// snapshot. When no provider is registered the route
123 /// returns `503 Service Unavailable`.
124 ///
125 /// # Examples
126 ///
127 /// ```no_run
128 /// use std::sync::Arc;
129 /// use dynomite::admin::cluster_info::ClusterInfoSnapshot;
130 /// use dynomite::stats::{Snapshot, StatsServer};
131 /// use parking_lot::Mutex;
132 ///
133 /// # async fn _example() -> std::io::Result<()> {
134 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
135 /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)
136 /// .await?
137 /// .with_cluster_info_provider(Arc::new(ClusterInfoSnapshot::synthetic));
138 /// drop(server);
139 /// # Ok(())
140 /// # }
141 /// ```
142 #[must_use]
143 pub fn with_cluster_info_provider(mut self, provider: ClusterInfoProvider) -> Self {
144 self.cluster_info = Some(provider);
145 self
146 }
147
148 /// Attach a [`RingProvider`] so the server answers
149 /// `GET /ring` with a freshly assembled, JSON-serialized
150 /// view of every peer's tokens, dc, rack, and state. When no
151 /// provider is registered the route returns `503 Service
152 /// Unavailable`.
153 ///
154 /// # Examples
155 ///
156 /// ```no_run
157 /// use std::sync::Arc;
158 /// use dynomite::admin::cluster_info::RingSnapshot;
159 /// use dynomite::stats::{Snapshot, StatsServer};
160 /// use parking_lot::Mutex;
161 ///
162 /// # async fn _example() -> std::io::Result<()> {
163 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
164 /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)
165 /// .await?
166 /// .with_ring_provider(Arc::new(RingSnapshot::default));
167 /// drop(server);
168 /// # Ok(())
169 /// # }
170 /// ```
171 #[must_use]
172 pub fn with_ring_provider(mut self, provider: RingProvider) -> Self {
173 self.ring = Some(provider);
174 self
175 }
176
177 /// Returns the local socket address the server is listening on.
178 ///
179 /// # Examples
180 ///
181 /// ```no_run
182 /// use std::sync::Arc;
183 /// use dynomite::stats::{Snapshot, StatsServer};
184 /// use parking_lot::Mutex;
185 ///
186 /// # async fn _example() -> std::io::Result<()> {
187 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
188 /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink).await?;
189 /// let addr = server.local_addr()?;
190 /// assert!(addr.port() != 0);
191 /// # Ok(())
192 /// # }
193 /// ```
194 pub fn local_addr(&self) -> io::Result<SocketAddr> {
195 self.listener.local_addr()
196 }
197
198 /// Accept a single connection, serve one HTTP/1.1 request, and
199 /// return.
200 ///
201 /// # Examples
202 ///
203 /// ```no_run
204 /// use std::sync::Arc;
205 /// use dynomite::stats::{Snapshot, StatsServer};
206 /// use parking_lot::Mutex;
207 ///
208 /// # async fn _example() -> std::io::Result<()> {
209 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
210 /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink).await?;
211 /// server.accept_one().await?;
212 /// # Ok(())
213 /// # }
214 /// ```
215 pub async fn accept_one(&self) -> io::Result<()> {
216 let (sock, _peer) = self.listener.accept().await?;
217 let snapshot = self.source.lock().clone();
218 let cluster_info = self.cluster_info.clone();
219 let ring = self.ring.clone();
220 serve_connection(sock, snapshot, cluster_info, ring).await
221 }
222
223 /// Run the accept loop until cancelled. Each connection is handled
224 /// on a fresh task so a slow client cannot stall the listener.
225 ///
226 /// # Examples
227 ///
228 /// ```no_run
229 /// use std::sync::Arc;
230 /// use dynomite::stats::{Snapshot, StatsServer};
231 /// use parking_lot::Mutex;
232 ///
233 /// # async fn _example() -> std::io::Result<()> {
234 /// let sink = Arc::new(Mutex::new(Snapshot::default()));
235 /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink).await?;
236 /// let _ = tokio::spawn(async move { server.run().await });
237 /// # Ok(())
238 /// # }
239 /// ```
240 pub async fn run(self) -> io::Result<()> {
241 let span = tracing::info_span!(
242 "stats_server.run",
243 local = %self.listener.local_addr().map_or_else(|_| String::from("?"), |a| a.to_string()),
244 );
245 let listener = self.listener;
246 let source = self.source;
247 let cluster_info = self.cluster_info;
248 let ring = self.ring;
249 async move {
250 loop {
251 let (sock, _peer) = listener.accept().await?;
252 let snapshot = source.lock().clone();
253 let ci = cluster_info.clone();
254 let rg = ring.clone();
255 tokio::spawn(async move {
256 let _ = serve_connection(sock, snapshot, ci, rg).await;
257 });
258 }
259 }
260 .instrument(span)
261 .await
262 }
263}
264
265async fn serve_connection(
266 mut sock: TcpStream,
267 snapshot: Snapshot,
268 cluster_info: Option<ClusterInfoProvider>,
269 ring: Option<RingProvider>,
270) -> io::Result<()> {
271 let mut buf = vec![0u8; MAX_REQUEST_BYTES];
272 let mut filled = 0usize;
273 loop {
274 if filled == buf.len() {
275 return write_response(&mut sock, 400, "Bad Request", b"").await;
276 }
277 let read_result = tokio::time::timeout(READ_TIMEOUT, sock.read(&mut buf[filled..])).await;
278 let Ok(Ok(n)) = read_result else {
279 // Read error or timeout: close silently, matching the
280 // reference error path which drops the connection without
281 // writing a response.
282 let _ = sock.shutdown().await;
283 return Ok(());
284 };
285 if n == 0 {
286 break;
287 }
288 filled += n;
289 let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
290 let mut req = httparse::Request::new(&mut headers);
291 match req.parse(&buf[..filled]) {
292 Ok(httparse::Status::Complete(_)) => {
293 return handle_parsed(&mut sock, &req, snapshot, cluster_info, ring).await;
294 }
295 Ok(httparse::Status::Partial) => continue,
296 Err(_) => {
297 return write_response(&mut sock, 400, "Bad Request", b"").await;
298 }
299 }
300 }
301 Ok(())
302}
303
304async fn handle_parsed(
305 sock: &mut TcpStream,
306 req: &httparse::Request<'_, '_>,
307 snapshot: Snapshot,
308 cluster_info: Option<ClusterInfoProvider>,
309 ring: Option<RingProvider>,
310) -> io::Result<()> {
311 let path = req.path.unwrap_or("/");
312 if !matches!(req.method, Some("GET")) {
313 return write_response(sock, 405, "Method Not Allowed", b"").await;
314 }
315 match path {
316 "/" | "/info" | "/stats" => {
317 let body = snapshot.to_json();
318 write_json_response(sock, body.as_bytes()).await
319 }
320 "/metrics" => {
321 let body = render_prometheus(&snapshot);
322 write_metrics_response(sock, body.as_bytes()).await
323 }
324 "/cluster-info.txt" => match cluster_info {
325 Some(provider) => {
326 let snap = provider();
327 let mut body: Vec<u8> = Vec::with_capacity(4096);
328 if format_text(&snap, &mut body).is_err() {
329 return write_response(sock, 500, "Internal Server Error", b"").await;
330 }
331 write_text_response(sock, &body).await
332 }
333 None => write_response(sock, 503, "Service Unavailable", b"").await,
334 },
335 "/ring" | "/ring.json" => match ring {
336 Some(provider) => match serde_json::to_vec(&provider()) {
337 Ok(body) => write_json_response(sock, &body).await,
338 Err(_) => write_response(sock, 500, "Internal Server Error", b"").await,
339 },
340 None => write_response(sock, 503, "Service Unavailable", b"").await,
341 },
342 _ => write_response(sock, 200, "OK", b"OK\r\n").await,
343 }
344}
345
346async fn write_text_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
347 let header = format!(
348 "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=us-ascii\r\n\
349 Content-Length: {}\r\nConnection: close\r\n\r\n",
350 body.len()
351 );
352 sock.write_all(header.as_bytes()).await?;
353 sock.write_all(body).await?;
354 sock.shutdown().await?;
355 Ok(())
356}
357
358async fn write_response(
359 sock: &mut TcpStream,
360 code: u16,
361 reason: &str,
362 body: &[u8],
363) -> io::Result<()> {
364 let header = format!(
365 "HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
366 body.len()
367 );
368 sock.write_all(header.as_bytes()).await?;
369 if !body.is_empty() {
370 sock.write_all(body).await?;
371 }
372 sock.shutdown().await?;
373 Ok(())
374}
375
376async fn write_json_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
377 let header = format!(
378 "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\n\
379 Content-Length: {}\r\nConnection: close\r\n\r\n",
380 body.len()
381 );
382 sock.write_all(header.as_bytes()).await?;
383 sock.write_all(body).await?;
384 sock.shutdown().await?;
385 Ok(())
386}
387
388async fn write_metrics_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
389 let header = format!(
390 "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\n\
391 Content-Length: {}\r\nConnection: close\r\n\r\n",
392 body.len()
393 );
394 sock.write_all(header.as_bytes()).await?;
395 sock.write_all(body).await?;
396 sock.shutdown().await?;
397 Ok(())
398}