Skip to content

Commit

Permalink
feat: [#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 7421306
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");

Check warning on line 27 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L27

Added line #L27 was not covered by tests

if addr.port() != UNKNOWN_PORT {
let health_check_url = format!("http://{addr}/health_check");

Check warning on line 30 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L29-L30

Added lines #L29 - L30 were not covered by tests

if !get_req_is_ok(&health_check_url).await {
return responses::error(format!("API is not healthy. Health check endpoint: {health_check_url}"));

Check warning on line 33 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L32-L33

Added lines #L32 - L33 were not covered by tests
}
}

Check warning on line 35 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L35

Added line #L35 was not covered by tests
}

// 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

Check warning on line 43 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L43

Added line #L43 was not covered by tests
.bind_address
.parse()
.expect("invalid socket address for HTTP tracker");

Check warning on line 46 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L46

Added line #L46 was not covered by tests

if addr.port() != UNKNOWN_PORT {
let health_check_url = format!("http://{addr}/health_check");

Check warning on line 49 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L48-L49

Added lines #L48 - L49 were not covered by tests

if !get_req_is_ok(&health_check_url).await {
return responses::error(format!(

Check warning on line 52 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L51-L52

Added lines #L51 - L52 were not covered by tests
"HTTP Tracker is not healthy. Health check endpoint: {health_check_url}"
));
}
}

Check warning on line 56 in src/servers/health_check_api/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L56

Added line #L56 was not covered by tests
}

// 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,

Check warning on line 17 in src/servers/http/v1/handlers/health_check.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/http/v1/handlers/health_check.rs#L17

Added line #L17 was not covered by tests
}
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 7421306

Please sign in to comment.