Skip to content

Commit 80044c2

Browse files
committed
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.
1 parent ef296f7 commit 80044c2

File tree

6 files changed

+88
-5
lines changed

6 files changed

+88
-5
lines changed

src/servers/health_check_api/handlers.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::net::SocketAddr;
12
use std::sync::Arc;
23

34
use axum::extract::State;
@@ -7,16 +8,53 @@ use torrust_tracker_configuration::Configuration;
78
use super::resources::Report;
89
use super::responses;
910

11+
/// If port 0 is specified in the configuration the OS will automatically
12+
/// assign a free port. But we do now know in from the configuration.
13+
/// We can only know it after starting the socket.
14+
const UNKNOWN_PORT: u16 = 0;
15+
1016
/// Endpoint for container health check.
17+
///
18+
/// This endpoint only checks services when we know the port from the
19+
/// configuration. If port 0 is specified in the configuration the health check
20+
/// for that service is skipped.
1121
pub(crate) async fn health_check_handler(State(config): State<Arc<Configuration>>) -> Json<Report> {
22+
// todo: when port 0 is specified in the configuration get the port from the
23+
// running service, after starting it as we do for testing with ephemeral
24+
// configurations.
25+
1226
if config.http_api.enabled {
13-
let health_check_url = format!("http://{}/health_check", config.http_api.bind_address);
14-
if !get_req_is_ok(&health_check_url).await {
15-
return responses::error(format!("API is not healthy. Health check endpoint: {health_check_url}"));
27+
let addr: SocketAddr = config.http_api.bind_address.parse().expect("invalid socket address for API");
28+
29+
if addr.port() != UNKNOWN_PORT {
30+
let health_check_url = format!("http://{addr}/health_check");
31+
32+
if !get_req_is_ok(&health_check_url).await {
33+
return responses::error(format!("API is not healthy. Health check endpoint: {health_check_url}"));
34+
}
1635
}
1736
}
1837

19-
// todo: for all HTTP Trackers, if enabled, check if is healthy
38+
for http_tracker_config in &config.http_trackers {
39+
if !http_tracker_config.enabled {
40+
continue;
41+
}
42+
43+
let addr: SocketAddr = http_tracker_config
44+
.bind_address
45+
.parse()
46+
.expect("invalid socket address for HTTP tracker");
47+
48+
if addr.port() != UNKNOWN_PORT {
49+
let health_check_url = format!("http://{addr}/health_check");
50+
51+
if !get_req_is_ok(&health_check_url).await {
52+
return responses::error(format!(
53+
"HTTP Tracker is not healthy. Health check endpoint: {health_check_url}"
54+
));
55+
}
56+
}
57+
}
2058

2159
// todo: for all UDP Trackers, if enabled, check if is healthy
2260

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use axum::Json;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[allow(clippy::unused_async)]
5+
pub async fn handler() -> Json<Report> {
6+
Json(Report { status: Status::Ok })
7+
}
8+
9+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
10+
pub enum Status {
11+
Ok,
12+
Error,
13+
}
14+
15+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
16+
pub struct Report {
17+
pub status: Status,
18+
}

src/servers/http/v1/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::tracker::error::Error;
77

88
pub mod announce;
99
pub mod common;
10+
pub mod health_check;
1011
pub mod scrape;
1112

1213
impl From<Error> for responses::error::Error {

src/servers/http/v1/routes.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use axum::Router;
66
use axum_client_ip::SecureClientIpSource;
77
use tower_http::compression::CompressionLayer;
88

9-
use super::handlers::{announce, scrape};
9+
use super::handlers::{announce, health_check, scrape};
1010
use crate::tracker::Tracker;
1111

1212
/// It adds the routes to the router.
@@ -16,6 +16,8 @@ use crate::tracker::Tracker;
1616
#[allow(clippy::needless_pass_by_value)]
1717
pub fn router(tracker: Arc<Tracker>) -> Router {
1818
Router::new()
19+
// Health check
20+
.route("/health_check", get(health_check::handler))
1921
// Announce request
2022
.route("/announce", get(announce::handle_without_key).with_state(tracker.clone()))
2123
.route("/announce/:key", get(announce::handle_with_key).with_state(tracker.clone()))

tests/servers/http/client.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ impl Client {
6060
.await
6161
}
6262

63+
pub async fn health_check(&self) -> Response {
64+
self.get(&self.build_path("health_check")).await
65+
}
66+
6367
pub async fn get(&self, path: &str) -> Response {
6468
self.reqwest.get(self.build_url(path)).send().await.unwrap()
6569
}

tests/servers/http/v1/contract.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@ async fn test_environment_should_be_started_and_stopped() {
1313

1414
mod for_all_config_modes {
1515

16+
use torrust_tracker::servers::http::v1::handlers::health_check::{Report, Status};
17+
use torrust_tracker_test_helpers::configuration;
18+
19+
use crate::servers::http::client::Client;
20+
use crate::servers::http::test_environment::running_test_environment;
21+
use crate::servers::http::v1::contract::V1;
22+
23+
#[tokio::test]
24+
async fn health_check_endpoint_should_return_ok_if_the_http_tracker_is_running() {
25+
let test_env = running_test_environment::<V1>(configuration::ephemeral_with_reverse_proxy()).await;
26+
27+
let response = Client::new(*test_env.bind_address()).health_check().await;
28+
29+
assert_eq!(response.status(), 200);
30+
assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
31+
assert_eq!(response.json::<Report>().await.unwrap(), Report { status: Status::Ok });
32+
33+
test_env.stop().await;
34+
}
35+
1636
mod and_running_on_reverse_proxy {
1737
use torrust_tracker_test_helpers::configuration;
1838

0 commit comments

Comments
 (0)