Compare commits
7 commits
4a04612393
...
db08186911
Author | SHA1 | Date | |
---|---|---|---|
db08186911 | |||
70ef6e0e8e | |||
4cd67c3867 | |||
64d53a5f51 | |||
661b80bcb4 | |||
864f38f0da | |||
201cae7ad1 |
2 changed files with 73 additions and 39 deletions
64
index.html
64
index.html
|
@ -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 --></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;
|
||||||
|
|
46
src/main.rs
46
src/main.rs
|
@ -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 => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue