Skip to content

Commit

Permalink
Merge torrust#641: Tracker checker: call health check endpoints
Browse files Browse the repository at this point in the history
b2ef4e0 feat: tracker checker command (Jose Celano)

Pull request description:

  Console command that runs some checks against running trackers. This PR only implements:

  - Basic scaffolding for the new binary (console app).
  - Make a request to the health check endpoints.

  You can run it with:

  ```console
  cargo run --bin tracker_checker "./share/default/config/tracker_checker.json"
  ```

  The configuration file contains the services you wan to check:

  ```json
  {
      "udp_trackers": [
          "127.0.0.1:6969"
      ],
      "http_trackers": [
          "http://127.0.0.1:7070"
      ],
      "health_checks": [
          "http://127.0.0.1:1313/health_check"
      ]
  }
  ```

  For the `health_checks` it only makes a request and shows OK if the response status was 200, otherwise, it shows the error.

ACKs for top commit:
  josecelano:
    ACK b2ef4e0

Tree-SHA512: 7f817b10a3edb114ae745fc84c6ca46235851ef1f50e5b4a964bf6c3b29d43478b679101bbbf0ea68986edc4a55752977e372d8b077a7ce1f71d70fece6cd462
  • Loading branch information
josecelano committed Jan 22, 2024
2 parents 7ea6fb0 + b2ef4e0 commit 444c395
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "pa
torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" }
tower-http = { version = "0", features = ["compression-full"] }
uuid = { version = "1", features = ["v4"] }
colored = "2.1.0"
url = "2.5.0"

[dev-dependencies]
criterion = { version = "0.5.1", features = ["async_tokio"] }
Expand Down
11 changes: 11 additions & 0 deletions share/default/config/tracker_checker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"udp_trackers": [
"127.0.0.1:6969"
],
"http_trackers": [
"http://127.0.0.1:7070"
],
"health_checks": [
"http://127.0.0.1:1313/health_check"
]
}
11 changes: 11 additions & 0 deletions src/bin/tracker_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Program to run checks against running trackers.
//!
//! ```text
//! cargo run --bin tracker_checker "./share/default/config/tracker_checker.json"
//! ```
use torrust_tracker::checker::app;

#[tokio::main]
async fn main() {
app::run().await;
}
53 changes: 53 additions & 0 deletions src/checker/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::sync::Arc;

use super::config::Configuration;
use super::console::Console;
use crate::checker::config::parse_from_json;
use crate::checker::service::Service;

pub const NUMBER_OF_ARGUMENTS: usize = 2;

/// # Panics
///
/// Will panic if:
///
/// - It can't read the json configuration file.
/// - The configuration file is invalid.
pub async fn run() {
let args = parse_arguments();
let config = setup_config(&args);
let console_printer = Console {};
let service = Service {
config: Arc::new(config),
console: console_printer,
};

service.run_checks().await;
}

pub struct Arguments {
pub config_path: String,
}

fn parse_arguments() -> Arguments {
let args: Vec<String> = std::env::args().collect();

if args.len() < NUMBER_OF_ARGUMENTS {
eprintln!("Usage: cargo run --bin tracker_checker <PATH_TO_CONFIG_FILE>");
eprintln!("For example: cargo run --bin tracker_checker ./share/default/config/tracker_checker.json");
std::process::exit(1);
}

let config_path = &args[1];

Arguments {
config_path: config_path.to_string(),
}
}

fn setup_config(args: &Arguments) -> Configuration {
let file_content = std::fs::read_to_string(args.config_path.clone())
.unwrap_or_else(|_| panic!("Can't read config file {}", args.config_path));

parse_from_json(&file_content).expect("Invalid config format")
}
152 changes: 152 additions & 0 deletions src/checker/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::fmt;
use std::net::SocketAddr;

use reqwest::Url as ServiceUrl;
use serde::Deserialize;
use url;

/// It parses the configuration from a JSON format.
///
/// # Errors
///
/// Will return an error if the configuration is not valid.
///
/// # Panics
///
/// Will panic if unable to read the configuration file.
pub fn parse_from_json(json: &str) -> Result<Configuration, ConfigurationError> {
let plain_config: PlainConfiguration = serde_json::from_str(json).map_err(ConfigurationError::JsonParseError)?;
Configuration::try_from(plain_config)
}

/// DTO for the configuration to serialize/deserialize configuration.
///
/// Configuration does not need to be valid.
#[derive(Deserialize)]
struct PlainConfiguration {
pub udp_trackers: Vec<String>,
pub http_trackers: Vec<String>,
pub health_checks: Vec<String>,
}

/// Validated configuration
pub struct Configuration {
pub udp_trackers: Vec<SocketAddr>,
pub http_trackers: Vec<ServiceUrl>,
pub health_checks: Vec<ServiceUrl>,
}

#[derive(Debug)]
pub enum ConfigurationError {
JsonParseError(serde_json::Error),
InvalidUdpAddress(std::net::AddrParseError),
InvalidUrl(url::ParseError),
}

impl fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigurationError::JsonParseError(e) => write!(f, "JSON parse error: {e}"),
ConfigurationError::InvalidUdpAddress(e) => write!(f, "Invalid UDP address: {e}"),
ConfigurationError::InvalidUrl(e) => write!(f, "Invalid URL: {e}"),
}
}
}

impl TryFrom<PlainConfiguration> for Configuration {
type Error = ConfigurationError;

fn try_from(plain_config: PlainConfiguration) -> Result<Self, Self::Error> {
let udp_trackers = plain_config
.udp_trackers
.into_iter()
.map(|s| s.parse::<SocketAddr>().map_err(ConfigurationError::InvalidUdpAddress))
.collect::<Result<Vec<_>, _>>()?;

let http_trackers = plain_config
.http_trackers
.into_iter()
.map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
.collect::<Result<Vec<_>, _>>()?;

let health_checks = plain_config
.health_checks
.into_iter()
.map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
.collect::<Result<Vec<_>, _>>()?;

Ok(Configuration {
udp_trackers,
http_trackers,
health_checks,
})
}
}

#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use super::*;

#[test]
fn configuration_should_be_build_from_plain_serializable_configuration() {
let dto = PlainConfiguration {
udp_trackers: vec!["127.0.0.1:8080".to_string()],
http_trackers: vec!["http://127.0.0.1:8080".to_string()],
health_checks: vec!["http://127.0.0.1:8080/health".to_string()],
};

let config = Configuration::try_from(dto).expect("A valid configuration");

assert_eq!(
config.udp_trackers,
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)]
);
assert_eq!(
config.http_trackers,
vec![ServiceUrl::parse("http://127.0.0.1:8080").unwrap()]
);
assert_eq!(
config.health_checks,
vec![ServiceUrl::parse("http://127.0.0.1:8080/health").unwrap()]
);
}

mod building_configuration_from_plan_configuration {
use crate::checker::config::{Configuration, PlainConfiguration};

#[test]
fn it_should_fail_when_a_tracker_udp_address_is_invalid() {
let plain_config = PlainConfiguration {
udp_trackers: vec!["invalid_address".to_string()],
http_trackers: vec![],
health_checks: vec![],
};

assert!(Configuration::try_from(plain_config).is_err());
}

#[test]
fn it_should_fail_when_a_tracker_http_address_is_invalid() {
let plain_config = PlainConfiguration {
udp_trackers: vec![],
http_trackers: vec!["not_a_url".to_string()],
health_checks: vec![],
};

assert!(Configuration::try_from(plain_config).is_err());
}

#[test]
fn it_should_fail_when_a_health_check_http_address_is_invalid() {
let plain_config = PlainConfiguration {
udp_trackers: vec![],
http_trackers: vec![],
health_checks: vec!["not_a_url".to_string()],
};

assert!(Configuration::try_from(plain_config).is_err());
}
}
}
38 changes: 38 additions & 0 deletions src/checker/console.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use super::printer::{Printer, CLEAR_SCREEN};

pub struct Console {}

impl Default for Console {
fn default() -> Self {
Self::new()
}
}

impl Console {
#[must_use]
pub fn new() -> Self {
Self {}
}
}

impl Printer for Console {
fn clear(&self) {
self.print(CLEAR_SCREEN);
}

fn print(&self, output: &str) {
print!("{}", &output);
}

fn eprint(&self, output: &str) {
eprint!("{}", &output);
}

fn println(&self, output: &str) {
println!("{}", &output);
}

fn eprintln(&self, output: &str) {
eprintln!("{}", &output);
}
}
72 changes: 72 additions & 0 deletions src/checker/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::cell::RefCell;

use super::printer::{Printer, CLEAR_SCREEN};

pub struct Logger {
output: RefCell<String>,
}

impl Default for Logger {
fn default() -> Self {
Self::new()
}
}

impl Logger {
#[must_use]
pub fn new() -> Self {
Self {
output: RefCell::new(String::new()),
}
}

pub fn log(&self) -> String {
self.output.borrow().clone()
}
}

impl Printer for Logger {
fn clear(&self) {
self.print(CLEAR_SCREEN);
}

fn print(&self, output: &str) {
*self.output.borrow_mut() = format!("{}{}", self.output.borrow(), &output);
}

fn eprint(&self, output: &str) {
*self.output.borrow_mut() = format!("{}{}", self.output.borrow(), &output);
}

fn println(&self, output: &str) {
self.print(&format!("{}/n", &output));
}

fn eprintln(&self, output: &str) {
self.eprint(&format!("{}/n", &output));
}
}

#[cfg(test)]
mod tests {
use crate::checker::logger::Logger;
use crate::checker::printer::{Printer, CLEAR_SCREEN};

#[test]
fn should_capture_the_clear_screen_command() {
let console_logger = Logger::new();

console_logger.clear();

assert_eq!(CLEAR_SCREEN, console_logger.log());
}

#[test]
fn should_capture_the_print_command_output() {
let console_logger = Logger::new();

console_logger.print("OUTPUT");

assert_eq!("OUTPUT", console_logger.log());
}
}
6 changes: 6 additions & 0 deletions src/checker/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod app;
pub mod config;
pub mod console;
pub mod logger;
pub mod printer;
pub mod service;
Loading

0 comments on commit 444c395

Please sign in to comment.