Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/network-monitor/assets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -643,8 +643,8 @@ function updateDisplay() {
</div>
` : ''}
${details.RemoteProverStatus?.test ? (() => {
const t = details.RemoteProverStatus.test;
const ts = details.RemoteProverStatus.test_status;
const t = details.RemoteProverStatus.test.details;
const ts = details.RemoteProverStatus.test.status;
return `
<div class="nested-status">
<strong>Proof Generation Testing (${t.proof_type}):</strong>
Expand Down
34 changes: 26 additions & 8 deletions bin/network-monitor/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

use anyhow::Result;
use miden_node_utils::logging::OpenTelemetry;
use tokio::sync::watch;
use tracing::{debug, info, instrument, warn};

use crate::COMPONENT;
use crate::config::MonitorConfig;
use crate::frontend::ServerState;
use crate::monitor::tasks::Tasks;
use crate::status::ServiceStatus;

/// Start the network monitoring service.
///
Expand Down Expand Up @@ -92,15 +94,31 @@ pub async fn start_monitor(config: MonitorConfig) -> Result<()> {

// Initialize HTTP server.
debug!(target: COMPONENT, "Initializing HTTP server");

// Build the flat services Vec in the order the dashboard expects to render cards.
let mut services: Vec<watch::Receiver<ServiceStatus>> = vec![rpc_rx];
if let Some(rx) = faucet_rx {
services.push(rx);
}
services.extend(prover_rxs);
if let Some(rx) = explorer_rx {
services.push(rx);
}
if let Some(rx) = ntx_increment_rx {
services.push(rx);
}
if let Some(rx) = ntx_tracking_rx {
services.push(rx);
}
if let Some(rx) = note_transport_rx {
services.push(rx);
}
if let Some(rx) = validator_rx {
services.push(rx);
}

let server_state = ServerState {
rpc: rpc_rx,
provers: prover_rxs,
faucet: faucet_rx,
ntx_increment: ntx_increment_rx,
ntx_tracking: ntx_tracking_rx,
explorer: explorer_rx,
note_transport: note_transport_rx,
validator: validator_rx,
services,
monitor_version: env!("CARGO_PKG_VERSION").to_string(),
network_name: config.network_name.clone(),
};
Expand Down
115 changes: 13 additions & 102 deletions bin/network-monitor/src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,19 @@ use tracing::{info, instrument};

use crate::COMPONENT;
use crate::config::MonitorConfig;
use crate::status::{NetworkStatus, RemoteProverDetails, ServiceDetails, ServiceStatus, Status};
use crate::status::{NetworkStatus, ServiceStatus};

// SERVER STATE
// ================================================================================================

/// State for the web server containing watch receivers for all services.
///
/// Each entry in `services` is a `ServiceStatus` channel. The frontend simply snapshots every
/// entry on each `/status` request. Adding a new service is just pushing another receiver into
/// this Vec at startup; no changes to this struct or `get_status` are required.
#[derive(Clone)]
pub struct ServerState {
pub rpc: watch::Receiver<ServiceStatus>,
pub provers: Vec<(watch::Receiver<ServiceStatus>, watch::Receiver<ServiceStatus>)>,
pub faucet: Option<watch::Receiver<ServiceStatus>>,
pub ntx_increment: Option<watch::Receiver<ServiceStatus>>,
pub ntx_tracking: Option<watch::Receiver<ServiceStatus>>,
pub explorer: Option<watch::Receiver<ServiceStatus>>,
pub note_transport: Option<watch::Receiver<ServiceStatus>>,
pub validator: Option<watch::Receiver<ServiceStatus>>,
pub services: Vec<watch::Receiver<ServiceStatus>>,
pub monitor_version: String,
pub network_name: String,
}
Expand Down Expand Up @@ -71,59 +68,20 @@ async fn get_dashboard() -> Html<&'static str> {
async fn get_status(
axum::extract::State(server_state): axum::extract::State<ServerState>,
) -> axum::response::Json<NetworkStatus> {
let current_time = SystemTime::now()
let services: Vec<ServiceStatus> =
server_state.services.iter().map(|rx| rx.borrow().clone()).collect();

let last_updated = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();

let mut services = Vec::new();

// Collect RPC status
services.push(server_state.rpc.borrow().clone());

// Collect faucet status if available
if let Some(faucet_rx) = &server_state.faucet {
services.push(faucet_rx.borrow().clone());
}

// Collect all remote prover statuses, merging status + test into a single entry per prover.
for (prover_status_rx, prover_test_rx) in &server_state.provers {
services.push(merge_prover(&prover_status_rx.borrow(), &prover_test_rx.borrow()));
}

// Collect explorer status if available
if let Some(explorer_rx) = &server_state.explorer {
services.push(explorer_rx.borrow().clone());
}

// Collect counter increment status if enabled
if let Some(ntx_increment_rx) = &server_state.ntx_increment {
services.push(ntx_increment_rx.borrow().clone());
}

// Collect counter tracking status if enabled
if let Some(ntx_tracking_rx) = &server_state.ntx_tracking {
services.push(ntx_tracking_rx.borrow().clone());
}

// Collect note transport status if available
if let Some(note_transport_rx) = &server_state.note_transport {
services.push(note_transport_rx.borrow().clone());
}

// Collect validator status if available
if let Some(validator_rx) = &server_state.validator {
services.push(validator_rx.borrow().clone());
}

let network_status = NetworkStatus {
axum::response::Json(NetworkStatus {
services,
last_updated: current_time,
last_updated,
monitor_version: server_state.monitor_version.clone(),
network_name: server_state.network_name.clone(),
};

axum::response::Json(network_status)
})
}

async fn serve_css() -> Response {
Expand All @@ -149,50 +107,3 @@ async fn serve_favicon() -> Response {
)
.into_response()
}

/// Merges the status and test receivers for a single remote prover into one `ServiceStatus`.
///
/// The combined status is `Unhealthy` if either the status check or the test failed, `Unknown`
/// if the status checker has not yet seen the prover, and `Healthy` otherwise. The test result
/// is only attached when the test task has produced an actual `RemoteProverTest` result (before
/// the first test completes, the test channel holds the initial prover status and should not be
/// surfaced as a test).
fn merge_prover(status: &ServiceStatus, test: &ServiceStatus) -> ServiceStatus {
// Extract prover status details, or pass through the raw status if the prover is down
// (details will be `ServiceDetails::Error` in that case).
let status_details = match &status.details {
ServiceDetails::ProverStatusCheck(d) => d.clone(),
_ => return status.clone(),
};

// Only attach test details once the test task has produced a real result.
let (test_details, test_status, test_error) = match &test.details {
ServiceDetails::ProverTestResult(d) => {
(Some(d.clone()), Some(test.status.clone()), test.error.clone())
},
_ => (None, None, None),
};

let details = ServiceDetails::RemoteProverStatus(RemoteProverDetails {
status: status_details,
test: test_details,
test_status: test_status.clone(),
test_error: test_error.clone(),
});

let name = &status.name;
let base = match (&status.status, &test_status) {
(Status::Unhealthy, _) | (_, Some(Status::Unhealthy)) => {
let error = status
.error
.clone()
.or(test_error)
.unwrap_or_else(|| "prover is unhealthy".to_string());
ServiceStatus::unhealthy(name, error, details)
},
(Status::Unknown, _) => ServiceStatus::unknown(name, details),
_ => ServiceStatus::healthy(name, details),
};

base.with_last_checked(status.last_checked)
}
Loading
Loading