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