diff --git a/Cargo.toml b/Cargo.toml index e6ec7b14..0cf846e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" [dependencies] anyhow = "1.0.89" +askama = "0.12.1" +axum = "0.7.7" +gethostname = "0.5.0" log = "0.4.22" prost = "0.13.3" pyo3 = {version="0.22.3", features = ["extension-module"]} diff --git a/src/lighthouse.rs b/src/lighthouse.rs index efc0d565..d3f67c2f 100644 --- a/src/lighthouse.rs +++ b/src/lighthouse.rs @@ -4,18 +4,23 @@ // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. +use core::net::SocketAddr; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use std::time::Instant; use anyhow::Result; +use askama::Template; +use axum::{response::Html, routing::get, Router}; +use gethostname::gethostname; use log::{error, info}; use structopt::StructOpt; use tokio::sync::broadcast; use tokio::sync::Mutex; use tokio::task::JoinSet; use tokio::time::sleep; +use tonic::service::Routes; use tonic::transport::Server; use tonic::{Request, Response, Status}; @@ -191,11 +196,33 @@ impl Lighthouse { } async fn _run_grpc(self: Arc) -> Result<()> { - let bind = self.opt.bind.parse()?; - info!("Lighthouse listening on {}", bind); + let bind: SocketAddr = self.opt.bind.parse()?; + info!( + "Lighthouse listening on: http://{}:{}", + gethostname().into_string().unwrap(), + bind.port() + ); + + let self_clone = self.clone(); + + // Setup HTTP endpoints + let app = Router::new() + .route( + "/", + get(|| async { Html(IndexTemplate {}.render().unwrap()) }), + ) + .route( + "/status", + get(move || async { self_clone.get_status().await }), + ); + + // register the GRPC service + let routes = Routes::from(app).add_service(LighthouseServiceServer::new(self)); Server::builder() - .add_service(LighthouseServiceServer::new(self)) + // allow non-GRPC connections + .accept_http1(true) + .add_routes(routes) .serve(bind) .await .map_err(|e| e.into()) @@ -213,6 +240,19 @@ impl Lighthouse { } Ok(()) } + + async fn get_status(self: Arc) -> Html { + let template = { + let state = self.state.lock().await; + + StatusTemplate { + quorum_id: state.quorum_id, + prev_quorum: state.prev_quorum.clone(), + heartbeats: state.heartbeats.clone(), + } + }; + Html(template.render().unwrap()) + } } #[tonic::async_trait] @@ -271,6 +311,18 @@ impl LighthouseService for Arc { } } +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate {} + +#[derive(Template)] +#[template(path = "status.html")] +struct StatusTemplate { + prev_quorum: Option, + quorum_id: i64, + heartbeats: HashMap, +} + #[cfg(test)] mod tests { use super::*; diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..dcd601ae --- /dev/null +++ b/templates/index.html @@ -0,0 +1,57 @@ + + Lighthouse Dashboard - torchft + + + + + +
+

Lighthouse Dashboard - torchft

+ +
+ +
+ Loading... +
diff --git a/templates/status.html b/templates/status.html new file mode 100644 index 00000000..080e629e --- /dev/null +++ b/templates/status.html @@ -0,0 +1,38 @@ +

Quorum Status

+ +Current quorum_id: {{quorum_id}} + +

Previous Quorum

+{% if let Some(prev_quorum) = prev_quorum %} + +Previous quorum id: {{prev_quorum.quorum_id}} + +
+{% for member in prev_quorum.participants %} + +
+ {{ member.replica_id }}
+ Step: {{ member.step }}
+ Manager: {{ member.address }}
+ TCPStore: {{ member.store_address }} +
+ +{% endfor %} +
+ +{% endif %} + +

Heartbeats

+ +
    +{% for replica_id in heartbeats.keys() %} + + {% let age = heartbeats[replica_id].elapsed().as_secs_f64() %} +
  • + {{ replica_id }}: seen {{ age }}s ago +
  • + +{% endfor %} +
+ +