Compare commits

...

7 commits

2 changed files with 73 additions and 39 deletions

View file

@ -7,27 +7,37 @@
<title>uppe.rs</title> <title>uppe.rs</title>
<style> <style>
span.cell { span.cell {
position: relative;
display: inline-block; display: inline-block;
width: 1rem; width: .5rem;
height: 1rem; height: 1.2rem;
border: 1px solid var(--secondary); border: 1px solid var(--secondary);
font-size: 6pt; font-size: 8pt;
line-height: 1rem; line-height: 1.2rem;
background-color: rgba(var(--secondary-rgb), 0.4); background-color: rgba(var(--secondary-rgb), 0.4);
margin-top: .2rem; margin-top: .3rem;
margin-bottom: .2rem; margin-bottom: .3rem;
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
transition: .1s; transition: .1s;
text-align: center; text-align: center;
cursor: default; cursor: default;
border-radius: .3rem;
color: #ffffff00;
} }
span.cell:hover { span.cell:hover {
padding-top: .2rem; padding-top: .3rem;
padding-bottom: .2rem; padding-bottom: .3rem;
width: 2rem;
font-size: 8pt;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
line-height: 1rem; margin-right: -0.75rem;
margin-left: -0.75rem;
color: var(--background);
background-color: rgba(var(--secondary-rgb), 1);
z-index: 1;
font-weight: bold;
} }
span.cell.warning { span.cell.warning {
border-color: var(--accent); border-color: var(--accent);
@ -36,6 +46,9 @@
border-color: var(--accent); border-color: var(--accent);
background-color: rgba(var(--accent-rgb), 0.4); background-color: rgba(var(--accent-rgb), 0.4);
} }
span.cell.error:hover {
background-color: rgba(var(--accent-rgb), 1);
}
hr.color { hr.color {
color: var(--accent); color: var(--accent);
border-color: var(--accent); border-color: var(--accent);
@ -43,9 +56,20 @@
hr.separator { hr.separator {
margin: 2em; margin: 2em;
} }
span.nobr { div.card {
display: inline-block;
white-space: nowrap; white-space: nowrap;
margin-right: 1em; overflow-x: scroll;
max-width: 100%;
margin-top: 2em;
border-radius: 1em;
border: 1px solid var(--background-secondary);
padding: 1em;
box-sizing: border-box;
transition: .3s;
}
div.card:hover {
background-color: var(--background-dim);
} }
</style> </style>
</head> </head>
@ -53,6 +77,7 @@
<h1>uppe.rs</h1> <h1>uppe.rs</h1>
<p>keeping track of your infra's up status</p> <p>keeping track of your infra's up status</p>
<hr class="color"/> <hr class="color"/>
<small style="display: block" class="rev">now --&gt;</small>
<main id="uppe-rs-content"> <main id="uppe-rs-content">
@ -66,7 +91,7 @@ function cell(timestamp, rtt) {
warning = " warning"; warning = " warning";
} }
if (rtt === null) { if (rtt === null) {
return `<span class="cell error" title="${d}"></span>`; return `<span class="cell error" title="${d}"></span>`;
} else { } else {
return `<span class="cell${warning}" title="${rtt}ms -- ${d}">${rtt}</span>`; return `<span class="cell${warning}" title="${rtt}ms -- ${d}">${rtt}</span>`;
} }
@ -78,12 +103,11 @@ function card(key, history, last_rtt) {
bar += cell(el[0], el[1]); bar += cell(el[0], el[1]);
} }
return `<div class="card"> return `<div class="card">
<h3>${key} (${last_rtt}ms)</h3> <h3 class="mt-0">${key} <code class="color">${last_rtt ? last_rtt + 'ms' : 'DOWN'}</code></h3>
<div class="box"> <div class="box">
<span class="nobr">${bar}</span> ${bar}
</div> </div>
</div> </div>`;
<hr class="separator"/>`;
} }
let main = document.getElementById("uppe-rs-content"); let main = document.getElementById("uppe-rs-content");
@ -91,15 +115,21 @@ let main = document.getElementById("uppe-rs-content");
async function updateStatus() { async function updateStatus() {
let res = await fetch("/api/status") let res = await fetch("/api/status")
let status = await res.json() let status = await res.json()
if (status.error) {
console.error("server error:", status);
return;
}
let keys = Object.keys(status); let keys = Object.keys(status);
keys.sort(); keys.sort();
let out = ""; let out = "";
for (let key of keys) { for (let key of keys) {
let res = await fetch(`/api/status/${key}`); let res = await fetch(`/api/status/${key}?limit=120`);
let history = await res.json(); let history = await res.json();
out += card(key, history, status[key]); out += card(key, history, status[key]);
out += "\n";
} }
main.innerHTML = out; main.innerHTML = out;

View file

@ -65,11 +65,11 @@ async fn entry(cli: Cli, config: Config, db: Database) -> Result<(), Box<dyn std
for (key, service) in config.service { for (key, service) in config.service {
let interval = service.interval_s.unwrap_or(default_interval); let interval = service.interval_s.unwrap_or(default_interval);
let db = db.clone(); let db = db.clone();
let sid = db.sid(&key).await?; let sid = db.sid(&key, true).await?;
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
let res = test(&service.endpoint).await; let res = test_route(&service.endpoint).await;
let value = match res { let value = match res {
Ok(rtt) => Some(rtt), Ok(rtt) => Some(rtt),
Err(e) => { Err(e) => {
@ -98,6 +98,15 @@ async fn entry(cli: Cli, config: Config, db: Database) -> Result<(), Box<dyn std
Ok(()) Ok(())
} }
async fn test_route(url: &str) -> reqwest::Result<i64> {
let before = chrono::Utc::now();
reqwest::get(url)
.await?
.error_for_status()?;
let delta = chrono::Utc::now() - before;
Ok(delta.num_milliseconds())
}
// ============= APIs // ============= APIs
@ -129,15 +138,6 @@ async fn root() -> Html<&'static str> {
Html(include_str!("../index.html")) Html(include_str!("../index.html"))
} }
async fn test(url: &str) -> reqwest::Result<i64> {
let before = chrono::Utc::now();
reqwest::get(url)
.await?
.error_for_status()?;
let delta = chrono::Utc::now() - before;
Ok(delta.num_milliseconds())
}
use axum::{extract::{Path, Query, State}, response::{Html, IntoResponse}, Json}; use axum::{extract::{Path, Query, State}, response::{Html, IntoResponse}, Json};
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -172,8 +172,8 @@ async fn api_status_service(
Query(q): Query<ServiceStatusQuery>, Query(q): Query<ServiceStatusQuery>,
) -> ApiResult<Vec<(i64, Option<i64>)>> { ) -> ApiResult<Vec<(i64, Option<i64>)>> {
let limit = q.limit.unwrap_or(50).min(250); let limit = q.limit.unwrap_or(50).min(250);
let sid = db.sid(&service).await?; let sid = db.sid(&service, false).await?;
Ok(Json(db.get(sid, Some(limit)).await?)) Ok(Json(db.get(sid, limit).await?))
} }
@ -195,8 +195,8 @@ impl Database {
"CREATE TABLE IF NOT EXISTS events ( "CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
service INTEGER NOT NULL, service INTEGER NOT NULL,
time BIG INTEGER NOT NULL, time BIGINT NOT NULL,
value BIG INTEGER NULL value BIGINT NULL
)", params![] )", params![]
)?; )?;
@ -224,7 +224,7 @@ impl Database {
async fn services(&self) -> rusqlite::Result<Vec<(i64, String)>> { async fn services(&self) -> rusqlite::Result<Vec<(i64, String)>> {
let db = self.0.lock().await; let db = self.0.lock().await;
let mut stmt = db.prepare("SELECT * FROM services")?; let mut stmt = db.prepare("SELECT id, name FROM services")?;
let res = stmt.query_map( let res = stmt.query_map(
params![], params![],
|row| Ok((row.get(0)?, row.get(1)?)) |row| Ok((row.get(0)?, row.get(1)?))
@ -241,12 +241,12 @@ impl Database {
Ok(()) Ok(())
} }
async fn get(&self, sid: i64, limit: Option<i64>) -> rusqlite::Result<Vec<Event>> { async fn get(&self, sid: i64, limit: i64) -> rusqlite::Result<Vec<Event>> {
let db = self.0.lock().await; let db = self.0.lock().await;
let mut stmt = db.prepare("SELECT time, value FROM events WHERE service = :sid LIMIT :limit")?; let mut stmt = db.prepare("SELECT time, value FROM events WHERE service = :sid LIMIT :limit")?;
let results = stmt.query_map( let results = stmt.query_map(
named_params! { ":sid": sid, ":limit": limit }, named_params! { ":sid": sid, ":limit": limit },
|row| Ok((row.get(0)?, row.get(1).optional()?)), |row| Ok((row.get(0)?, row.get(1)?)),
)?; )?;
Ok( Ok(
@ -257,7 +257,7 @@ impl Database {
} }
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
async fn sid(&self, service: &str) -> rusqlite::Result<i64> { async fn sid(&self, service: &str, upsert: bool) -> rusqlite::Result<i64> {
let res = { let res = {
let db = self.0.lock().await; let db = self.0.lock().await;
let mut stmt = db.prepare("SELECT id FROM services WHERE name = ?")?; let mut stmt = db.prepare("SELECT id FROM services WHERE name = ?")?;
@ -267,8 +267,12 @@ impl Database {
match res { match res {
Some(sid) => Ok(sid), Some(sid) => Ok(sid),
None => { None => {
self.0.lock().await.execute("INSERT INTO services(name) VALUES (?)", params![service])?; if upsert {
self.sid(service).await self.0.lock().await.execute("INSERT INTO services(name) VALUES (?)", params![service])?;
self.sid(service, upsert).await
} else {
Err(rusqlite::Error::QueryReturnedNoRows)
}
} }
} }
} }
@ -278,7 +282,7 @@ impl Database {
let mut stmt = db.prepare("SELECT value FROM events WHERE service = :sid AND time > :time")?; let mut stmt = db.prepare("SELECT value FROM events WHERE service = :sid AND time > :time")?;
stmt.query_row( stmt.query_row(
named_params! { ":sid": sid, ":time": since }, named_params! { ":sid": sid, ":time": since },
|row| row.get(0).optional() |row| row.get::<usize, Option<i64>>(0)
) )
} }
} }