Skip to main content

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}