Skip to content

Commit 444c395

Browse files
committed
Merge #641: Tracker checker: call health check endpoints
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
2 parents 7ea6fb0 + b2ef4e0 commit 444c395

12 files changed

+451
-0
lines changed

Cargo.lock

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "pa
6868
torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" }
6969
tower-http = { version = "0", features = ["compression-full"] }
7070
uuid = { version = "1", features = ["v4"] }
71+
colored = "2.1.0"
72+
url = "2.5.0"
7173

7274
[dev-dependencies]
7375
criterion = { version = "0.5.1", features = ["async_tokio"] }
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"udp_trackers": [
3+
"127.0.0.1:6969"
4+
],
5+
"http_trackers": [
6+
"http://127.0.0.1:7070"
7+
],
8+
"health_checks": [
9+
"http://127.0.0.1:1313/health_check"
10+
]
11+
}

src/bin/tracker_checker.rs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Program to run checks against running trackers.
2+
//!
3+
//! ```text
4+
//! cargo run --bin tracker_checker "./share/default/config/tracker_checker.json"
5+
//! ```
6+
use torrust_tracker::checker::app;
7+
8+
#[tokio::main]
9+
async fn main() {
10+
app::run().await;
11+
}

src/checker/app.rs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::sync::Arc;
2+
3+
use super::config::Configuration;
4+
use super::console::Console;
5+
use crate::checker::config::parse_from_json;
6+
use crate::checker::service::Service;
7+
8+
pub const NUMBER_OF_ARGUMENTS: usize = 2;
9+
10+
/// # Panics
11+
///
12+
/// Will panic if:
13+
///
14+
/// - It can't read the json configuration file.
15+
/// - The configuration file is invalid.
16+
pub async fn run() {
17+
let args = parse_arguments();
18+
let config = setup_config(&args);
19+
let console_printer = Console {};
20+
let service = Service {
21+
config: Arc::new(config),
22+
console: console_printer,
23+
};
24+
25+
service.run_checks().await;
26+
}
27+
28+
pub struct Arguments {
29+
pub config_path: String,
30+
}
31+
32+
fn parse_arguments() -> Arguments {
33+
let args: Vec<String> = std::env::args().collect();
34+
35+
if args.len() < NUMBER_OF_ARGUMENTS {
36+
eprintln!("Usage: cargo run --bin tracker_checker <PATH_TO_CONFIG_FILE>");
37+
eprintln!("For example: cargo run --bin tracker_checker ./share/default/config/tracker_checker.json");
38+
std::process::exit(1);
39+
}
40+
41+
let config_path = &args[1];
42+
43+
Arguments {
44+
config_path: config_path.to_string(),
45+
}
46+
}
47+
48+
fn setup_config(args: &Arguments) -> Configuration {
49+
let file_content = std::fs::read_to_string(args.config_path.clone())
50+
.unwrap_or_else(|_| panic!("Can't read config file {}", args.config_path));
51+
52+
parse_from_json(&file_content).expect("Invalid config format")
53+
}

src/checker/config.rs

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use std::fmt;
2+
use std::net::SocketAddr;
3+
4+
use reqwest::Url as ServiceUrl;
5+
use serde::Deserialize;
6+
use url;
7+
8+
/// It parses the configuration from a JSON format.
9+
///
10+
/// # Errors
11+
///
12+
/// Will return an error if the configuration is not valid.
13+
///
14+
/// # Panics
15+
///
16+
/// Will panic if unable to read the configuration file.
17+
pub fn parse_from_json(json: &str) -> Result<Configuration, ConfigurationError> {
18+
let plain_config: PlainConfiguration = serde_json::from_str(json).map_err(ConfigurationError::JsonParseError)?;
19+
Configuration::try_from(plain_config)
20+
}
21+
22+
/// DTO for the configuration to serialize/deserialize configuration.
23+
///
24+
/// Configuration does not need to be valid.
25+
#[derive(Deserialize)]
26+
struct PlainConfiguration {
27+
pub udp_trackers: Vec<String>,
28+
pub http_trackers: Vec<String>,
29+
pub health_checks: Vec<String>,
30+
}
31+
32+
/// Validated configuration
33+
pub struct Configuration {
34+
pub udp_trackers: Vec<SocketAddr>,
35+
pub http_trackers: Vec<ServiceUrl>,
36+
pub health_checks: Vec<ServiceUrl>,
37+
}
38+
39+
#[derive(Debug)]
40+
pub enum ConfigurationError {
41+
JsonParseError(serde_json::Error),
42+
InvalidUdpAddress(std::net::AddrParseError),
43+
InvalidUrl(url::ParseError),
44+
}
45+
46+
impl fmt::Display for ConfigurationError {
47+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48+
match self {
49+
ConfigurationError::JsonParseError(e) => write!(f, "JSON parse error: {e}"),
50+
ConfigurationError::InvalidUdpAddress(e) => write!(f, "Invalid UDP address: {e}"),
51+
ConfigurationError::InvalidUrl(e) => write!(f, "Invalid URL: {e}"),
52+
}
53+
}
54+
}
55+
56+
impl TryFrom<PlainConfiguration> for Configuration {
57+
type Error = ConfigurationError;
58+
59+
fn try_from(plain_config: PlainConfiguration) -> Result<Self, Self::Error> {
60+
let udp_trackers = plain_config
61+
.udp_trackers
62+
.into_iter()
63+
.map(|s| s.parse::<SocketAddr>().map_err(ConfigurationError::InvalidUdpAddress))
64+
.collect::<Result<Vec<_>, _>>()?;
65+
66+
let http_trackers = plain_config
67+
.http_trackers
68+
.into_iter()
69+
.map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
70+
.collect::<Result<Vec<_>, _>>()?;
71+
72+
let health_checks = plain_config
73+
.health_checks
74+
.into_iter()
75+
.map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
76+
.collect::<Result<Vec<_>, _>>()?;
77+
78+
Ok(Configuration {
79+
udp_trackers,
80+
http_trackers,
81+
health_checks,
82+
})
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
89+
90+
use super::*;
91+
92+
#[test]
93+
fn configuration_should_be_build_from_plain_serializable_configuration() {
94+
let dto = PlainConfiguration {
95+
udp_trackers: vec!["127.0.0.1:8080".to_string()],
96+
http_trackers: vec!["http://127.0.0.1:8080".to_string()],
97+
health_checks: vec!["http://127.0.0.1:8080/health".to_string()],
98+
};
99+
100+
let config = Configuration::try_from(dto).expect("A valid configuration");
101+
102+
assert_eq!(
103+
config.udp_trackers,
104+
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)]
105+
);
106+
assert_eq!(
107+
config.http_trackers,
108+
vec![ServiceUrl::parse("http://127.0.0.1:8080").unwrap()]
109+
);
110+
assert_eq!(
111+
config.health_checks,
112+
vec![ServiceUrl::parse("http://127.0.0.1:8080/health").unwrap()]
113+
);
114+
}
115+
116+
mod building_configuration_from_plan_configuration {
117+
use crate::checker::config::{Configuration, PlainConfiguration};
118+
119+
#[test]
120+
fn it_should_fail_when_a_tracker_udp_address_is_invalid() {
121+
let plain_config = PlainConfiguration {
122+
udp_trackers: vec!["invalid_address".to_string()],
123+
http_trackers: vec![],
124+
health_checks: vec![],
125+
};
126+
127+
assert!(Configuration::try_from(plain_config).is_err());
128+
}
129+
130+
#[test]
131+
fn it_should_fail_when_a_tracker_http_address_is_invalid() {
132+
let plain_config = PlainConfiguration {
133+
udp_trackers: vec![],
134+
http_trackers: vec!["not_a_url".to_string()],
135+
health_checks: vec![],
136+
};
137+
138+
assert!(Configuration::try_from(plain_config).is_err());
139+
}
140+
141+
#[test]
142+
fn it_should_fail_when_a_health_check_http_address_is_invalid() {
143+
let plain_config = PlainConfiguration {
144+
udp_trackers: vec![],
145+
http_trackers: vec![],
146+
health_checks: vec!["not_a_url".to_string()],
147+
};
148+
149+
assert!(Configuration::try_from(plain_config).is_err());
150+
}
151+
}
152+
}

src/checker/console.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use super::printer::{Printer, CLEAR_SCREEN};
2+
3+
pub struct Console {}
4+
5+
impl Default for Console {
6+
fn default() -> Self {
7+
Self::new()
8+
}
9+
}
10+
11+
impl Console {
12+
#[must_use]
13+
pub fn new() -> Self {
14+
Self {}
15+
}
16+
}
17+
18+
impl Printer for Console {
19+
fn clear(&self) {
20+
self.print(CLEAR_SCREEN);
21+
}
22+
23+
fn print(&self, output: &str) {
24+
print!("{}", &output);
25+
}
26+
27+
fn eprint(&self, output: &str) {
28+
eprint!("{}", &output);
29+
}
30+
31+
fn println(&self, output: &str) {
32+
println!("{}", &output);
33+
}
34+
35+
fn eprintln(&self, output: &str) {
36+
eprintln!("{}", &output);
37+
}
38+
}

src/checker/logger.rs

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::cell::RefCell;
2+
3+
use super::printer::{Printer, CLEAR_SCREEN};
4+
5+
pub struct Logger {
6+
output: RefCell<String>,
7+
}
8+
9+
impl Default for Logger {
10+
fn default() -> Self {
11+
Self::new()
12+
}
13+
}
14+
15+
impl Logger {
16+
#[must_use]
17+
pub fn new() -> Self {
18+
Self {
19+
output: RefCell::new(String::new()),
20+
}
21+
}
22+
23+
pub fn log(&self) -> String {
24+
self.output.borrow().clone()
25+
}
26+
}
27+
28+
impl Printer for Logger {
29+
fn clear(&self) {
30+
self.print(CLEAR_SCREEN);
31+
}
32+
33+
fn print(&self, output: &str) {
34+
*self.output.borrow_mut() = format!("{}{}", self.output.borrow(), &output);
35+
}
36+
37+
fn eprint(&self, output: &str) {
38+
*self.output.borrow_mut() = format!("{}{}", self.output.borrow(), &output);
39+
}
40+
41+
fn println(&self, output: &str) {
42+
self.print(&format!("{}/n", &output));
43+
}
44+
45+
fn eprintln(&self, output: &str) {
46+
self.eprint(&format!("{}/n", &output));
47+
}
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use crate::checker::logger::Logger;
53+
use crate::checker::printer::{Printer, CLEAR_SCREEN};
54+
55+
#[test]
56+
fn should_capture_the_clear_screen_command() {
57+
let console_logger = Logger::new();
58+
59+
console_logger.clear();
60+
61+
assert_eq!(CLEAR_SCREEN, console_logger.log());
62+
}
63+
64+
#[test]
65+
fn should_capture_the_print_command_output() {
66+
let console_logger = Logger::new();
67+
68+
console_logger.print("OUTPUT");
69+
70+
assert_eq!("OUTPUT", console_logger.log());
71+
}
72+
}

src/checker/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod app;
2+
pub mod config;
3+
pub mod console;
4+
pub mod logger;
5+
pub mod printer;
6+
pub mod service;

0 commit comments

Comments
 (0)