initial version
This commit is contained in:
commit
79ec95913a
4 changed files with 2500 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
2256
Cargo.lock
generated
Normal file
2256
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "webmonitor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.97"
|
||||||
|
consul = "0.4.2"
|
||||||
|
df-consul = "0.3.5"
|
||||||
|
reqwest = "0.12.15"
|
||||||
|
structopt = "0.3.26"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
230
src/main.rs
Normal file
230
src/main.rs
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
use df_consul::Consul;
|
||||||
|
use reqwest::ClientBuilder;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use structopt::StructOpt;
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
#[structopt(name = "webmonitor")]
|
||||||
|
pub struct Opt {
|
||||||
|
/// Address of consul server
|
||||||
|
#[structopt(
|
||||||
|
long = "consul-addr",
|
||||||
|
env = "WEBMONITOR_CONSUL_HOST",
|
||||||
|
default_value = "http://127.0.0.1:8500"
|
||||||
|
)]
|
||||||
|
pub consul_addr: String,
|
||||||
|
|
||||||
|
/// CA certificate for Consul server with TLS
|
||||||
|
#[structopt(long = "consul-ca-cert", env = "WEBMONITOR_CONSUL_CA_CERT")]
|
||||||
|
pub consul_ca_cert: Option<String>,
|
||||||
|
|
||||||
|
/// Skip TLS verification for Consul
|
||||||
|
#[structopt(
|
||||||
|
long = "consul-tls-skip-verify",
|
||||||
|
env = "WEBMONITOR_CONSUL_TLS_SKIP_VERIFY"
|
||||||
|
)]
|
||||||
|
pub consul_tls_skip_verify: bool,
|
||||||
|
|
||||||
|
/// Client certificate for Consul server with TLS
|
||||||
|
#[structopt(long = "consul-client-cert", env = "WEBMONITOR_CONSUL_CLIENT_CERT")]
|
||||||
|
pub consul_client_cert: Option<String>,
|
||||||
|
|
||||||
|
/// Client key for Consul server with TLS
|
||||||
|
#[structopt(long = "consul-client-key", env = "WEBMONITOR_CONSUL_CLIENT_KEY")]
|
||||||
|
pub consul_client_key: Option<String>,
|
||||||
|
|
||||||
|
/// DNS provider
|
||||||
|
#[structopt(
|
||||||
|
long = "domain",
|
||||||
|
env = "WEBMONITOR_DOMAIN",
|
||||||
|
default_value = "deuxfleurs.fr"
|
||||||
|
)]
|
||||||
|
pub domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct GarageNode {
|
||||||
|
node: String,
|
||||||
|
site: Option<String>,
|
||||||
|
ip: String,
|
||||||
|
port: u16,
|
||||||
|
public_ipv4: Option<String>,
|
||||||
|
public_ipv6: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GarageNode {
|
||||||
|
fn has_public_addr(&self, addr: SocketAddr) -> bool {
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(a) => self.public_ipv4 == Some(a.ip().to_string()),
|
||||||
|
SocketAddr::V6(a) => self.public_ipv6 == Some(a.ip().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn garage_nodes(consul: &Consul) -> anyhow::Result<Vec<GarageNode>> {
|
||||||
|
let services = consul
|
||||||
|
.health_service_instances("garage-web", None)
|
||||||
|
.await?
|
||||||
|
.into_inner();
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
for s in services {
|
||||||
|
nodes.push(GarageNode {
|
||||||
|
node: s.node.node,
|
||||||
|
site: s.node.meta.get("site").cloned(),
|
||||||
|
ip: s.service.address,
|
||||||
|
port: s.service.port,
|
||||||
|
public_ipv4: s.node.meta.get("public_ipv4").cloned(),
|
||||||
|
public_ipv6: s.node.meta.get("public_ipv6").cloned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum EndpointKind {
|
||||||
|
Public,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
use EndpointKind::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Endpoint {
|
||||||
|
addr: SocketAddr,
|
||||||
|
kind: EndpointKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum CheckError {
|
||||||
|
WrongStatusCode(reqwest::StatusCode),
|
||||||
|
RequestPrepare(reqwest::Error),
|
||||||
|
RequestError(reqwest::Error),
|
||||||
|
}
|
||||||
|
use CheckError::*;
|
||||||
|
|
||||||
|
impl fmt::Display for CheckError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
WrongStatusCode(s) => write!(f, "wrong status code: {s}"),
|
||||||
|
RequestPrepare(e) => write!(f, "when preparing request: {e}"),
|
||||||
|
RequestError(e) => write!(f, "when sending request: {:?}", e.source()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckResult = Result<(), CheckError>;
|
||||||
|
|
||||||
|
async fn check_endpoint(domain: &str, e: Endpoint) -> CheckResult {
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.timeout(Duration::new(15, 0))
|
||||||
|
.resolve_to_addrs(domain, &[e.addr])
|
||||||
|
.build()
|
||||||
|
.map_err(|e| RequestError(e))?;
|
||||||
|
let url = match e.kind {
|
||||||
|
Public => format!("https://{domain}"),
|
||||||
|
Internal => format!("http://{domain}"),
|
||||||
|
};
|
||||||
|
let req = client.get(url).build().map_err(|e| RequestError(e))?;
|
||||||
|
let resp = client.execute(req).await;
|
||||||
|
match resp {
|
||||||
|
Ok(resp) if resp.status() == reqwest::StatusCode::OK => Ok(()),
|
||||||
|
Ok(resp) => Err(WrongStatusCode(resp.status())),
|
||||||
|
Err(e) => Err(RequestError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_endpoints(domain: &str, es: &[Endpoint]) -> Vec<(Endpoint, CheckResult, Duration)> {
|
||||||
|
let mut set = JoinSet::new();
|
||||||
|
for e in es {
|
||||||
|
let e = *e;
|
||||||
|
let domain = domain.to_string();
|
||||||
|
set.spawn(async move {
|
||||||
|
let start_t = Instant::now();
|
||||||
|
let res = check_endpoint(&domain, e).await;
|
||||||
|
let duration = Instant::now().duration_since(start_t);
|
||||||
|
(e, res, duration)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set.join_all().await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_result(nodes: &[GarageNode], (e, res, duration): &(Endpoint, CheckResult, Duration)) {
|
||||||
|
match e.addr {
|
||||||
|
// approximate padding to align with v6 addresses
|
||||||
|
SocketAddr::V4(_) => print!("{} ", e.addr),
|
||||||
|
SocketAddr::V6(_) => print!("{} ", e.addr),
|
||||||
|
};
|
||||||
|
print!("\t");
|
||||||
|
// display a human-friendly site or node name
|
||||||
|
let human_name = match e.kind {
|
||||||
|
Public => nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.has_public_addr(e.addr))
|
||||||
|
.and_then(|n| n.site.clone()),
|
||||||
|
Internal => nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.ip == e.addr.ip().to_string())
|
||||||
|
.map(|n| n.node.clone()),
|
||||||
|
};
|
||||||
|
print!("[{}]\t", human_name.unwrap_or(String::from("??")));
|
||||||
|
match res {
|
||||||
|
Ok(()) => print!("OK"),
|
||||||
|
Err(e) => print!("ERROR: {e}"),
|
||||||
|
}
|
||||||
|
println!("\t({:.2}s)", duration.as_secs_f32());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
|
info!("Starting webmonitor");
|
||||||
|
|
||||||
|
let consul_config = df_consul::Config {
|
||||||
|
addr: opt.consul_addr.clone(),
|
||||||
|
ca_cert: opt.consul_ca_cert.clone(),
|
||||||
|
tls_skip_verify: opt.consul_tls_skip_verify,
|
||||||
|
client_cert: opt.consul_client_cert.clone(),
|
||||||
|
client_key: opt.consul_client_key.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let consul = df_consul::Consul::new(consul_config, "").expect("Cannot build Consul");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let garage_nodes = garage_nodes(&consul).await?;
|
||||||
|
|
||||||
|
let public_endpoints: Vec<Endpoint> = tokio::net::lookup_host((opt.domain.as_ref(), 0))
|
||||||
|
.await?
|
||||||
|
.map(|addr| Endpoint { addr, kind: Public })
|
||||||
|
.collect();
|
||||||
|
let internal_endpoints: Vec<Endpoint> = garage_nodes
|
||||||
|
.iter()
|
||||||
|
.map(|n| Endpoint {
|
||||||
|
addr: SocketAddr::from((n.ip.parse::<IpAddr>().unwrap(), n.port)),
|
||||||
|
kind: Internal,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let (public_res, internal_res) = tokio::join! {
|
||||||
|
check_endpoints(&opt.domain, &public_endpoints),
|
||||||
|
check_endpoints(&opt.domain, &internal_endpoints),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("--- public endpoints ---");
|
||||||
|
public_res
|
||||||
|
.iter()
|
||||||
|
.for_each(|res| print_result(&garage_nodes, res));
|
||||||
|
println!("--- internal endpoints ---");
|
||||||
|
internal_res
|
||||||
|
.iter()
|
||||||
|
.for_each(|res| print_result(&garage_nodes, res));
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::new(60, 0)).await;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue