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