Skip to content

Commit

Permalink
feat: [torrust#508] add health check enpoint to HTTP tracker
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
josecelano committed Nov 24, 2023
1 parent ef296f7 commit 80044c2
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 5 deletions.
46 changes: 42 additions & 4 deletions src/servers/health_check_api/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::net::SocketAddr;
use std::sync::Arc;

use axum::extract::State;
Expand All @@ -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<Arc<Configuration>>) -> Json<Report> {
// 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

Expand Down
18 changes: 18 additions & 0 deletions src/servers/http/v1/handlers/health_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use axum::Json;
use serde::{Deserialize, Serialize};

#[allow(clippy::unused_async)]
pub async fn handler() -> Json<Report> {
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,
}
1 change: 1 addition & 0 deletions src/servers/http/v1/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::tracker::error::Error;

pub mod announce;
pub mod common;
pub mod health_check;
pub mod scrape;

impl From<Error> for responses::error::Error {
Expand Down
4 changes: 3 additions & 1 deletion src/servers/http/v1/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -16,6 +16,8 @@ use crate::tracker::Tracker;
#[allow(clippy::needless_pass_by_value)]
pub fn router(tracker: Arc<Tracker>) -> 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()))
Expand Down
4 changes: 4 additions & 0 deletions tests/servers/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
20 changes: 20 additions & 0 deletions tests/servers/http/v1/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<V1>(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::<Report>().await.unwrap(), Report { status: Status::Ok });

test_env.stop().await;
}

mod and_running_on_reverse_proxy {
use torrust_tracker_test_helpers::configuration;

Expand Down

0 comments on commit 80044c2

Please sign in to comment.