//! Dashboard view over MVP cluster provisioning and lifecycle datastream records. use std::collections::{BTreeMap, VecDeque}; use dashboard::FrameEvent; use dashboard::view::DashboardView; use datastream::Record; use datastream::frame::{Frame, StreamId}; use serde::Serialize; use serde_json::{Value, json}; use std::sync::RwLock; use crate::observability_surface as obs; use crate::provisioning::{ProvisionEventKind, ProvisionLogStream}; use crate::telemetry::{ MVP_LIFECYCLE, MVP_PROVISIONING_EVENTS, MVP_PROVISIONING_LOGS, MvpLifecycleRecord, MvpProvisionEventRecord, MvpProvisionLogRecord, }; const CHANNELS: &[&str] = &[ MVP_LIFECYCLE, MVP_PROVISIONING_EVENTS, MVP_PROVISIONING_LOGS, ]; const EVENT_LOG_CAP: usize = 256; const LOG_TAIL_CAP: usize = 128; #[derive(Default)] pub struct MvpClusterDashboardView { state: RwLock, } impl MvpClusterDashboardView { pub fn new() -> Self { Self::default() } } #[derive(Default, Serialize)] struct MvpClusterDashboardState { events: VecDeque, nodes: BTreeMap, } #[derive(Clone, Serialize)] struct DashboardEventEntry { channel: String, position: u64, label: String, run_id: Option, node_id: Option, } #[derive(Default, Serialize)] struct ProvisionNodeView { node_id: u64, phase: String, last_message: Option, stdout_tail: VecDeque, stderr_tail: VecDeque, provider_tail: VecDeque, } impl MvpClusterDashboardState { fn push_event(&mut self, entry: DashboardEventEntry) { if self.events.len() == EVENT_LOG_CAP { self.events.pop_front(); } self.events.push_back(entry); } fn apply_provision_event(&mut self, frame: &Frame, record: MvpProvisionEventRecord) { let phase = provision_kind_label(record.event.kind); let node = self .nodes .entry(record.event.node_id) .or_insert_with(|| ProvisionNodeView { node_id: record.event.node_id, ..ProvisionNodeView::default() }); node.phase = phase.to_owned(); node.last_message = record.event.message.clone(); self.push_event(DashboardEventEntry { channel: MVP_PROVISIONING_EVENTS.to_owned(), position: frame.position.0, label: phase.to_owned(), run_id: Some(record.event.run_id), node_id: Some(record.event.node_id), }); } fn apply_provision_log(&mut self, record: MvpProvisionLogRecord) { let node = self .nodes .entry(record.line.node_id) .or_insert_with(|| ProvisionNodeView { node_id: record.line.node_id, ..ProvisionNodeView::default() }); let tail = match record.line.stream { ProvisionLogStream::Stdout => &mut node.stdout_tail, ProvisionLogStream::Stderr => &mut node.stderr_tail, ProvisionLogStream::Provider => &mut node.provider_tail, }; if tail.len() == LOG_TAIL_CAP { tail.pop_front(); } tail.push_back(record.line.line); } fn apply_lifecycle(&mut self, frame: &Frame, record: MvpLifecycleRecord) { let (run_id, node_id) = lifecycle_ids(&record.event); self.push_event(DashboardEventEntry { channel: MVP_LIFECYCLE.to_owned(), position: frame.position.0, label: format!("{:?}", record.event.kind()), run_id, node_id, }); } } impl DashboardView for MvpClusterDashboardView { fn id(&self) -> &'static str { "mvp/cluster" } fn title(&self) -> &'static str { "MVP cluster" } fn channels(&self) -> &'static [&'static str] { CHANNELS } fn ingest(&self, _stream: &StreamId, frame: &Frame, _event: &FrameEvent) { let mut state = self.state.write().expect("MVP dashboard state poisoned"); match frame.channel.as_str() { MVP_PROVISIONING_EVENTS => { if let Ok(record) = MvpProvisionEventRecord::decode(&frame.payload) { state.apply_provision_event(frame, record); } } MVP_PROVISIONING_LOGS => { if let Ok(record) = MvpProvisionLogRecord::decode(&frame.payload) { state.apply_provision_log(record); } } MVP_LIFECYCLE => { if let Ok(record) = MvpLifecycleRecord::decode(&frame.payload) { state.apply_lifecycle(frame, record); } } _ => {} } } fn snapshot_json(&self) -> Value { let state = self.state.read().expect("MVP dashboard state poisoned"); json!({ "events": state.events, "nodes": state.nodes.values().collect::>(), }) } fn html(&self) -> Option<&'static str> { Some(MVP_CLUSTER_HTML) } } fn provision_kind_label(kind: ProvisionEventKind) -> &'static str { match kind { ProvisionEventKind::ProvisionStart => "provision_start", ProvisionEventKind::NodeLive => "node_live", ProvisionEventKind::ProvisionFailed => "provision_failed", ProvisionEventKind::NodeStopped => "node_stopped", } } fn lifecycle_ids(event: &obs::Event) -> (Option, Option) { match event { obs::Event::RunScoped { run_id, .. } | obs::Event::StageScoped { run_id, .. } => { (Some(run_id.0), None) } obs::Event::NodeScoped { node_id, .. } | obs::Event::NodeFaulted { node_id, .. } => { (None, Some(node_id.0)) } obs::Event::EdgeScoped { .. } | obs::Event::RingScoped { .. } | obs::Event::ObjectScoped { .. } | obs::Event::StepScoped { .. } | obs::Event::WorkerScoped { .. } => (None, None), } } const MVP_CLUSTER_HTML: &str = r#" MVP cluster

MVP cluster

loading…

Provisioned nodes

nodephasemessage

Event log

poschannelrunnodeevent

Selected node logs

select a node row
"#;