1#[cfg(feature = "host")]
8use super::IcNodeStatusRefreshReport;
9use super::{
10 IcNodeAssignmentStatusCounts, IcNodeProviderStatusReport, IcNodeStatusCounts,
11 IcNodeStatusObservation, IcNodeStatusReport, IcNodeStatusRow, IcSubnetStatusReport,
12};
13use crate::{
14 table::{ColumnAlign, render_table},
15 text_value::{sanitize_text, yes_no},
16};
17
18const COMPACT_PRINCIPAL_CHARS: usize = 8;
19
20#[must_use]
22pub fn ic_node_status_report_text(report: &IcNodeStatusReport) -> String {
23 let mut sections = observation_lines(&report.observation);
24 sections.push(counts_line("nodes", &report.counts.statuses));
25 sections.push(assignment_status_line(&report.counts.assignment_statuses));
26 push_table_section(&mut sections, render_node_table(&report.nodes));
27 sections.join("\n")
28}
29
30#[must_use]
32pub fn ic_subnet_status_report_text(report: &IcSubnetStatusReport) -> String {
33 let mut sections = observation_lines(&report.observation);
34 sections.push(format!(
35 "subnets: total={} attention={} returned={} assigned_nodes={}",
36 report.subnet_count,
37 report.attention_subnet_count,
38 report.returned_subnet_count,
39 report.assigned_node_count
40 ));
41 let headers = [
42 "SUBNET",
43 "NODES",
44 "UP",
45 "DEGRADED",
46 "DOWN",
47 "DISABLED",
48 "UNKNOWN",
49 "F",
50 "+DOWN >F",
51 "+NON-UP >F",
52 ];
53 let rows = report
54 .subnets
55 .iter()
56 .map(|row| {
57 [
58 compact(&row.subnet_id),
59 row.statuses.total.to_string(),
60 row.statuses.up.to_string(),
61 row.statuses.degraded.to_string(),
62 row.statuses.down.to_string(),
63 row.statuses.disabled.to_string(),
64 row.statuses.unknown.to_string(),
65 row.fault_tolerance_node_count.to_string(),
66 row.additional_down_nodes_to_exceed_fault_tolerance
67 .to_string(),
68 row.additional_non_up_nodes_to_exceed_fault_tolerance
69 .to_string(),
70 ]
71 })
72 .collect::<Vec<_>>();
73 push_table_section(
74 &mut sections,
75 render_table(
76 &headers,
77 &rows,
78 &[
79 ColumnAlign::Left,
80 ColumnAlign::Right,
81 ColumnAlign::Right,
82 ColumnAlign::Right,
83 ColumnAlign::Right,
84 ColumnAlign::Right,
85 ColumnAlign::Right,
86 ColumnAlign::Right,
87 ColumnAlign::Right,
88 ColumnAlign::Right,
89 ],
90 ),
91 );
92 let non_up = report
93 .subnets
94 .iter()
95 .flat_map(|subnet| subnet.non_up_nodes.iter())
96 .cloned()
97 .collect::<Vec<_>>();
98 if !non_up.is_empty() {
99 sections.push(String::new());
100 sections.push("non-up node evidence:".to_string());
101 sections.push(render_node_table(&non_up));
102 }
103 sections.join("\n")
104}
105
106#[must_use]
108pub fn ic_node_provider_status_report_text(report: &IcNodeProviderStatusReport) -> String {
109 let mut sections = observation_lines(&report.observation);
110 sections.push(format!(
111 "node_providers: total={} attention={} returned={}",
112 report.provider_count, report.attention_provider_count, report.returned_provider_count
113 ));
114 let headers = [
115 "NODE PROVIDER",
116 "NAME",
117 "NODES",
118 "UP",
119 "DEGRADED",
120 "DOWN",
121 "DISABLED",
122 "UNKNOWN",
123 "ASSIGNED UP/NON-UP",
124 "UNASSIGNED UP/NON-UP",
125 "API BN UP/NON-UP",
126 "UNKNOWN ASN UP/NON-UP",
127 ];
128 let rows = report
129 .providers
130 .iter()
131 .map(|row| {
132 [
133 compact(&row.node_provider_id),
134 sanitize_text(&row.node_provider_name),
135 row.counts.statuses.total.to_string(),
136 row.counts.statuses.up.to_string(),
137 row.counts.statuses.degraded.to_string(),
138 row.counts.statuses.down.to_string(),
139 row.counts.statuses.disabled.to_string(),
140 row.counts.statuses.unknown.to_string(),
141 up_non_up(&row.counts.assignment_statuses.assigned),
142 up_non_up(&row.counts.assignment_statuses.unassigned),
143 up_non_up(&row.counts.assignment_statuses.api_boundary),
144 up_non_up(&row.counts.assignment_statuses.unknown),
145 ]
146 })
147 .collect::<Vec<_>>();
148 push_table_section(
149 &mut sections,
150 render_table(
151 &headers,
152 &rows,
153 &[
154 ColumnAlign::Left,
155 ColumnAlign::Left,
156 ColumnAlign::Right,
157 ColumnAlign::Right,
158 ColumnAlign::Right,
159 ColumnAlign::Right,
160 ColumnAlign::Right,
161 ColumnAlign::Right,
162 ColumnAlign::Right,
163 ColumnAlign::Right,
164 ColumnAlign::Right,
165 ColumnAlign::Right,
166 ],
167 ),
168 );
169 sections.join("\n")
170}
171
172fn push_table_section(sections: &mut Vec<String>, table: String) {
173 sections.push(String::new());
174 sections.push(table);
175}
176
177#[cfg(feature = "host")]
179#[must_use]
180pub fn ic_node_status_refresh_report_text(report: &IcNodeStatusRefreshReport) -> String {
181 [
182 format!("network: {}", sanitize_text(&report.network)),
183 format!("fetched_at: {}", sanitize_text(&report.fetched_at)),
184 format!(
185 "source_endpoint: {}",
186 sanitize_text(&report.source_endpoint)
187 ),
188 counts_line("nodes", &report.counts.statuses),
189 assignment_status_line(&report.counts.assignment_statuses),
190 format!(
191 "replaced_existing_cache: {}",
192 yes_no(report.replaced_existing_cache)
193 ),
194 format!("cache_path: {}", sanitize_text(&report.cache_path)),
195 format!(
196 "refresh_lock_path: {}",
197 sanitize_text(&report.refresh_lock_path)
198 ),
199 ]
200 .join("\n")
201}
202
203fn observation_lines(observation: &IcNodeStatusObservation) -> Vec<String> {
204 let mut lines = vec![format!(
205 "observed node status: network={} fetched_at={} source={} certified={} point_in_time={}",
206 sanitize_text(&observation.source.network),
207 sanitize_text(&observation.source.fetched_at),
208 sanitize_text(&observation.source.source_endpoint),
209 yes_no(observation.source.certified),
210 yes_no(observation.source.point_in_time_guaranteed)
211 )];
212 lines.push(format!(
213 "scope: {} cloud_engine_nodes_included={}",
214 observation.scope.as_str(),
215 yes_no(observation.cloud_engine_nodes_included)
216 ));
217 if let Some(cache) = &observation.cache {
218 lines.push(format!(
219 "cache: fresh={} age={}s stale_after={}s path={}",
220 yes_no(cache.cache_fresh),
221 cache.age_seconds,
222 cache.stale_after_seconds,
223 sanitize_text(&cache.cache_path)
224 ));
225 }
226 lines
227}
228
229fn counts_line(label: &str, counts: &IcNodeStatusCounts) -> String {
230 format!(
231 "{label}: total={} up={} degraded={} down={} disabled={} unknown={} non_up={}",
232 counts.total,
233 counts.up,
234 counts.degraded,
235 counts.down,
236 counts.disabled,
237 counts.unknown,
238 counts.non_up()
239 )
240}
241
242fn assignment_status_line(counts: &IcNodeAssignmentStatusCounts) -> String {
243 format!(
244 "assignments (total/up/non_up): assigned={}/{}/{} unassigned={}/{}/{} api_boundary={}/{}/{} unknown={}/{}/{}",
245 counts.assigned.total,
246 counts.assigned.up,
247 counts.assigned.non_up(),
248 counts.unassigned.total,
249 counts.unassigned.up,
250 counts.unassigned.non_up(),
251 counts.api_boundary.total,
252 counts.api_boundary.up,
253 counts.api_boundary.non_up(),
254 counts.unknown.total,
255 counts.unknown.up,
256 counts.unknown.non_up()
257 )
258}
259
260fn up_non_up(counts: &IcNodeStatusCounts) -> String {
261 format!("{}/{}", counts.up, counts.non_up())
262}
263
264fn render_node_table(nodes: &[IcNodeStatusRow]) -> String {
265 let headers = ["NODE", "STATUS", "TYPE", "SUBNET", "PROVIDER", "ALERT"];
266 let rows = nodes
267 .iter()
268 .map(|node| {
269 [
270 compact(&node.node_id),
271 sanitize_text(&node.status),
272 sanitize_text(&node.node_type),
273 node.subnet_id
274 .as_deref()
275 .map_or_else(|| "-".to_string(), compact),
276 compact(&node.node_provider_id),
277 node.alert_name
278 .as_deref()
279 .map_or_else(|| "-".to_string(), sanitize_text),
280 ]
281 })
282 .collect::<Vec<_>>();
283 render_table(
284 &headers,
285 &rows,
286 &[
287 ColumnAlign::Left,
288 ColumnAlign::Left,
289 ColumnAlign::Left,
290 ColumnAlign::Left,
291 ColumnAlign::Left,
292 ColumnAlign::Left,
293 ],
294 )
295}
296
297fn compact(value: &str) -> String {
298 sanitize_text(value)
299 .chars()
300 .take(COMPACT_PRINCIPAL_CHARS)
301 .collect()
302}