1use std::{
5 net::SocketAddr,
6 sync::{
7 Arc, LazyLock,
8 atomic::{AtomicBool, Ordering},
9 },
10};
11
12use axum::{Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
13use prometheus::{Encoder, IntGaugeVec, TextEncoder, register_int_gauge_vec};
14use tokio_util::sync::CancellationToken;
15
16static BUILD_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
26 let gauge = register_int_gauge_vec!(
27 "polychrome_build_info",
28 "Always 1 for a live process; the `version` label carries the build's release version.",
29 &["version"]
30 )
31 .expect("register polychrome_build_info");
32 gauge.with_label_values(&[env!("CARGO_PKG_VERSION")]).set(1);
33 gauge
34});
35
36#[derive(Clone, Default)]
39pub struct Health {
40 ready: Arc<AtomicBool>,
41}
42
43impl Health {
44 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn set_ready(&self, ready: bool) {
52 self.ready.store(ready, Ordering::Relaxed);
53 }
54}
55
56pub async fn serve(
65 addr: SocketAddr,
66 health: Health,
67 shutdown: CancellationToken,
68) -> anyhow::Result<()> {
69 let listener = tokio::net::TcpListener::bind(addr).await?;
70 serve_on(listener, health, shutdown).await
71}
72
73pub async fn serve_on(
81 listener: tokio::net::TcpListener,
82 health: Health,
83 shutdown: CancellationToken,
84) -> anyhow::Result<()> {
85 let app = Router::new()
86 .route("/healthz", get(|| async { StatusCode::OK }))
87 .route("/livez", get(|| async { StatusCode::OK }))
88 .route("/readyz", get(readyz))
89 .route("/metrics", get(metrics))
90 .with_state(health);
91
92 let addr = listener.local_addr()?;
93 tracing::info!(%addr, "side-server listening");
94 axum::serve(listener, app)
95 .with_graceful_shutdown(async move { shutdown.cancelled().await })
96 .await?;
97 Ok(())
98}
99
100async fn readyz(State(health): State<Health>) -> impl IntoResponse {
102 if health.ready.load(Ordering::Relaxed) {
103 StatusCode::OK
104 } else {
105 StatusCode::SERVICE_UNAVAILABLE
106 }
107}
108
109async fn metrics() -> impl IntoResponse {
112 LazyLock::force(&BUILD_INFO);
113 let families = prometheus::gather();
114 let mut buf = Vec::new();
115 let encoder = TextEncoder::new();
116 if encoder.encode(&families, &mut buf).is_err() {
117 return (StatusCode::INTERNAL_SERVER_ERROR, Vec::new()).into_response();
118 }
119 (
120 [(axum::http::header::CONTENT_TYPE, encoder.format_type())],
121 buf,
122 )
123 .into_response()
124}
125
126#[cfg(test)]
127mod tests {
128 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
129
130 use tokio::io::{AsyncReadExt, AsyncWriteExt};
131 use tokio_util::sync::CancellationToken;
132
133 use super::{Health, serve_on};
134
135 async fn status_line(addr: std::net::SocketAddr, path: &str) -> String {
137 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
138 stream
139 .write_all(
140 format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
141 .as_bytes(),
142 )
143 .await
144 .unwrap();
145 let mut buf = String::new();
146 stream.read_to_string(&mut buf).await.unwrap();
147 buf.lines().next().unwrap_or_default().to_owned()
148 }
149
150 #[tokio::test]
151 async fn serve_on_takes_a_prebound_listener_and_readyz_tracks_the_flag() {
152 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
153 let addr = listener.local_addr().unwrap();
154 let health = Health::new();
155 let shutdown = CancellationToken::new();
156 let srv = tokio::spawn(serve_on(listener, health.clone(), shutdown.clone()));
157
158 assert!(status_line(addr, "/readyz").await.contains("503"));
161 assert!(status_line(addr, "/livez").await.contains("200"));
162 health.set_ready(true);
163 assert!(status_line(addr, "/readyz").await.contains("200"));
164 health.set_ready(false);
165 assert!(status_line(addr, "/readyz").await.contains("503"));
166
167 shutdown.cancel();
168 srv.await.unwrap().unwrap();
169 }
170
171 #[tokio::test]
174 async fn metrics_reports_build_info_with_version_label() {
175 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
176 let addr = listener.local_addr().unwrap();
177 let shutdown = CancellationToken::new();
178 let srv = tokio::spawn(serve_on(listener, Health::new(), shutdown.clone()));
179
180 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
181 stream
182 .write_all(b"GET /metrics HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
183 .await
184 .unwrap();
185 let mut buf = String::new();
186 stream.read_to_string(&mut buf).await.unwrap();
187
188 let expected = format!(
189 "polychrome_build_info{{version=\"{}\"}} 1",
190 env!("CARGO_PKG_VERSION")
191 );
192 assert!(
193 buf.contains(&expected),
194 "scrape missing labelled build_info:\n{buf}"
195 );
196
197 shutdown.cancel();
198 srv.await.unwrap().unwrap();
199 }
200}