use std::time::Duration;
use rustlavel_client::Client;
use rustlavel_core::Json;
use rustlavel_http::{Response, Status};
use crate::route::Routes;
pub const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
pub async fn check(client: &Client, routes: &Routes, path: &str) -> Response {
let mut services = Vec::new();
let mut all_well = true;
for (pattern, upstream) in routes.upstreams() {
let well = match upstream.resolved().await {
Some(resolved) => {
let url = format!("{}{path}", resolved.base());
matches!(
tokio::time::timeout(PROBE_TIMEOUT, client.get(&url).send()).await,
Ok(Ok(response)) if response.is_success()
)
}
None => false,
};
all_well &= well;
services.push(Json::object([
("route", Json::from(pattern)),
("upstream", Json::from(upstream.base())),
("status", Json::from(if well { "up" } else { "down" })),
]));
}
let status = if all_well { Status::OK } else { Status(503) };
Response::new(status).with_json(Json::object([
("status", Json::from(if all_well { "up" } else { "degraded" })),
("services", Json::Array(services)),
]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::route::Upstream;
#[tokio::test]
async fn an_empty_table_is_healthy() {
let response = check(&Client::new(), &Routes::new(), "/health").await;
assert_eq!(response.status.0, 200);
}
#[tokio::test]
async fn a_service_that_resolves_to_nothing_is_down() {
struct Nothing;
impl crate::route::Locate for Nothing {
fn base_for<'a>(
&'a self,
_service: &'a str,
) -> std::pin::Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
Box::pin(async { None })
}
}
let mut routes = Routes::new();
routes.add("/api/*", Upstream::service("orders", std::sync::Arc::new(Nothing)));
let response = check(&Client::new(), &routes, "/health").await;
assert_eq!(response.status.0, 503);
let body = String::from_utf8_lossy(&response.body).to_string();
assert!(body.contains("service://ORDERS"), "the body does not name the route: {body}");
assert!(body.contains("down"), "{body}");
}
#[tokio::test]
async fn an_unreachable_upstream_makes_the_gateway_degraded_and_says_which() {
let mut routes = Routes::new();
routes.add("/api/*", Upstream::at("http://127.0.0.1:1"));
let response = check(&Client::new().timeout(PROBE_TIMEOUT), &routes, "/health").await;
assert_eq!(response.status.0, 503);
let body = String::from_utf8_lossy(&response.body).to_string();
assert!(body.contains("degraded"), "{body}");
assert!(body.contains("127.0.0.1:1"), "the body does not say which upstream: {body}");
assert!(body.contains("down"), "{body}");
}
}