Compare commits

..

No commits in common. "db081869113e9b5b1b37675e49e9c75305f3f5f0" and "4a04612393cc391718e6ac441194e471d83e3416" have entirely different histories.

2 changed files with 39 additions and 73 deletions

View file

@ -7,37 +7,27 @@
<title>uppe.rs</title> <title>uppe.rs</title>
<style> <style>
span.cell { span.cell {
position: relative;
display: inline-block; display: inline-block;
width: .5rem; width: 1rem;
height: 1.2rem; height: 1rem;
border: 1px solid var(--secondary); border: 1px solid var(--secondary);
font-size: 8pt; font-size: 6pt;
line-height: 1.2rem; line-height: 1rem;
background-color: rgba(var(--secondary-rgb), 0.4); background-color: rgba(var(--secondary-rgb), 0.4);
margin-top: .3rem; margin-top: .2rem;
margin-bottom: .3rem; margin-bottom: .2rem;
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: .3rem; padding-top: .2rem;
padding-bottom: .3rem; padding-bottom: .2rem;
width: 2rem;
font-size: 8pt;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
margin-right: -0.75rem; line-height: 1rem;
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);
@ -46,9 +36,6 @@
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);
@ -56,20 +43,9 @@
hr.separator { hr.separator {
margin: 2em; margin: 2em;
} }
div.card { span.nobr {
display: inline-block;
white-space: nowrap; white-space: nowrap;
overflow-x: scroll; margin-right: 1em;
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>
@ -77,7 +53,6 @@
<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">
@ -91,7 +66,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>`;
} }
@ -103,11 +78,12 @@ 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 class="mt-0">${key} <code class="color">${last_rtt ? last_rtt + 'ms' : 'DOWN'}</code></h3> <h3>${key} (${last_rtt}ms)</h3>
<div class="box"> <div class="box">
${bar} <span class="nobr">${bar}</span>
</div> </div>
</div>`; </div>
<hr class="separator"/>`;
} }
let main = document.getElementById("uppe-rs-content"); let main = document.getElementById("uppe-rs-content");
@ -115,21 +91,15 @@ 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}?limit=120`); let res = await fetch(`/api/status/${key}`);
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, true).await?; let sid = db.sid(&key).await?;
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
let res = test_route(&service.endpoint).await; let res = test(&service.endpoint).await;
let value = match res { let value = match res {
Ok(rtt) => Some(rtt), Ok(rtt) => Some(rtt),
Err(e) => { Err(e) => {
@ -98,15 +98,6 @@ 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
@ -138,6 +129,15 @@ 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, false).await?; let sid = db.sid(&service).await?;
Ok(Json(db.get(sid, limit).await?)) Ok(Json(db.get(sid, Some(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 BIGINT NOT NULL, time BIG INTEGER NOT NULL,
value BIGINT NULL value BIG INTEGER 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 id, name FROM services")?; let mut stmt = db.prepare("SELECT * 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: i64) -> rusqlite::Result<Vec<Event>> { async fn get(&self, sid: i64, limit: Option<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)?)), |row| Ok((row.get(0)?, row.get(1).optional()?)),
)?; )?;
Ok( Ok(
@ -257,7 +257,7 @@ impl Database {
} }
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
async fn sid(&self, service: &str, upsert: bool) -> rusqlite::Result<i64> { async fn sid(&self, service: &str) -> 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,12 +267,8 @@ impl Database {
match res { match res {
Some(sid) => Ok(sid), Some(sid) => Ok(sid),
None => { None => {
if upsert { self.0.lock().await.execute("INSERT INTO services(name) VALUES (?)", params![service])?;
self.0.lock().await.execute("INSERT INTO services(name) VALUES (?)", params![service])?; self.sid(service).await
self.sid(service, upsert).await
} else {
Err(rusqlite::Error::QueryReturnedNoRows)
}
} }
} }
} }
@ -282,7 +278,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::<usize, Option<i64>>(0) |row| row.get(0).optional()
) )
} }
} }