Skip to content

Commit

Permalink
Merge pull request #48 from elsirion/2024-10-health-check
Browse files Browse the repository at this point in the history
Add guardian health check
  • Loading branch information
elsirion authored Nov 3, 2024
2 parents 4d03895 + b9dcb19 commit 06077eb
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 63 deletions.
24 changes: 24 additions & 0 deletions fmo_api_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct FederationSummary {
pub deposits: Amount,
pub invite: String,
pub nostr_votes: FederationRating,
pub health: FederationHealth,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
Expand All @@ -38,3 +39,26 @@ pub struct FederationUtxo {
pub out_point: bitcoin::OutPoint,
pub amount: Amount,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianHealth {
pub avg_uptime: f32,
pub avg_latency: f32,
pub latest: Option<GuardianHealthLatest>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianHealthLatest {
pub block_height: u32,
pub block_outdated: bool,
pub session_count: u32,
pub session_outdated: bool,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FederationHealth {
Online,
Degraded,
Offline,
}
101 changes: 97 additions & 4 deletions fmo_frontend/src/components/federation/guardians.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
use fedimint_core::NumPeers;
use leptos::{component, view, IntoView};
use std::collections::BTreeMap;

use fedimint_core::config::FederationId;
use fedimint_core::util::backon::FibonacciBuilder;
use fedimint_core::util::retry;
use fedimint_core::{NumPeers, PeerId};
use fmo_api_types::GuardianHealth;
use leptos::{component, create_resource, view, IntoView, SignalGet};

use crate::BASE_URL;

#[component]
pub fn Guardians(guardians: Vec<Guardian>) -> impl IntoView {
pub fn Guardians(federation_id: FederationId, guardians: Vec<Guardian>) -> impl IntoView {
let n = guardians.len();
let t = NumPeers::from(n).threshold();

let health_resource = create_resource(
|| (),
move |()| async move { fetch_guardian_health(federation_id).await },
);

let guardians = guardians
.into_iter()
.map(|guardian| {
.enumerate()
.map(|(guardian_idx, guardian)| {
view! {
<li class="py-3 sm:py-4">
<div class="flex items-center">
Expand All @@ -19,6 +33,69 @@ pub fn Guardians(guardians: Vec<Guardian>) -> impl IntoView {
<p class="text-sm text-gray-500 truncate dark:text-gray-400">
{guardian.url}
</p>
<p>
{ move || match health_resource.get() {
Some(health) => {
let health = health.get(&PeerId::from(guardian_idx as u16)).expect("Guardian exists");

let mut badges = vec![];
if let Some(latest) = &health.latest {
badges.push(view! {
<span class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
Online
</span>
}.into_view());

if latest.session_outdated {
badges.push(view!{
<span class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">
<abbr title="Guardian is lacking behind others" >
"Session " { latest.session_count.to_string() }
</abbr>
</span>
}.into_view());
} else {
badges.push(view!{
<span class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
"Session " { latest.session_count.to_string() }
</span>
}.into_view());
}

if latest.block_outdated {
badges.push(view!{
<span class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">
<abbr title="Guardian's bitcoind is out of sync" >
"Block " { (latest.block_height - 1).to_string() }
</abbr>
</span>
}.into_view());
} else {
badges.push(view!{
<span class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
"Block " { (latest.block_height - 1).to_string() }
</span>
}.into_view());
}
} else {
badges.push(view! {
<span class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">
Offline
</span>
}.into_view());
}

badges.into_view()
}
None => {
view! {
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">
"Loading"
</span>
}.into_view()
}
}}
</p>
</div>
</div>
</li>
Expand Down Expand Up @@ -49,3 +126,19 @@ pub struct Guardian {
pub name: String,
pub url: String,
}

async fn fetch_guardian_health(id: FederationId) -> BTreeMap<PeerId, GuardianHealth> {
retry(
"fetching guardian health",
FibonacciBuilder::default().with_max_times(usize::MAX),
|| async move {
reqwest::get(format!("{}/federations/{}/health", BASE_URL, id))
.await?
.json::<BTreeMap<PeerId, GuardianHealth>>()
.await
.map_err(Into::into)
},
)
.await
.expect("Will never return Err")
}
16 changes: 9 additions & 7 deletions fmo_frontend/src/components/federation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ pub fn Federation() -> impl IntoView {
view! {
<div class="flex flex-wrap items-stretch gap-4 ">
<div class="flex-1 min-w-[400px]">
<Guardians guardians=config
.global
.api_endpoints.values().map(|guardian| Guardian {
name: guardian.name.clone(),
url: guardian.url.to_string(),
})
.collect()
<Guardians
federation_id=id().unwrap()
guardians=config
.global
.api_endpoints.values().map(|guardian| Guardian {
name: guardian.name.clone(),
url: guardian.url.to_string(),
})
.collect()
/>
</div>
<div class="flex-1 min-w-[400px]">
Expand Down
34 changes: 21 additions & 13 deletions fmo_frontend/src/components/federations/federation_row.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use std::str::FromStr;

use fedimint_core::config::FederationId;
use fedimint_core::Amount;
use fmo_api_types::FederationRating;
use fmo_api_types::{FederationHealth, FederationRating};
use leptos::{component, view, IntoView};

use crate::components::federations::rating::Rating;
Expand All @@ -18,16 +16,8 @@ pub fn FederationRow(
total_assets: Amount,
avg_txs: f64,
avg_volume: Amount,
health: FederationHealth,
) -> impl IntoView {
let degraded_federations = [FederationId::from_str(
"4b13a146ee4ba732b2b8914a72a0a2e5873e3e942da2d4eeefd85a5fe41f27ba",
)
.expect("can be parsed")];

if degraded_federations.contains(&id) {
return view! {}.into_view();
}

view! {
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th
Expand All @@ -48,7 +38,25 @@ pub fn FederationRow(
/>
</td>
<td class="px-6 py-4">
<Copyable text=invite/>
{ match health {
FederationHealth::Online => {
view! { <Copyable text=invite/> }.into_view()
},
FederationHealth::Degraded => {
view! {
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-yellow-300 border border-yellow-300">
"Degraded"
</span>
}.into_view()
}
FederationHealth::Offline => {
view! {
<span class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">
"Offline"
</span>
}.into_view()
},
}}
</td>
<td class="px-6 py-4">{total_assets.as_bitcoin(6).to_string()}</td>
<td class="px-6 py-4">
Expand Down
13 changes: 10 additions & 3 deletions fmo_frontend/src/components/federations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod rating;
mod totals;

use fedimint_core::Amount;
use fmo_api_types::FederationSummary;
use fmo_api_types::{FederationHealth, FederationSummary};
use leptos::{component, create_resource, view, IntoView, SignalGet};

use crate::components::federations::federation_row::FederationRow;
Expand Down Expand Up @@ -33,6 +33,7 @@ pub fn Federations() -> impl IntoView {
total_assets=summary.deposits
avg_txs=avg_txs
avg_volume=avg_volume
health=summary.health
/>
}
})
Expand Down Expand Up @@ -89,7 +90,13 @@ async fn fetch_federations() -> anyhow::Result<Vec<(FederationSummary, f64, Amou

let federations = federations
.into_iter()
.map(|federation_summary| {
.filter_map(|federation_summary| {
// Don't show offline federations for now. Eventually I'd like to only not show
// them if they have been offline for a long time.
if federation_summary.health == FederationHealth::Offline {
return None;
}

let avg_txs = federation_summary
.last_7d_activity
.iter()
Expand All @@ -104,7 +111,7 @@ async fn fetch_federations() -> anyhow::Result<Vec<(FederationSummary, f64, Amou
.sum::<u64>()
/ federation_summary.last_7d_activity.len() as u64,
);
(federation_summary, avg_txs, avg_volume)
Some((federation_summary, avg_txs, avg_volume))
})
.collect::<Vec<_>>();

Expand Down
Loading

0 comments on commit 06077eb

Please sign in to comment.