Using heartbeat api instead
This commit is contained in:
parent
4cedb17b60
commit
d024280878
10 changed files with 319 additions and 40 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -1205,9 +1205,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.145"
|
version = "1.0.146"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1577,6 +1577,8 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,5 @@ anyhow = "1.0"
|
||||||
fluent-templates = "0.13.2"
|
fluent-templates = "0.13.2"
|
||||||
unic-langid = "0.9.6"
|
unic-langid = "0.9.6"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0.146"
|
||||||
|
|
|
||||||
124
formgen.txt
Normal file
124
formgen.txt
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
.
|
||||||
|
├── Cargo.toml
|
||||||
|
├── formgen.txt
|
||||||
|
├── .gitignore
|
||||||
|
└── src
|
||||||
|
├── i18n
|
||||||
|
│ ├── loader.rs
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ └── translate.rs
|
||||||
|
├── locales
|
||||||
|
│ ├── en-US
|
||||||
|
│ │ └── main.ftl
|
||||||
|
│ └── pt-BR
|
||||||
|
│ └── main.ftl
|
||||||
|
└── main.rs
|
||||||
|
|
||||||
|
6 directories, 9 files
|
||||||
|
|
||||||
|
|
||||||
|
=== CONTEÚDO DOS ARQUIVOS ===
|
||||||
|
|
||||||
|
|
||||||
|
=== ./src/main.rs ===
|
||||||
|
|
||||||
|
use anyhow::{Ok, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
mod i18n;
|
||||||
|
use i18n::{init_locales, t};
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(author, version, about)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short, long, env = "UPTIME_KUMA_URL")]
|
||||||
|
url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long, env = "UPTIME_KUMA_API_KEY")]
|
||||||
|
api_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
init_locales()?;
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
let url = args
|
||||||
|
.url
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("{}", t("missing_url")))?;
|
||||||
|
let api_key = args
|
||||||
|
.api_key
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("{}", t("missing_api_key")))?;
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client.get(url).basic_auth("", Some(api_key)).send()?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let metrics = response.text()?;
|
||||||
|
println!("{}", t("success"));
|
||||||
|
println!("{}", t("metrics_preview"));
|
||||||
|
println!("{}", &metrics[..200.min(metrics.len())]);
|
||||||
|
} else {
|
||||||
|
println!("{}", response.status());
|
||||||
|
println!("{}: {}", t("Response"), response.text()?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
=== ./src/i18n/translate.rs ===
|
||||||
|
|
||||||
|
use crate::i18n::loader::LOCALES;
|
||||||
|
use fluent_templates::Loader;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
fn get_sys_locale() -> LanguageIdentifier {
|
||||||
|
let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR"));
|
||||||
|
LanguageIdentifier::from_str(&sys_lang).expect("Invalid language")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn t(key: &str) -> String {
|
||||||
|
LOCALES.lookup(&get_sys_locale(), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
=== ./src/i18n/loader.rs ===
|
||||||
|
|
||||||
|
use anyhow::Ok;
|
||||||
|
use fluent_templates::static_loader;
|
||||||
|
|
||||||
|
static_loader! {
|
||||||
|
pub static LOCALES = {
|
||||||
|
locales: "./src/locales",
|
||||||
|
fallback_language: "pt-BR",
|
||||||
|
customise: |bundle| bundle.set_use_isolating(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_locales() -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
=== ./src/i18n/mod.rs ===
|
||||||
|
|
||||||
|
pub mod loader;
|
||||||
|
pub mod translate;
|
||||||
|
|
||||||
|
pub use loader::init_locales;
|
||||||
|
pub use translate::t;
|
||||||
|
|
||||||
|
=== ./Cargo.toml ===
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "uptime-kuma-dashboard"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
fluent-templates = "0.13.2"
|
||||||
|
unic-langid = "0.9.6"
|
||||||
|
sys-locale = "0.3.2"
|
||||||
4
src/heartbeat/mod.rs
Normal file
4
src/heartbeat/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod parser;
|
||||||
|
pub mod model;
|
||||||
|
pub use parser::{format_duration, parse_response,status_to_string};
|
||||||
|
pub use model::HeartbeatResponse;
|
||||||
103
src/heartbeat/model.rs
Normal file
103
src/heartbeat/model.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct HeartbeatEntry {
|
||||||
|
pub status: u8,
|
||||||
|
pub time: String,
|
||||||
|
pub msg: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ping: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct MonitorHeartbeats {
|
||||||
|
pub monitor_id: u64,
|
||||||
|
pub heartbeats: Vec<HeartbeatEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct UptimeData {
|
||||||
|
pub monitor_id: u64,
|
||||||
|
pub period_hours: u32,
|
||||||
|
pub uptime_percentage: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UptimeData {
|
||||||
|
pub fn get_perc_formated(&self) -> String {
|
||||||
|
format!("{:.2}%", self.uptime_percentage * 100.00)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct HeartbeatResponse {
|
||||||
|
#[serde(rename = "heartbeatList")]
|
||||||
|
pub heartbeat_list_raw: HashMap<String, Vec<HeartbeatEntry>>,
|
||||||
|
|
||||||
|
#[serde(rename = "uptimeList")]
|
||||||
|
pub uptime_list_raw: HashMap<String, f64>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub monitors: Vec<MonitorHeartbeats>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub uptime_data: Vec<UptimeData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeartbeatResponse {
|
||||||
|
pub fn process(&mut self) -> Result<()> {
|
||||||
|
for (monitor_id_str, heartbeats) in &self.heartbeat_list_raw {
|
||||||
|
let monitor_id = monitor_id_str
|
||||||
|
.parse::<u64>()
|
||||||
|
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
|
||||||
|
|
||||||
|
self.monitors.push(MonitorHeartbeats {
|
||||||
|
monitor_id,
|
||||||
|
heartbeats: heartbeats.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, percentage) in &self.uptime_list_raw {
|
||||||
|
let parts: Vec<&str> = key.split('_').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(anyhow::anyhow!("Formato invalido (colocar o t) {}", key));
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitor_id = parts[0]
|
||||||
|
.parse::<u64>()
|
||||||
|
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
|
||||||
|
|
||||||
|
let period_hours = parts[1]
|
||||||
|
.parse::<u32>()
|
||||||
|
.with_context(|| format!("{}", t("invalid-period-hours")))?;
|
||||||
|
|
||||||
|
self.uptime_data.push(UptimeData {
|
||||||
|
monitor_id,
|
||||||
|
period_hours,
|
||||||
|
uptime_percentage: *percentage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.monitors.sort_by_key(|m| m.monitor_id);
|
||||||
|
self.uptime_data
|
||||||
|
.sort_by_key(|u| (u.monitor_id, u.period_hours));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_uptime(&self, monitor_id: u64, period_hours: u32) -> Option<&UptimeData> {
|
||||||
|
self.uptime_data
|
||||||
|
.iter()
|
||||||
|
.find(|u| u.monitor_id == monitor_id && u.period_hours == period_hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_heartbeat(&self, monitor_id: u64) -> Option<&HeartbeatEntry> {
|
||||||
|
self.monitors
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.monitor_id == monitor_id)
|
||||||
|
.and_then(|m| m.heartbeats.last())
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/heartbeat/parser.rs
Normal file
28
src/heartbeat/parser.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
use anyhow::{Context, Ok, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::i18n::t;
|
||||||
|
use super::HeartbeatResponse;
|
||||||
|
|
||||||
|
pub fn parse_response(json_text: &str) -> Result<HeartbeatResponse> {
|
||||||
|
let mut response: HeartbeatResponse = serde_json::from_str(json_text)
|
||||||
|
.with_context(|| t("invalid-json-heartbeat"))?;
|
||||||
|
|
||||||
|
response.process()?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_to_string(status: u8) -> String {
|
||||||
|
match status {
|
||||||
|
1 => t("up"),
|
||||||
|
2 => t("down"),
|
||||||
|
_ => t("unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_duration(duration_ms:Option<u64>) -> String {
|
||||||
|
match duration_ms {
|
||||||
|
Some(ms) if ms < 1000 => format!("{}ms", ms),
|
||||||
|
Some(ms) => format!("{:.1}s", ms),
|
||||||
|
None => "N/A".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/i18n/loader.rs
Normal file
14
src/i18n/loader.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use anyhow::Ok;
|
||||||
|
use fluent_templates::static_loader;
|
||||||
|
|
||||||
|
static_loader! {
|
||||||
|
pub static LOCALES = {
|
||||||
|
locales: "./src/locales",
|
||||||
|
fallback_language: "pt-BR",
|
||||||
|
customise: |bundle| bundle.set_use_isolating(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_locales() -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
5
src/i18n/mod.rs
Normal file
5
src/i18n/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod loader;
|
||||||
|
pub mod translate;
|
||||||
|
|
||||||
|
pub use loader::init_locales;
|
||||||
|
pub use translate::t;
|
||||||
13
src/i18n/translate.rs
Normal file
13
src/i18n/translate.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::i18n::loader::LOCALES;
|
||||||
|
use fluent_templates::Loader;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
fn get_sys_locale() -> LanguageIdentifier {
|
||||||
|
let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR"));
|
||||||
|
LanguageIdentifier::from_str(&sys_lang).expect("Invalid language")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn t(key: &str) -> String {
|
||||||
|
LOCALES.lookup(&get_sys_locale(), key)
|
||||||
|
}
|
||||||
60
src/main.rs
60
src/main.rs
|
|
@ -1,58 +1,42 @@
|
||||||
use std::str::FromStr;
|
use anyhow::Result;
|
||||||
|
|
||||||
use anyhow::{Ok, Result};
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use fluent_templates::{Loader, static_loader};
|
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use unic_langid::LanguageIdentifier;
|
use std::result::Result::Ok;
|
||||||
|
mod i18n;
|
||||||
static_loader! {
|
use i18n::{init_locales, t};
|
||||||
static LOCALES = {
|
mod heartbeat;
|
||||||
locales: "./src/locales",
|
|
||||||
fallback_language: "pt-BR",
|
|
||||||
customise: |bundle| bundle.set_use_isolating(false),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(author, version, about)]
|
#[command(author, version, about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short, long, env = "UPTIME_KUMA_URL")]
|
#[arg(short, long, env = "UPTIME_KUMA_URL")]
|
||||||
url: Option<String>,
|
base_url: String,
|
||||||
|
|
||||||
#[arg(short, long, env = "UPTIME_KUMA_API_KEY")]
|
#[arg(short, long, env = "STATUS_PAGE_SLUG")]
|
||||||
api_key: Option<String>,
|
slug: String,
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sys_locale() -> LanguageIdentifier {
|
|
||||||
let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR"));
|
|
||||||
LanguageIdentifier::from_str(&sys_lang).expect("Invalid language")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn t(key: &str) -> String {
|
|
||||||
LOCALES.lookup(&get_sys_locale(), key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
init_locales()?;
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let url = args
|
|
||||||
.url
|
|
||||||
.as_deref()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("{}", t("missing_url")))?;
|
|
||||||
let api_key = args
|
|
||||||
.api_key
|
|
||||||
.as_deref()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("{}", t("missing_api_key")))?;
|
|
||||||
|
|
||||||
|
let heartbeat_url = format!(
|
||||||
|
"{}/api/status-page/heartbeat/{}",
|
||||||
|
args.base_url.trim_end_matches("/"),
|
||||||
|
args.slug
|
||||||
|
);
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let response = client.get(url).basic_auth("", Some(api_key)).send()?;
|
println!("{}", heartbeat_url);
|
||||||
|
|
||||||
|
let response = client.get(heartbeat_url).send()?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let metrics = response.text()?;
|
let json_text = response.text()?;
|
||||||
println!("{}", t("success"));
|
match heartbeat::parse_response(&json_text) {
|
||||||
println!("{}", t("metrics_preview"));
|
Ok(data) => println!("moises: {}", data.uptime_data[0].get_perc_formated()),
|
||||||
println!("{}", &metrics[..200.min(metrics.len())]);
|
Err(e) => println!("{}", e),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("{}", response.status());
|
println!("{}", response.status());
|
||||||
println!("{}: {}", t("Response"), response.text()?);
|
println!("{}: {}", t("Response"), response.text()?);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue