This commit is contained in:
nora 2023-09-02 18:14:08 +02:00
parent 8af7829b0e
commit f17454ae67
3 changed files with 125 additions and 4 deletions

View file

@ -18,7 +18,7 @@ pub struct CheckResult {
pub state: CheckState, pub state: CheckState,
} }
#[derive(sqlx::Type)] #[derive(Debug, PartialEq, sqlx::Type)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
pub enum CheckState { pub enum CheckState {
Ok, Ok,

View file

@ -7,6 +7,7 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use chrono::{DateTime, Utc};
use eyre::{Context, Result}; use eyre::{Context, Result};
use http::StatusCode; use http::StatusCode;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
@ -58,14 +59,19 @@ fn compute_status(checks: Vec<Check>) -> Vec<WebsiteStatus> {
websites websites
.into_iter() .into_iter()
.map(|(website, checks)| { .map(|(website, mut checks)| {
checks.sort_by_key(|check| check.0);
let mut last_ok = None; let mut last_ok = None;
let mut count_ok = 0; let mut count_ok = 0;
const BAR_ELEMS: usize = 100;
let bar_classes = checks_to_classes(&checks, BAR_ELEMS);
let len = checks.len(); let len = checks.len();
checks.into_iter().for_each(|(time, result)| { checks.into_iter().for_each(|(time, result)| {
last_ok = std::cmp::max(last_ok, Some(time));
if let CheckState::Ok = result { if let CheckState::Ok = result {
last_ok = std::cmp::max(last_ok, Some(time));
count_ok += 1; count_ok += 1;
} }
}); });
@ -81,6 +87,84 @@ fn compute_status(checks: Vec<Check>) -> Vec<WebsiteStatus> {
ok_ratio, ok_ratio,
count_ok, count_ok,
total_requests: len, total_requests: len,
bar_classes,
}
})
.collect()
}
enum BarClass {
Green,
Orange,
Red,
Unknown,
}
impl BarClass {
fn as_class(&self) -> &'static str {
match self {
Self::Green => "check-result-green",
Self::Orange => "check-result-orange",
Self::Red => "check-result-red",
Self::Unknown => "check-result-unknown",
}
}
}
/// Converts a list of (sorted by time) checks at arbitrary dates into a list of boxes for the
/// frontend, in a fixed sensical timeline.
/// We slice the time from the first check to the last check (maybe something like last check-30d
/// in the future) into slices and aggregate all checks from these times into these slices.
fn checks_to_classes(checks: &[(DateTime<Utc>, CheckState)], classes: usize) -> Vec<BarClass> {
assert_ne!(classes, 0);
let Some(first) = checks.first() else {
return vec![];
};
let last = checks.last().unwrap();
let mut bins = vec![vec![]; classes];
let first = first.0.timestamp_millis();
let last = last.0.timestamp_millis();
let last_rel = last - first;
assert!(last.is_positive(), "checks not ordered correctly");
for check in checks {
let time_rel = check.0.timestamp_millis() - first;
assert!(first.is_positive(), "checks not ordered correctly");
/*
5 bins:
| | | | | |
0.0 0.2 0.4 0.6 0.8 1.0 division
0.0 1.0 2.0 3.0 4.0 5.0 after multiply
*/
let bin = (time_rel as f64) / (last_rel as f64) * ((classes) as f64);
let bin = bin as usize; // flooring on purpose
let bin = if bin == classes { bin - 1 } else { bin };
bins[bin].push(check);
}
bins.iter()
.map(|checks| {
let ok = checks
.iter()
.filter(|check| check.1 == CheckState::Ok)
.count();
let all = checks.len();
if all == 0 {
BarClass::Unknown
} else if all == ok {
BarClass::Green
} else if ok == 0 {
BarClass::Red
} else if ok > 0 && ok < all {
BarClass::Orange
} else {
unreachable!("i dont think logic works like this")
} }
}) })
.collect() .collect()
@ -92,6 +176,7 @@ struct WebsiteStatus {
ok_ratio: String, ok_ratio: String,
total_requests: usize, total_requests: usize,
count_ok: usize, count_ok: usize,
bar_classes: Vec<BarClass>,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -5,6 +5,36 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Uptime</title> <title>Uptime</title>
<style>
html {
font-family: Arial, Helvetica, sans-serif;
}
.check-result-bar {
display: flex;
}
.check-result {
height: 10px;
width: 5px;
}
.check-result-red {
background-color: red;
}
.check-result-orange {
background-color: orange;
}
.check-result-green {
background-color: green;
}
.check-result-unknown {
background-color: grey;
}
</style>
</head> </head>
<body> <body>
<main> <main>
@ -16,6 +46,12 @@
<p>Last OK: <span class="utc-timestamp">{{ check.last_ok.as_deref().unwrap() }}</span></p> <p>Last OK: <span class="utc-timestamp">{{ check.last_ok.as_deref().unwrap() }}</span></p>
{% endif %} {% endif %}
<div class="check-result-bar">
{% for result in check.bar_classes %}
<div class="check-result {{ result.as_class() }}"></div>
{% endfor %}
</div>
{% endfor %} {% endfor %}
</main> </main>