fet: reduce LOC by reducing comments
This commit is contained in:
@@ -13,10 +13,7 @@ use tracing::{error, info};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Install default rustls crypto provider (required for cert expiry checks)
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.ok(); // ok() — ignore error if already installed
|
||||
rustls::crypto::ring::default_provider().install_default().ok();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
|
||||
|
||||
@@ -1,38 +1,3 @@
|
||||
/// PingQL query evaluation against a check response.
|
||||
///
|
||||
/// Query shape (MongoDB-inspired):
|
||||
///
|
||||
/// Simple equality:
|
||||
/// { "status": 200 }
|
||||
///
|
||||
/// Operators:
|
||||
/// { "status": { "$eq": 200 } }
|
||||
/// { "status": { "$ne": 500 } }
|
||||
/// { "status": { "$gte": 200, "$lt": 300 } }
|
||||
/// { "body": { "$contains": "healthy" } }
|
||||
/// { "body": { "$startsWith": "OK" } }
|
||||
/// { "body": { "$endsWith": "done" } }
|
||||
/// { "body": { "$regex": "ok|healthy" } }
|
||||
/// { "body": { "$exists": true } }
|
||||
/// { "status": { "$in": [200, 201, 204] } }
|
||||
///
|
||||
/// CSS selector (HTML parsing):
|
||||
/// { "$select": "span.status", "$eq": "operational" }
|
||||
///
|
||||
/// JSONPath:
|
||||
/// { "$json": "$.data.status", "$eq": "ok" }
|
||||
///
|
||||
/// Response time:
|
||||
/// { "$responseTime": { "$lt": 500 } }
|
||||
///
|
||||
/// Certificate expiry:
|
||||
/// { "$certExpiry": { "$gt": 30 } }
|
||||
///
|
||||
/// Logical:
|
||||
/// { "$and": [ { "status": 200 }, { "body": { "$contains": "ok" } } ] }
|
||||
/// { "$or": [ { "status": 200 }, { "status": 204 } ] }
|
||||
/// { "$not": { "status": 500 } }
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use regex::Regex;
|
||||
use scraper::{Html, Selector};
|
||||
@@ -46,11 +11,9 @@ pub struct Response {
|
||||
pub cert_expiry_days: Option<i64>,
|
||||
}
|
||||
|
||||
/// Returns true if `query` matches `response`. No query = always up.
|
||||
pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
match query {
|
||||
Value::Object(map) => {
|
||||
// $consider — "up" (default) or "down": flips result if conditions match
|
||||
if let Some(consider) = map.get("$consider") {
|
||||
let is_down = consider.as_str() == Some("down");
|
||||
let rest: serde_json::Map<String, Value> = map.iter()
|
||||
@@ -61,7 +24,6 @@ pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
return Ok(if is_down { !matches } else { matches });
|
||||
}
|
||||
|
||||
// $and / $or / $not
|
||||
if let Some(and) = map.get("$and") {
|
||||
let Value::Array(clauses) = and else { bail!("$and expects array") };
|
||||
return Ok(clauses.iter().all(|c| evaluate(c, response).unwrap_or(false)));
|
||||
@@ -74,19 +36,14 @@ pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
return Ok(!evaluate(not, response)?);
|
||||
}
|
||||
|
||||
// $responseTime
|
||||
if let Some(cond) = map.get("$responseTime") {
|
||||
let val = Value::Number(serde_json::Number::from(response.latency_ms.unwrap_or(0)));
|
||||
return eval_condition(cond, &val, response);
|
||||
}
|
||||
|
||||
// $certExpiry
|
||||
if let Some(cond) = map.get("$certExpiry") {
|
||||
let val = Value::Number(serde_json::Number::from(response.cert_expiry_days.unwrap_or(0)));
|
||||
return eval_condition(cond, &val, response);
|
||||
}
|
||||
|
||||
// $json — { "$json": { "$.path": { "$op": val } } }
|
||||
if let Some(json_path_map) = map.get("$json") {
|
||||
let path_map = match json_path_map {
|
||||
Value::Object(m) => m,
|
||||
@@ -99,13 +56,11 @@ pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// $select — { "$select": { "css.selector": { "$op": val } } }
|
||||
if let Some(sel_map) = map.get("$select") {
|
||||
let sel_obj = match sel_map {
|
||||
Value::Object(m) => m,
|
||||
_ => bail!("$select expects an object {{ selector: condition }}"),
|
||||
};
|
||||
// Parse HTML once for all selectors
|
||||
let doc = Html::parse_document(&response.body);
|
||||
for (selector, condition) in sel_obj {
|
||||
let sel = Selector::parse(selector)
|
||||
@@ -118,7 +73,6 @@ pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Field-level checks
|
||||
for (field, condition) in map {
|
||||
let field_val = resolve_field(field, response);
|
||||
if !eval_condition(condition, &field_val, response)? {
|
||||
@@ -154,7 +108,6 @@ fn resolve_json_path(body: &str, path: &str) -> Value {
|
||||
if path.is_empty() { return obj; }
|
||||
let mut current = &obj;
|
||||
for part in path.split('.') {
|
||||
// Handle array indexing like "items[0]"
|
||||
if let Some(idx_start) = part.find('[') {
|
||||
let key = &part[..idx_start];
|
||||
if !key.is_empty() {
|
||||
@@ -184,7 +137,6 @@ fn resolve_json_path(body: &str, path: &str) -> Value {
|
||||
|
||||
fn eval_condition(condition: &Value, field_val: &Value, response: &Response) -> Result<bool> {
|
||||
match condition {
|
||||
// Shorthand: { "status": 200 }
|
||||
Value::Number(n) => Ok(field_val.as_f64() == n.as_f64()),
|
||||
Value::String(s) => Ok(field_val.as_str() == Some(s.as_str())),
|
||||
Value::Bool(b) => Ok(field_val.as_bool() == Some(*b)),
|
||||
@@ -242,11 +194,8 @@ fn eval_op(op: &str, field_val: &Value, val: &Value, response: &Response) -> Res
|
||||
}
|
||||
}
|
||||
"$select" => {
|
||||
// Nested: { "body": { "$select": "css", "$eq": "val" } }
|
||||
let sel_str = val.as_str().unwrap_or("");
|
||||
let selected = css_select(&response.body, sel_str);
|
||||
// If no comparison operator follows, just check existence
|
||||
selected.is_some()
|
||||
css_select(&response.body, sel_str).is_some()
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unknown query operator: {op}");
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::time::Instant;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
// Cache native root certs per OS thread to avoid reloading from disk on every check.
|
||||
thread_local! {
|
||||
static ROOT_CERTS: Arc<Vec<ureq::tls::Certificate<'static>>> = Arc::new(
|
||||
rustls_native_certs::load_native_certs()
|
||||
@@ -19,7 +18,6 @@ thread_local! {
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch due monitors from coordinator, run them, post results back.
|
||||
pub async fn fetch_and_run(
|
||||
client: &reqwest::Client,
|
||||
coordinator_url: &str,
|
||||
@@ -27,8 +25,6 @@ pub async fn fetch_and_run(
|
||||
region: &str,
|
||||
in_flight: &Arc<Mutex<HashSet<String>>>,
|
||||
) -> Result<usize> {
|
||||
// Fetch monitors due within the next 2s — nodes receive exact scheduled_at_ms
|
||||
// and sleep until that moment, so all regions fire in tight coordination.
|
||||
let url = if region.is_empty() {
|
||||
format!("{coordinator_url}/internal/due?lookahead_ms=2000")
|
||||
} else {
|
||||
@@ -45,12 +41,10 @@ pub async fn fetch_and_run(
|
||||
let n = monitors.len();
|
||||
if n == 0 { return Ok(0); }
|
||||
|
||||
// Shared read-only strings — clone the Arc instead of allocating per monitor
|
||||
let coordinator_url: Arc<str> = Arc::from(coordinator_url);
|
||||
let token: Arc<str> = Arc::from(token);
|
||||
let region: Arc<str> = Arc::from(region);
|
||||
|
||||
// Spawn all checks — fire and forget, skip if already in-flight
|
||||
let mut spawned = 0usize;
|
||||
for monitor in monitors {
|
||||
{
|
||||
@@ -66,7 +60,6 @@ pub async fn fetch_and_run(
|
||||
let coordinator_url = Arc::clone(&coordinator_url);
|
||||
let token = Arc::clone(&token);
|
||||
let region_owned = Arc::clone(®ion);
|
||||
// run_id: hash(monitor_id, interval_bucket) — same across all regions for this window
|
||||
let run_id_owned = {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
@@ -77,7 +70,6 @@ pub async fn fetch_and_run(
|
||||
bucket.hash(&mut h);
|
||||
format!("{:016x}", h.finish())
|
||||
};
|
||||
// Convert scheduled_at_ms to an ISO string for storage in the ping
|
||||
let scheduled_at_iso = monitor.scheduled_at_ms.map(|ms| {
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp_millis(ms)
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
@@ -85,7 +77,6 @@ pub async fn fetch_and_run(
|
||||
});
|
||||
let in_flight = in_flight.clone();
|
||||
tokio::spawn(async move {
|
||||
// Sleep until the exact scheduled moment — tight multi-region coordination
|
||||
if let Some(ms) = monitor.scheduled_at_ms {
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
if ms > now_ms {
|
||||
@@ -94,7 +85,6 @@ pub async fn fetch_and_run(
|
||||
}
|
||||
}
|
||||
let timeout_ms = monitor.timeout_ms.unwrap_or(30000);
|
||||
// Hard deadline: timeout + 5s buffer, so hung checks always resolve
|
||||
let deadline = std::time::Duration::from_millis(timeout_ms + 5000);
|
||||
let result = match tokio::time::timeout(deadline, run_check(&client, &monitor, scheduled_at_iso.clone(), ®ion_owned, &run_id_owned)).await {
|
||||
Ok(r) => r,
|
||||
@@ -113,8 +103,6 @@ pub async fn fetch_and_run(
|
||||
run_id: Some(run_id_owned.clone()),
|
||||
},
|
||||
};
|
||||
// Post result first, then clear in-flight — this prevents the next
|
||||
// poll from picking up the monitor again before the ping is persisted.
|
||||
if let Err(e) = post_result(&client, &coordinator_url, &token, result).await {
|
||||
warn!("Failed to post result for {}: {e}", monitor.id);
|
||||
}
|
||||
@@ -126,10 +114,7 @@ pub async fn fetch_and_run(
|
||||
}
|
||||
|
||||
async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Option<String>, region: &str, run_id: &str) -> PingResult {
|
||||
// Record when the check actually started (used as checked_at in the ping)
|
||||
let checked_at = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Compute jitter: how late we actually started vs when we were scheduled
|
||||
let jitter_ms: Option<i64> = scheduled_at.as_deref().and_then(|s| {
|
||||
let scheduled = chrono::DateTime::parse_from_rfc3339(s).ok()?;
|
||||
let now = chrono::Utc::now();
|
||||
@@ -138,15 +123,10 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Build request with method, headers, body, timeout
|
||||
let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase();
|
||||
let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000));
|
||||
let is_https = monitor.url.starts_with("https://");
|
||||
|
||||
// Run the check in a real OS thread using ureq (blocking, synchronous HTTP).
|
||||
// ureq sets SO_RCVTIMEO/SO_SNDTIMEO at the socket level, which reliably
|
||||
// interrupts even a hanging TLS handshake — unlike async reqwest which
|
||||
// cannot cancel syscall-level blocks via future cancellation.
|
||||
let url = monitor.url.clone();
|
||||
let req_headers = monitor.request_headers.clone();
|
||||
let req_body = monitor.request_body.clone();
|
||||
@@ -190,9 +170,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||
},
|
||||
Ok((status, headers, body)) => {
|
||||
|
||||
// Start cert expiry check in background — don't block result posting.
|
||||
// We'll use None for cert_expiry_days in query evaluation since it
|
||||
// shouldn't delay the main result by seconds of extra TLS handshake.
|
||||
let cert_handle = if is_https {
|
||||
let cert_url = monitor.url.clone();
|
||||
Some(tokio::spawn(async move {
|
||||
@@ -210,9 +187,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||
|
||||
let query = &monitor.query;
|
||||
|
||||
// Evaluate query if present (cert_expiry_days not yet available —
|
||||
// $certExpiry queries will use None here; the actual value is
|
||||
// attached to the result once the background check completes)
|
||||
let (up, query_error) = if let Some(q) = query {
|
||||
let response = Response {
|
||||
status,
|
||||
@@ -225,16 +199,13 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||
Ok(result) => (result, None),
|
||||
Err(e) => {
|
||||
warn!("Query error for {}: {e}", monitor.id);
|
||||
// Fall back to status-based up/down
|
||||
(status < 400, Some(e.to_string()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default: up if 2xx/3xx
|
||||
(status < 400, None)
|
||||
};
|
||||
|
||||
// Await the cert check now (it's been running concurrently during query eval)
|
||||
let cert_expiry_days = match cert_handle {
|
||||
Some(h) => h.await.unwrap_or(None),
|
||||
None => None,
|
||||
@@ -265,10 +236,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||
}
|
||||
}
|
||||
|
||||
/// Run an HTTP check synchronously using ureq.
|
||||
/// ureq applies timeouts at the socket/IO level (not just future cancellation),
|
||||
/// which reliably interrupts hanging TLS handshakes.
|
||||
/// Must be called from a std::thread (not async context).
|
||||
fn run_check_blocking(
|
||||
url: &str,
|
||||
method: &str,
|
||||
@@ -276,7 +243,6 @@ fn run_check_blocking(
|
||||
body: Option<&str>,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<(u16, HashMap<String, String>, String), String> {
|
||||
// Reuse cached root certs (loaded once per OS thread via thread_local)
|
||||
let root_certs = ROOT_CERTS.with(|c| Arc::clone(c));
|
||||
|
||||
let tls = ureq::tls::TlsConfig::builder()
|
||||
@@ -352,8 +318,6 @@ fn run_check_blocking(
|
||||
Ok((status, resp_headers, body_out))
|
||||
}
|
||||
|
||||
/// Check SSL certificate expiry for a given HTTPS URL.
|
||||
/// Returns the number of days until the certificate expires.
|
||||
async fn check_cert_expiry(url: &str) -> Result<Option<i64>> {
|
||||
use rustls::ClientConfig;
|
||||
use rustls::pki_types::ServerName;
|
||||
@@ -361,12 +325,10 @@ async fn check_cert_expiry(url: &str) -> Result<Option<i64>> {
|
||||
use tokio_rustls::TlsConnector;
|
||||
use x509_parser::prelude::*;
|
||||
|
||||
// Parse host and port from URL
|
||||
let url_parsed = reqwest::Url::parse(url)?;
|
||||
let host = url_parsed.host_str().unwrap_or("");
|
||||
let port = url_parsed.port().unwrap_or(443);
|
||||
|
||||
// Build a rustls config that captures certificates
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
@@ -380,7 +342,6 @@ async fn check_cert_expiry(url: &str) -> Result<Option<i64>> {
|
||||
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
|
||||
// Get peer certificates
|
||||
let (_, conn) = tls_stream.get_ref();
|
||||
let certs = conn.peer_certificates().unwrap_or(&[]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user