From 742130657bc3c04b3497400207e16250433de966 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 24 Nov 2023 17:01:05 +0000 Subject: [PATCH] feat: [#508] add health check enpoint to HTTP tracker http://localhost:7070/health_check And call the endpoint from the general application health check endpoint: http://localhost:1313/health_check Do not call the endpoint if: - The tracker is disabled. - The tracker configuration uses port 0 only knwon after starting the socket. todo: call the enpoint also when the port is 0 in the configuration. THe service can return back to the main app the port assiged by the OS. And the app can pass that port to the global app health check handler. --- src/servers/health_check_api/handlers.rs | 46 ++++++++++++++++++-- src/servers/http/v1/handlers/health_check.rs | 18 ++++++++ src/servers/http/v1/handlers/mod.rs | 1 + src/servers/http/v1/routes.rs | 4 +- tests/servers/http/client.rs | 4 ++ tests/servers/http/v1/contract.rs | 20 +++++++++ 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/servers/http/v1/handlers/health_check.rs diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs index 347106d6..0a95c321 100644 --- a/src/servers/health_check_api/handlers.rs +++ b/src/servers/health_check_api/handlers.rs @@ -1,3 +1,4 @@ +use std::net::SocketAddr; use std::sync::Arc; use axum::extract::State; @@ -7,16 +8,53 @@ use torrust_tracker_configuration::Configuration; use super::resources::Report; use super::responses; +/// If port 0 is specified in the configuration the OS will automatically +/// assign a free port. But we do now know in from the configuration. +/// We can only know it after starting the socket. +const UNKNOWN_PORT: u16 = 0; + /// Endpoint for container health check. +/// +/// This endpoint only checks services when we know the port from the +/// configuration. If port 0 is specified in the configuration the health check +/// for that service is skipped. pub(crate) async fn health_check_handler(State(config): State>) -> Json { + // todo: when port 0 is specified in the configuration get the port from the + // running service, after starting it as we do for testing with ephemeral + // configurations. + 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}")); + let addr: SocketAddr = config.http_api.bind_address.parse().expect("invalid socket address for API"); + + if addr.port() != UNKNOWN_PORT { + let health_check_url = format!("http://{addr}/health_check"); + + 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 + for http_tracker_config in &config.http_trackers { + if !http_tracker_config.enabled { + continue; + } + + let addr: SocketAddr = http_tracker_config + .bind_address + .parse() + .expect("invalid socket address for HTTP tracker"); + + if addr.port() != UNKNOWN_PORT { + let health_check_url = format!("http://{addr}/health_check"); + + if !get_req_is_ok(&health_check_url).await { + return responses::error(format!( + "HTTP Tracker is not healthy. Health check endpoint: {health_check_url}" + )); + } + } + } // todo: for all UDP Trackers, if enabled, check if is healthy diff --git a/src/servers/http/v1/handlers/health_check.rs b/src/servers/http/v1/handlers/health_check.rs new file mode 100644 index 00000000..b15af625 --- /dev/null +++ b/src/servers/http/v1/handlers/health_check.rs @@ -0,0 +1,18 @@ +use axum::Json; +use serde::{Deserialize, Serialize}; + +#[allow(clippy::unused_async)] +pub async fn handler() -> Json { + Json(Report { status: Status::Ok }) +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum Status { + Ok, + Error, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Report { + pub status: Status, +} diff --git a/src/servers/http/v1/handlers/mod.rs b/src/servers/http/v1/handlers/mod.rs index d78dee7d..d7fd0583 100644 --- a/src/servers/http/v1/handlers/mod.rs +++ b/src/servers/http/v1/handlers/mod.rs @@ -7,6 +7,7 @@ use crate::tracker::error::Error; pub mod announce; pub mod common; +pub mod health_check; pub mod scrape; impl From for responses::error::Error { diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index 6546dcbb..0b6b419c 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -6,7 +6,7 @@ use axum::Router; use axum_client_ip::SecureClientIpSource; use tower_http::compression::CompressionLayer; -use super::handlers::{announce, scrape}; +use super::handlers::{announce, health_check, scrape}; use crate::tracker::Tracker; /// It adds the routes to the router. @@ -16,6 +16,8 @@ use crate::tracker::Tracker; #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc) -> Router { Router::new() + // Health check + .route("/health_check", get(health_check::handler)) // Announce request .route("/announce", get(announce::handle_without_key).with_state(tracker.clone())) .route("/announce/:key", get(announce::handle_with_key).with_state(tracker.clone())) diff --git a/tests/servers/http/client.rs b/tests/servers/http/client.rs index 0dbdd9cf..03ed9aee 100644 --- a/tests/servers/http/client.rs +++ b/tests/servers/http/client.rs @@ -60,6 +60,10 @@ impl Client { .await } + pub async fn health_check(&self) -> Response { + self.get(&self.build_path("health_check")).await + } + pub async fn get(&self, path: &str) -> Response { self.reqwest.get(self.build_url(path)).send().await.unwrap() } diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs index 2e24af6b..b1900945 100644 --- a/tests/servers/http/v1/contract.rs +++ b/tests/servers/http/v1/contract.rs @@ -13,6 +13,26 @@ async fn test_environment_should_be_started_and_stopped() { mod for_all_config_modes { + use torrust_tracker::servers::http::v1::handlers::health_check::{Report, Status}; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::http::client::Client; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn health_check_endpoint_should_return_ok_if_the_http_tracker_is_running() { + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + + let response = Client::new(*test_env.bind_address()).health_check().await; + + 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 }); + + test_env.stop().await; + } + mod and_running_on_reverse_proxy { use torrust_tracker_test_helpers::configuration;