///|
/// Operational state used by a network health dashboard.
pub enum NetworkNodeHealthState {
NetworkNodeUnknown
NetworkNodeHealthy
NetworkNodeDegraded
NetworkNodeOffline
NetworkNodeMaintenance
}
///|
pub fn network_node_health_state_variants() -> Array[NetworkNodeHealthState] {
[
NetworkNodeUnknown,
NetworkNodeHealthy,
NetworkNodeDegraded,
NetworkNodeOffline,
NetworkNodeMaintenance,
]
}
///|
pub fn network_node_health_state_text(state : NetworkNodeHealthState) -> String {
match state {
NetworkNodeUnknown => "unknown"
NetworkNodeHealthy => "healthy"
NetworkNodeDegraded => "degraded"
NetworkNodeOffline => "offline"
NetworkNodeMaintenance => "maintenance"
}
}
///|
/// A node health sample with counters and timing.
pub struct NetworkNodeHealth {
node_id : Byte
name : String
mut state : NetworkNodeHealthState
mut last_seen_us : UInt64
timeout_us : UInt64
mut received : Int
mut transmitted : Int
mut errors : Int
mut missed_heartbeats : Int
latency_samples : Array[UInt64]
}
///|
pub fn network_node_health(
node_id : Byte,
name : String,
timeout_us? : UInt64 = 1_000_000,
) -> NetworkNodeHealth {
{
node_id,
name,
state: NetworkNodeUnknown,
last_seen_us: 0,
timeout_us,
received: 0,
transmitted: 0,
errors: 0,
missed_heartbeats: 0,
latency_samples: [],
}
}
///|
pub fn NetworkNodeHealth::node_id(self : NetworkNodeHealth) -> Byte {
self.node_id
}
///|
pub fn NetworkNodeHealth::name(self : NetworkNodeHealth) -> String {
self.name
}
///|
pub fn NetworkNodeHealth::state(
self : NetworkNodeHealth,
) -> NetworkNodeHealthState {
self.state
}
///|
pub fn NetworkNodeHealth::last_seen_us(self : NetworkNodeHealth) -> UInt64 {
self.last_seen_us
}
///|
pub fn NetworkNodeHealth::timeout_us(self : NetworkNodeHealth) -> UInt64 {
self.timeout_us
}
///|
pub fn NetworkNodeHealth::received(self : NetworkNodeHealth) -> Int {
self.received
}
///|
pub fn NetworkNodeHealth::transmitted(self : NetworkNodeHealth) -> Int {
self.transmitted
}
///|
pub fn NetworkNodeHealth::errors(self : NetworkNodeHealth) -> Int {
self.errors
}
///|
pub fn NetworkNodeHealth::missed_heartbeats(self : NetworkNodeHealth) -> Int {
self.missed_heartbeats
}
///|
pub fn NetworkNodeHealth::latency_samples(
self : NetworkNodeHealth,
) -> Array[UInt64] {
self.latency_samples.copy()
}
///|
pub fn NetworkNodeHealth::online(self : NetworkNodeHealth) -> Bool {
match self.state {
NetworkNodeOffline => false
_ => true
}
}
///|
pub fn NetworkNodeHealth::observe_frame(
self : NetworkNodeHealth,
timestamp_us : UInt64,
transmit : Bool,
) -> Unit {
self.last_seen_us = timestamp_us
self.state = NetworkNodeHealthy
if transmit {
self.transmitted += 1
} else {
self.received += 1
}
}
///|
pub fn NetworkNodeHealth::observe_heartbeat(
self : NetworkNodeHealth,
timestamp_us : UInt64,
latency_us? : UInt64 = 0,
) -> Unit {
self.last_seen_us = timestamp_us
self.state = NetworkNodeHealthy
if latency_us > 0 {
self.latency_samples.push(latency_us)
}
}
///|
pub fn NetworkNodeHealth::observe_error(self : NetworkNodeHealth) -> Unit {
self.errors += 1
if self.errors > 3 {
self.state = NetworkNodeDegraded
}
}
///|
pub fn NetworkNodeHealth::poll(
self : NetworkNodeHealth,
timestamp_us : UInt64,
) -> NetworkNodeHealthState {
if timestamp_us > self.last_seen_us + self.timeout_us {
self.missed_heartbeats += 1
self.state = NetworkNodeOffline
} else if self.errors > 0 &&
self.errors * 100 > self.received + self.transmitted + 1 {
self.state = NetworkNodeDegraded
}
self.state
}
///|
pub fn NetworkNodeHealth::average_latency_us(
self : NetworkNodeHealth,
) -> UInt64 {
if self.latency_samples.is_empty() {
0
} else {
let mut sum : UInt64 = 0
for sample in self.latency_samples {
sum += sample
}
sum / self.latency_samples.length().to_uint64()
}
}
///|
pub fn NetworkNodeHealth::p95_latency_us(self : NetworkNodeHealth) -> UInt64 {
if self.latency_samples.is_empty() {
0
} else {
let values = self.latency_samples.copy()
values.sort()
values[(values.length() - 1) * 95 / 100]
}
}
///|
pub fn NetworkNodeHealth::to_text(self : NetworkNodeHealth) -> String {
self.name +
" node=" +
self.node_id.to_string() +
" state=" +
network_node_health_state_text(self.state) +
" rx=" +
self.received.to_string() +
" tx=" +
self.transmitted.to_string() +
" errors=" +
self.errors.to_string()
}
///|
/// An error budget for a production CAN network.
pub struct NetworkErrorBudget {
max_error_rate : Double
max_drop_rate : Double
max_offline_nodes : Int
mut total_frames : Int
mut error_frames : Int
mut dropped_frames : Int
mut offline_nodes : Int
}
///|
pub fn network_error_budget(
max_error_rate? : Double = 0.01,
max_drop_rate? : Double = 0.01,
max_offline_nodes? : Int = 0,
) -> NetworkErrorBudget {
{
max_error_rate,
max_drop_rate,
max_offline_nodes,
total_frames: 0,
error_frames: 0,
dropped_frames: 0,
offline_nodes: 0,
}
}
///|
pub fn NetworkErrorBudget::record_frame(
self : NetworkErrorBudget,
error : Bool,
dropped : Bool,
) -> Unit {
self.total_frames += 1
if error {
self.error_frames += 1
}
if dropped {
self.dropped_frames += 1
}
}
///|
pub fn NetworkErrorBudget::set_offline_nodes(
self : NetworkErrorBudget,
count : Int,
) -> Unit {
self.offline_nodes = count
}
///|
pub fn NetworkErrorBudget::error_rate(self : NetworkErrorBudget) -> Double {
if self.total_frames == 0 {
0.0
} else {
self.error_frames.to_double() / self.total_frames.to_double()
}
}
///|
pub fn NetworkErrorBudget::drop_rate(self : NetworkErrorBudget) -> Double {
if self.total_frames == 0 {
0.0
} else {
self.dropped_frames.to_double() / self.total_frames.to_double()
}
}
///|
pub fn NetworkErrorBudget::within_budget(self : NetworkErrorBudget) -> Bool {
self.error_rate() <= self.max_error_rate &&
self.drop_rate() <= self.max_drop_rate &&
self.offline_nodes <= self.max_offline_nodes
}
///|
pub fn NetworkErrorBudget::total_frames(self : NetworkErrorBudget) -> Int {
self.total_frames
}
///|
pub fn NetworkErrorBudget::error_frames(self : NetworkErrorBudget) -> Int {
self.error_frames
}
///|
pub fn NetworkErrorBudget::dropped_frames(self : NetworkErrorBudget) -> Int {
self.dropped_frames
}
///|
pub fn NetworkErrorBudget::offline_nodes(self : NetworkErrorBudget) -> Int {
self.offline_nodes
}
///|
pub fn NetworkErrorBudget::max_error_rate(self : NetworkErrorBudget) -> Double {
self.max_error_rate
}
///|
pub fn NetworkErrorBudget::max_drop_rate(self : NetworkErrorBudget) -> Double {
self.max_drop_rate
}
///|
/// A summary of network health at one observation point.
pub struct NetworkHealthReport {
timestamp_us : UInt64
nodes : Array[NetworkNodeHealth]
online : Int
degraded : Int
offline : Int
budget : NetworkErrorBudget
}
///|
pub fn NetworkHealthReport::timestamp_us(self : NetworkHealthReport) -> UInt64 {
self.timestamp_us
}
///|
pub fn NetworkHealthReport::nodes(
self : NetworkHealthReport,
) -> Array[NetworkNodeHealth] {
self.nodes.copy()
}
///|
pub fn NetworkHealthReport::online(self : NetworkHealthReport) -> Int {
self.online
}
///|
pub fn NetworkHealthReport::degraded(self : NetworkHealthReport) -> Int {
self.degraded
}
///|
pub fn NetworkHealthReport::offline(self : NetworkHealthReport) -> Int {
self.offline
}
///|
pub fn NetworkHealthReport::budget(
self : NetworkHealthReport,
) -> NetworkErrorBudget {
self.budget
}
///|
pub fn NetworkHealthReport::healthy(self : NetworkHealthReport) -> Bool {
self.offline == 0 && self.degraded == 0 && self.budget.within_budget()
}
///|
pub fn NetworkHealthReport::to_text(self : NetworkHealthReport) -> String {
"t=" +
self.timestamp_us.to_string() +
" online=" +
self.online.to_string() +
" degraded=" +
self.degraded.to_string() +
" offline=" +
self.offline.to_string() +
" budget=" +
self.budget.within_budget().to_string()
}
///|
/// A fleet-level health tracker for virtual or hardware-connected nodes.
pub struct NetworkHealthTracker {
nodes : Array[NetworkNodeHealth]
budget : NetworkErrorBudget
mut samples : Int
mut last_timestamp_us : UInt64
}
///|
pub fn new_network_health_tracker(
budget? : NetworkErrorBudget = network_error_budget(),
) -> NetworkHealthTracker {
{ nodes: [], budget, samples: 0, last_timestamp_us: 0 }
}
///|
pub fn NetworkHealthTracker::add_node(
self : NetworkHealthTracker,
node : NetworkNodeHealth,
) -> Bool {
if self.find(node.node_id()) is Some(_) {
false
} else {
self.nodes.push(node)
true
}
}
///|
pub fn NetworkHealthTracker::find(
self : NetworkHealthTracker,
node_id : Byte,
) -> NetworkNodeHealth? {
for node in self.nodes {
if node.node_id() == node_id {
return Some(node)
}
}
None
}
///|
pub fn NetworkHealthTracker::observe_frame(
self : NetworkHealthTracker,
node_id : Byte,
timestamp_us : UInt64,
frame : Frame,
transmit : Bool,
) -> Bool {
match self.find_index(node_id) {
Some(index) => {
self.nodes[index].observe_frame(timestamp_us, transmit)
self.budget.record_frame(frame.is_error(), false)
self.last_timestamp_us = timestamp_us
true
}
None => false
}
}
///|
pub fn NetworkHealthTracker::observe_heartbeat(
self : NetworkHealthTracker,
node_id : Byte,
timestamp_us : UInt64,
latency_us? : UInt64 = 0,
) -> Bool {
match self.find_index(node_id) {
Some(index) => {
self.nodes[index].observe_heartbeat(timestamp_us, latency_us~)
true
}
None => false
}
}
///|
pub fn NetworkHealthTracker::observe_drop(self : NetworkHealthTracker) -> Unit {
self.budget.record_frame(false, true)
}
///|
pub fn NetworkHealthTracker::observe_error(
self : NetworkHealthTracker,
node_id : Byte,
) -> Bool {
match self.find_index(node_id) {
Some(index) => {
self.nodes[index].observe_error()
self.budget.record_frame(true, false)
true
}
None => false
}
}
///|
pub fn NetworkHealthTracker::poll(
self : NetworkHealthTracker,
timestamp_us : UInt64,
) -> NetworkHealthReport {
let mut online = 0
let mut degraded = 0
let mut offline = 0
for node in self.nodes {
match node.poll(timestamp_us) {
NetworkNodeHealthy => online += 1
NetworkNodeDegraded => degraded += 1
NetworkNodeOffline => offline += 1
_ => ()
}
}
self.budget.set_offline_nodes(offline)
self.samples += 1
self.last_timestamp_us = timestamp_us
{
timestamp_us,
nodes: self.nodes.copy(),
online,
degraded,
offline,
budget: self.budget,
}
}
///|
pub fn NetworkHealthTracker::nodes(
self : NetworkHealthTracker,
) -> Array[NetworkNodeHealth] {
self.nodes.copy()
}
///|
pub fn NetworkHealthTracker::budget(
self : NetworkHealthTracker,
) -> NetworkErrorBudget {
self.budget
}
///|
pub fn NetworkHealthTracker::samples(self : NetworkHealthTracker) -> Int {
self.samples
}
///|
pub fn NetworkHealthTracker::last_timestamp_us(
self : NetworkHealthTracker,
) -> UInt64 {
self.last_timestamp_us
}
///|
pub fn NetworkHealthTracker::reset(self : NetworkHealthTracker) -> Unit {
self.samples = 0
self.last_timestamp_us = 0
for node in self.nodes {
node.state = NetworkNodeUnknown
node.received = 0
node.transmitted = 0
node.errors = 0
node.missed_heartbeats = 0
node.latency_samples.clear()
}
}
///|
fn NetworkHealthTracker::find_index(
self : NetworkHealthTracker,
node_id : Byte,
) -> Int? {
for index, node in self.nodes {
if node.node_id() == node_id {
return Some(index)
}
}
None
}