From ef296f76bf3160e219f4b842fe1d055d4c9dc308 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 24 Nov 2023 16:13:04 +0000 Subject: [PATCH] feat: [#508] app health check endpoint checks API The app health check endpoint checks is the API is running healthy when is enabled. --- packages/test-helpers/src/configuration.rs | 12 +++++++ src/app.rs | 2 +- src/bootstrap/jobs/health_check_api.rs | 8 +++-- src/servers/health_check_api/handlers.rs | 31 +++++++++++++++++++ src/servers/health_check_api/mod.rs | 3 ++ src/servers/health_check_api/resources.rs | 31 +++++++++++++++++++ src/servers/health_check_api/responses.rs | 11 +++++++ src/servers/health_check_api/server.rs | 25 +++++++-------- tests/servers/health_check_api/contract.rs | 10 +++--- .../health_check_api/test_environment.rs | 8 +++-- 10 files changed, 115 insertions(+), 26 deletions(-) create mode 100644 src/servers/health_check_api/handlers.rs create mode 100644 src/servers/health_check_api/resources.rs create mode 100644 src/servers/health_check_api/responses.rs diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index b41f435e..388d0151 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -144,3 +144,15 @@ pub fn ephemeral_ipv6() -> Configuration { cfg } + +/// Ephemeral without running any services. +#[must_use] +pub fn ephemeral_with_no_services() -> Configuration { + let mut cfg = ephemeral(); + + cfg.http_api.enabled = false; + cfg.http_trackers[0].enabled = false; + cfg.udp_trackers[0].enabled = false; + + cfg +} diff --git a/src/app.rs b/src/app.rs index 6478cffb..e749f9c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -91,7 +91,7 @@ pub async fn start(config: Arc, tracker: Arc) - } // Start Health Check API - jobs.push(health_check_api::start_job(&config.health_check_api).await); + jobs.push(health_check_api::start_job(config).await); jobs } diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index 29c4ce14..96a703af 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -16,11 +16,12 @@ //! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) //! for the API configuration options. use std::net::SocketAddr; +use std::sync::Arc; use log::info; use tokio::sync::oneshot; use tokio::task::JoinHandle; -use torrust_tracker_configuration::HealthCheckApi; +use torrust_tracker_configuration::Configuration; use crate::servers::health_check_api::server; @@ -45,8 +46,9 @@ pub struct ApiServerJobStarted { /// # Panics /// /// It would panic if unable to send the `ApiServerJobStarted` notice. -pub async fn start_job(config: &HealthCheckApi) -> JoinHandle<()> { +pub async fn start_job(config: Arc) -> JoinHandle<()> { let bind_addr = config + .health_check_api .bind_address .parse::() .expect("Health Check API bind_address invalid."); @@ -57,7 +59,7 @@ pub async fn start_job(config: &HealthCheckApi) -> JoinHandle<()> { let join_handle = tokio::spawn(async move { info!("Starting Health Check API server: http://{}", bind_addr); - let handle = server::start(bind_addr, tx); + let handle = server::start(bind_addr, tx, config.clone()); if let Ok(()) = handle.await { info!("Health Check API server on http://{} stopped", bind_addr); diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs new file mode 100644 index 00000000..347106d6 --- /dev/null +++ b/src/servers/health_check_api/handlers.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::Json; +use torrust_tracker_configuration::Configuration; + +use super::resources::Report; +use super::responses; + +/// Endpoint for container health check. +pub(crate) async fn health_check_handler(State(config): State>) -> Json { + if config.http_api.enabled { + let health_check_url = format!("http://{}/health_check", config.http_api.bind_address); + if !get_req_is_ok(&health_check_url).await { + return responses::error(format!("API is not healthy. Health check endpoint: {health_check_url}")); + } + } + + // todo: for all HTTP Trackers, if enabled, check if is healthy + + // todo: for all UDP Trackers, if enabled, check if is healthy + + responses::ok() +} + +async fn get_req_is_ok(url: &str) -> bool { + match reqwest::get(url).await { + Ok(response) => response.status().is_success(), + Err(_err) => false, + } +} diff --git a/src/servers/health_check_api/mod.rs b/src/servers/health_check_api/mod.rs index 74f47ad3..ec608387 100644 --- a/src/servers/health_check_api/mod.rs +++ b/src/servers/health_check_api/mod.rs @@ -1 +1,4 @@ +pub mod handlers; +pub mod resources; +pub mod responses; pub mod server; diff --git a/src/servers/health_check_api/resources.rs b/src/servers/health_check_api/resources.rs new file mode 100644 index 00000000..3fadcf45 --- /dev/null +++ b/src/servers/health_check_api/resources.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum Status { + Ok, + Error, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Report { + pub status: Status, + pub message: String, +} + +impl Report { + #[must_use] + pub fn ok() -> Report { + Self { + status: Status::Ok, + message: String::new(), + } + } + + #[must_use] + pub fn error(message: String) -> Report { + Self { + status: Status::Error, + message, + } + } +} diff --git a/src/servers/health_check_api/responses.rs b/src/servers/health_check_api/responses.rs new file mode 100644 index 00000000..043e271d --- /dev/null +++ b/src/servers/health_check_api/responses.rs @@ -0,0 +1,11 @@ +use axum::Json; + +use super::resources::Report; + +pub fn ok() -> Json { + Json(Report::ok()) +} + +pub fn error(message: String) -> Json { + Json(Report::error(message)) +} diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs index cbd1b870..562772a8 100644 --- a/src/servers/health_check_api/server.rs +++ b/src/servers/health_check_api/server.rs @@ -3,25 +3,33 @@ //! This API is intended to be used by the container infrastructure to check if //! the whole application is healthy. use std::net::SocketAddr; +use std::sync::Arc; use axum::routing::get; use axum::{Json, Router}; use futures::Future; use log::info; -use serde_json::{json, Value}; +use serde_json::json; use tokio::sync::oneshot::Sender; +use torrust_tracker_configuration::Configuration; use crate::bootstrap::jobs::health_check_api::ApiServerJobStarted; +use crate::servers::health_check_api::handlers::health_check_handler; /// Starts Health Check API server. /// /// # Panics /// /// Will panic if binding to the socket address fails. -pub fn start(socket_addr: SocketAddr, tx: Sender) -> impl Future> { +pub fn start( + socket_addr: SocketAddr, + tx: Sender, + config: Arc, +) -> impl Future> { let app = Router::new() .route("/", get(|| async { Json(json!({})) })) - .route("/health_check", get(health_check_handler)); + .route("/health_check", get(health_check_handler)) + .with_state(config); let server = axum::Server::bind(&socket_addr).serve(app.into_make_service()); @@ -39,14 +47,3 @@ pub fn start(socket_addr: SocketAddr, tx: Sender) -> impl F running } - -/// Endpoint for container health check. -async fn health_check_handler() -> Json { - // todo: if enabled, check if the Tracker API is healthy - - // todo: if enabled, check if the HTTP Tracker is healthy - - // todo: if enabled, check if the UDP Tracker is healthy - - Json(json!({ "status": "Ok" })) -} diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs index 575e1066..6b816b85 100644 --- a/tests/servers/health_check_api/contract.rs +++ b/tests/servers/health_check_api/contract.rs @@ -1,14 +1,14 @@ -use torrust_tracker::servers::apis::v1::context::health_check::resources::{Report, Status}; +use torrust_tracker::servers::health_check_api::resources::Report; use torrust_tracker_test_helpers::configuration; use crate::servers::health_check_api::client::get; use crate::servers::health_check_api::test_environment; #[tokio::test] -async fn health_check_endpoint_should_return_status_ok() { - let configuration = configuration::ephemeral(); +async fn health_check_endpoint_should_return_status_ok_when_no_service_is_running() { + let configuration = configuration::ephemeral_with_no_services(); - let (bound_addr, test_env) = test_environment::start(&configuration.health_check_api).await; + let (bound_addr, test_env) = test_environment::start(configuration.into()).await; let url = format!("http://{bound_addr}/health_check"); @@ -16,7 +16,7 @@ async fn health_check_endpoint_should_return_status_ok() { assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); - assert_eq!(response.json::().await.unwrap(), Report { status: Status::Ok }); + assert_eq!(response.json::().await.unwrap(), Report::ok()); test_env.abort(); } diff --git a/tests/servers/health_check_api/test_environment.rs b/tests/servers/health_check_api/test_environment.rs index 6ad90eac..46e54dc4 100644 --- a/tests/servers/health_check_api/test_environment.rs +++ b/tests/servers/health_check_api/test_environment.rs @@ -1,15 +1,17 @@ use std::net::SocketAddr; +use std::sync::Arc; use tokio::sync::oneshot; use tokio::task::JoinHandle; use torrust_tracker::bootstrap::jobs::health_check_api::ApiServerJobStarted; use torrust_tracker::servers::health_check_api::server; -use torrust_tracker_configuration::HealthCheckApi; +use torrust_tracker_configuration::Configuration; /// Start the test environment for the Health Check API. /// It runs the API server. -pub async fn start(config: &HealthCheckApi) -> (SocketAddr, JoinHandle<()>) { +pub async fn start(config: Arc) -> (SocketAddr, JoinHandle<()>) { let bind_addr = config + .health_check_api .bind_address .parse::() .expect("Health Check API bind_address invalid."); @@ -17,7 +19,7 @@ pub async fn start(config: &HealthCheckApi) -> (SocketAddr, JoinHandle<()>) { let (tx, rx) = oneshot::channel::(); let join_handle = tokio::spawn(async move { - let handle = server::start(bind_addr, tx); + let handle = server::start(bind_addr, tx, config.clone()); if let Ok(()) = handle.await { panic!("Health Check API server on http://{bind_addr} stopped"); }