Skip to content

Commit

Permalink
feat: [#508] app health check endpoint checks API
Browse files Browse the repository at this point in the history
The app health check endpoint checks is the API is running healthy when
is enabled.
  • Loading branch information
josecelano committed Nov 24, 2023
1 parent e1a45a2 commit ef296f7
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 26 deletions.
12 changes: 12 additions & 0 deletions packages/test-helpers/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub async fn start(config: Arc<Configuration>, tracker: Arc<tracker::Tracker>) -
}

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

Check warning on line 94 in src/app.rs

View check run for this annotation

Codecov / codecov/patch

src/app.rs#L94

Added line #L94 was not covered by tests

jobs
}
8 changes: 5 additions & 3 deletions src/bootstrap/jobs/health_check_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<Configuration>) -> JoinHandle<()> {
let bind_addr = config

Check warning on line 50 in src/bootstrap/jobs/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

src/bootstrap/jobs/health_check_api.rs#L49-L50

Added lines #L49 - L50 were not covered by tests
.health_check_api
.bind_address
.parse::<std::net::SocketAddr>()
.expect("Health Check API bind_address invalid.");

Check warning on line 54 in src/bootstrap/jobs/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

src/bootstrap/jobs/health_check_api.rs#L54

Added line #L54 was not covered by tests
Expand All @@ -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);

Check warning on line 60 in src/bootstrap/jobs/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

src/bootstrap/jobs/health_check_api.rs#L59-L60

Added lines #L59 - L60 were not covered by tests

let handle = server::start(bind_addr, tx);
let handle = server::start(bind_addr, tx, config.clone());

Check warning on line 62 in src/bootstrap/jobs/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

src/bootstrap/jobs/health_check_api.rs#L62

Added line #L62 was not covered by tests

if let Ok(()) = handle.await {
info!("Health Check API server on http://{} stopped", bind_addr);

Check warning on line 65 in src/bootstrap/jobs/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

src/bootstrap/jobs/health_check_api.rs#L64-L65

Added lines #L64 - L65 were not covered by tests
Expand Down
31 changes: 31 additions & 0 deletions src/servers/health_check_api/handlers.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Configuration>>) -> Json<Report> {
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}"));

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

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L13-L15

Added lines #L13 - L15 were not covered by tests
}
}

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

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L17

Added line #L17 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L26 - L29 were not covered by tests
}
}

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

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/handlers.rs#L31

Added line #L31 was not covered by tests
3 changes: 3 additions & 0 deletions src/servers/health_check_api/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pub mod handlers;
pub mod resources;
pub mod responses;
pub mod server;
31 changes: 31 additions & 0 deletions src/servers/health_check_api/resources.rs
Original file line number Diff line number Diff line change
@@ -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,

Check warning on line 12 in src/servers/health_check_api/resources.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/resources.rs#L11-L12

Added lines #L11 - L12 were not covered by tests
}

impl Report {
#[must_use]
pub fn ok() -> Report {
Self {
status: Status::Ok,
message: String::new(),
}
}

#[must_use]
pub fn error(message: String) -> Report {
Self {

Check warning on line 26 in src/servers/health_check_api/resources.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/resources.rs#L25-L26

Added lines #L25 - L26 were not covered by tests
status: Status::Error,
message,
}
}

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

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/resources.rs#L30

Added line #L30 was not covered by tests
}
11 changes: 11 additions & 0 deletions src/servers/health_check_api/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use axum::Json;

use super::resources::Report;

pub fn ok() -> Json<Report> {
Json(Report::ok())
}

pub fn error(message: String) -> Json<Report> {
Json(Report::error(message))
}

Check warning on line 11 in src/servers/health_check_api/responses.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/health_check_api/responses.rs#L9-L11

Added lines #L9 - L11 were not covered by tests
25 changes: 11 additions & 14 deletions src/servers/health_check_api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiServerJobStarted>) -> impl Future<Output = hyper::Result<()>> {
pub fn start(
socket_addr: SocketAddr,
tx: Sender<ApiServerJobStarted>,
config: Arc<Configuration>,
) -> impl Future<Output = hyper::Result<()>> {
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());

Expand All @@ -39,14 +47,3 @@ pub fn start(socket_addr: SocketAddr, tx: Sender<ApiServerJobStarted>) -> impl F

running
}

/// Endpoint for container health check.
async fn health_check_handler() -> Json<Value> {
// 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" }))
}
10 changes: 5 additions & 5 deletions tests/servers/health_check_api/contract.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
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");

let response = get(&url).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 });
assert_eq!(response.json::<Report>().await.unwrap(), Report::ok());

test_env.abort();
}
8 changes: 5 additions & 3 deletions tests/servers/health_check_api/test_environment.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
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<Configuration>) -> (SocketAddr, JoinHandle<()>) {
let bind_addr = config
.health_check_api
.bind_address
.parse::<std::net::SocketAddr>()
.expect("Health Check API bind_address invalid.");

let (tx, rx) = oneshot::channel::<ApiServerJobStarted>();

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");
}
Expand Down

0 comments on commit ef296f7

Please sign in to comment.