metrics.rs (8493B)
1 use crate::{ 2 adapters::ui_data_store::BlockMetricRow, 3 app::{PeerDirection, PeerInfo}, 4 domain::Amount, 5 }; 6 7 use super::{ 8 PEER_STALE_AFTER_MS, now_ms, 9 types::{ 10 MempoolCounts, MetricsChart, MetricsPoint, MetricsResponse, MetricsValueKind, 11 NetworkHealthLocalState, NetworkHealthResponse, 12 }, 13 }; 14 15 pub(super) fn network_health( 16 local: NetworkHealthLocalState, 17 peers: &[PeerInfo], 18 mempool: MempoolCounts, 19 ) -> NetworkHealthResponse { 20 network_health_at(local, peers, mempool, now_ms()) 21 } 22 23 pub(super) fn metrics_response(enabled: bool, rows: Vec<BlockMetricRow>) -> MetricsResponse { 24 let latest = rows.last().cloned(); 25 MetricsResponse { 26 enabled, 27 latest, 28 charts: vec![ 29 metrics_chart( 30 "block-time", 31 "Time per block", 32 "s", 33 MetricsValueKind::Seconds, 34 &rows, 35 |row| { 36 row.block_time_ms 37 .filter(|_| row.height > 1) 38 .map(|ms| ms as f64 / 1_000.0) 39 }, 40 ), 41 metrics_chart( 42 "difficulty", 43 "Difficulty", 44 "bits", 45 MetricsValueKind::Number, 46 &rows, 47 |row| Some(row.mine_difficulty_bits as f64), 48 ), 49 metrics_chart( 50 "supply", 51 "IUNA in circulation", 52 "IUNA", 53 MetricsValueKind::Iuna, 54 &rows, 55 |row| Some(micro_iuna_as_iuna(row.circulating_supply)), 56 ), 57 metrics_chart( 58 "known-wallet-addresses", 59 "Known wallet addresses", 60 "addresses", 61 MetricsValueKind::Number, 62 &rows, 63 |row| Some(row.known_wallet_addresses as f64), 64 ), 65 metrics_chart( 66 "transactions", 67 "Transactions", 68 "tx", 69 MetricsValueKind::Number, 70 &rows, 71 |row| Some(row.transaction_count as f64), 72 ), 73 metrics_chart( 74 "burn-count", 75 "Burn transactions", 76 "burns", 77 MetricsValueKind::Number, 78 &rows, 79 |row| Some(row.burn_count as f64), 80 ), 81 metrics_chart( 82 "burn-amount", 83 "Burn amount", 84 "IUNA", 85 MetricsValueKind::Iuna, 86 &rows, 87 |row| Some(micro_iuna_as_iuna(row.burned_amount)), 88 ), 89 metrics_chart( 90 "total-burn", 91 "Total burn", 92 "IUNA", 93 MetricsValueKind::Iuna, 94 &rows, 95 |row| Some(micro_iuna_as_iuna(row.total_burned_amount)), 96 ), 97 metrics_chart( 98 "fees", 99 "Fees", 100 "IUNA", 101 MetricsValueKind::Iuna, 102 &rows, 103 |row| Some(micro_iuna_as_iuna(row.fees_amount)), 104 ), 105 metrics_chart( 106 "mine-actions", 107 "Mine actions", 108 "mine", 109 MetricsValueKind::Number, 110 &rows, 111 |row| Some(row.mine_count as f64), 112 ), 113 metrics_chart( 114 "vdf-rounds", 115 "VDF rounds", 116 "rounds", 117 MetricsValueKind::Number, 118 &rows, 119 |row| (row.vdf_rounds > 0).then_some(row.vdf_rounds as f64), 120 ), 121 ], 122 } 123 } 124 125 fn metrics_chart( 126 id: &'static str, 127 title: &'static str, 128 unit: &'static str, 129 value_kind: MetricsValueKind, 130 rows: &[BlockMetricRow], 131 value: impl Fn(&BlockMetricRow) -> Option<f64>, 132 ) -> MetricsChart { 133 MetricsChart { 134 id, 135 title, 136 unit, 137 value_kind, 138 points: rows 139 .iter() 140 .filter_map(|row| { 141 value(row).map(|value| MetricsPoint { 142 height: row.height, 143 value, 144 }) 145 }) 146 .collect(), 147 } 148 } 149 150 fn micro_iuna_as_iuna(amount: Amount) -> f64 { 151 amount as f64 / 1_000_000.0 152 } 153 154 pub(super) fn network_health_at( 155 local: NetworkHealthLocalState, 156 peers: &[PeerInfo], 157 mempool: MempoolCounts, 158 now_ms: u64, 159 ) -> NetworkHealthResponse { 160 let local_height = local.height; 161 let remote_best_height = peers.iter().filter_map(|peer| peer.last_known_height).max(); 162 let best_known_height = remote_best_height.unwrap_or(local_height).max(local_height); 163 let healthy_heights = peers 164 .iter() 165 .filter(|peer| peer.last_error.is_none()) 166 .filter_map(|peer| peer.last_known_height) 167 .collect::<Vec<_>>(); 168 let shared_height = healthy_heights 169 .iter() 170 .copied() 171 .min() 172 .unwrap_or(local_height) 173 .min(local_height); 174 let outbound_peers = peers 175 .iter() 176 .filter(|peer| peer.direction != PeerDirection::Inbound) 177 .count(); 178 let inbound_peers = peers 179 .iter() 180 .filter(|peer| peer.direction == PeerDirection::Inbound) 181 .count(); 182 let healthy_peers = peers 183 .iter() 184 .filter(|peer| peer.last_error.is_none() && peer.last_known_height.is_some()) 185 .count(); 186 let failed_peers = peers 187 .iter() 188 .filter(|peer| peer.last_error.is_some()) 189 .count(); 190 let stale_peers = peers 191 .iter() 192 .filter(|peer| { 193 peer.last_success_ms.is_some_and(|last_success| { 194 now_ms.saturating_sub(last_success) > PEER_STALE_AFTER_MS 195 }) 196 }) 197 .count(); 198 let banned_peers = peers 199 .iter() 200 .filter(|peer| peer.is_banned_at(now_ms)) 201 .count(); 202 let network_time_offset_ms = median_peer_clock_offset(peers, now_ms); 203 let bad_clock_peers = peers 204 .iter() 205 .filter(|peer| { 206 peer.last_clock_observed_ms.is_some_and(|observed_ms| { 207 now_ms.saturating_sub(observed_ms) <= PEER_STALE_AFTER_MS 208 }) 209 }) 210 .filter(|peer| peer.last_clock_offset_accepted == Some(false)) 211 .count(); 212 let lag_blocks = best_known_height.saturating_sub(local_height); 213 let last_error = peers.iter().rev().find_map(|peer| { 214 peer.last_error 215 .as_ref() 216 .map(|error| format!("{}: {error}", peer.address)) 217 }); 218 219 let state = if peers.is_empty() { 220 "isolated" 221 } else if banned_peers > 0 && healthy_peers == 0 { 222 "banned" 223 } else if lag_blocks > 0 { 224 "syncing" 225 } else if failed_peers > 0 && healthy_peers == 0 { 226 "peer errors" 227 } else if stale_peers > 0 && healthy_peers == stale_peers { 228 "stale" 229 } else if remote_best_height.is_some_and(|height| local_height > height) { 230 "ahead of peers" 231 } else { 232 "healthy" 233 } 234 .to_string(); 235 236 NetworkHealthResponse { 237 ok: !peers.is_empty() && lag_blocks == 0 && healthy_peers > stale_peers, 238 state, 239 local_height, 240 best_known_height, 241 shared_height, 242 lag_blocks, 243 outbound_peers, 244 inbound_peers, 245 healthy_peers, 246 failed_peers, 247 stale_peers, 248 banned_peers, 249 pending_transactions: local.pending_transactions, 250 pending_plain_transactions: mempool.plain_transactions, 251 pending_blinded_transactions: mempool.blinded_transactions, 252 pending_blinded_reveals: mempool.blinded_reveals, 253 network_time_offset_ms, 254 bad_clock_peers, 255 last_error, 256 } 257 } 258 259 fn median_peer_clock_offset(peers: &[PeerInfo], now_ms: u64) -> Option<i64> { 260 let mut offsets = peers 261 .iter() 262 .filter(|peer| peer.last_error.is_none()) 263 .filter(|peer| !peer.is_banned_at(now_ms)) 264 .filter(|peer| peer.last_clock_offset_accepted == Some(true)) 265 .filter(|peer| { 266 peer.last_clock_observed_ms.is_some_and(|observed_ms| { 267 now_ms.saturating_sub(observed_ms) <= PEER_STALE_AFTER_MS 268 }) 269 }) 270 .filter_map(|peer| peer.last_clock_offset_ms) 271 .collect::<Vec<_>>(); 272 if offsets.is_empty() { 273 return None; 274 } 275 offsets.sort_unstable(); 276 Some(offsets[offsets.len() / 2]) 277 }