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 sections.push(format!(
115 "unassigned_vs_assigned providers (less/equal/greater): up={}/{}/{} non_up={}/{}/{}",
116 report.unassigned_up_vs_assigned_up_provider_counts.less,
117 report.unassigned_up_vs_assigned_up_provider_counts.equal,
118 report.unassigned_up_vs_assigned_up_provider_counts.greater,
119 report
120 .unassigned_non_up_vs_assigned_non_up_provider_counts
121 .less,
122 report
123 .unassigned_non_up_vs_assigned_non_up_provider_counts
124 .equal,
125 report
126 .unassigned_non_up_vs_assigned_non_up_provider_counts
127 .greater,
128 ));
129 let headers = [
130 "NODE PROVIDER",
131 "NAME",
132 "NODES",
133 "UP",
134 "DEGRADED",
135 "DOWN",
136 "DISABLED",
137 "UNKNOWN",
138 "ASSIGNED UP/NON-UP",
139 "UNASSIGNED UP/NON-UP",
140 "API BN UP/NON-UP",
141 "UNKNOWN ASN UP/NON-UP",
142 "UNASN VS ASN UP/NON-UP",
143 ];
144 let rows = report
145 .providers
146 .iter()
147 .map(|row| {
148 [
149 compact(&row.node_provider_id),
150 sanitize_text(&row.node_provider_name),
151 row.counts.statuses.total.to_string(),
152 row.counts.statuses.up.to_string(),
153 row.counts.statuses.degraded.to_string(),
154 row.counts.statuses.down.to_string(),
155 row.counts.statuses.disabled.to_string(),
156 row.counts.statuses.unknown.to_string(),
157 up_non_up(&row.counts.assignment_statuses.assigned),
158 up_non_up(&row.counts.assignment_statuses.unassigned),
159 up_non_up(&row.counts.assignment_statuses.api_boundary),
160 up_non_up(&row.counts.assignment_statuses.unknown),
161 format!(
162 "{}/{}",
163 row.unassigned_up_vs_assigned_up.as_str(),
164 row.unassigned_non_up_vs_assigned_non_up.as_str()
165 ),
166 ]
167 })
168 .collect::<Vec<_>>();
169 push_table_section(
170 &mut sections,
171 render_table(
172 &headers,
173 &rows,
174 &[
175 ColumnAlign::Left,
176 ColumnAlign::Left,
177 ColumnAlign::Right,
178 ColumnAlign::Right,
179 ColumnAlign::Right,
180 ColumnAlign::Right,
181 ColumnAlign::Right,
182 ColumnAlign::Right,
183 ColumnAlign::Right,
184 ColumnAlign::Right,
185 ColumnAlign::Right,
186 ColumnAlign::Right,
187 ColumnAlign::Left,
188 ],
189 ),
190 );
191 sections.join("\n")
192}
193
194fn push_table_section(sections: &mut Vec<String>, table: String) {
195 sections.push(String::new());
196 sections.push(table);
197}
198
199#[cfg(feature = "host")]
201#[must_use]
202pub fn ic_node_status_refresh_report_text(report: &IcNodeStatusRefreshReport) -> String {
203 [
204 format!("network: {}", sanitize_text(&report.network)),
205 format!("fetched_at: {}", sanitize_text(&report.fetched_at)),
206 format!(
207 "source_endpoint: {}",
208 sanitize_text(&report.source_endpoint)
209 ),
210 counts_line("nodes", &report.counts.statuses),
211 assignment_status_line(&report.counts.assignment_statuses),
212 format!(
213 "replaced_existing_cache: {}",
214 yes_no(report.replaced_existing_cache)
215 ),
216 format!("cache_path: {}", sanitize_text(&report.cache_path)),
217 format!(
218 "refresh_lock_path: {}",
219 sanitize_text(&report.refresh_lock_path)
220 ),
221 ]
222 .join("\n")
223}
224
225fn observation_lines(observation: &IcNodeStatusObservation) -> Vec<String> {
226 let mut lines = vec![format!(
227 "observed node status: network={} fetched_at={} source={} certified={} point_in_time={}",
228 sanitize_text(&observation.source.network),
229 sanitize_text(&observation.source.fetched_at),
230 sanitize_text(&observation.source.source_endpoint),
231 yes_no(observation.source.certified),
232 yes_no(observation.source.point_in_time_guaranteed)
233 )];
234 lines.push(format!(
235 "scope: {} cloud_engine_nodes_included={}",
236 observation.scope.as_str(),
237 yes_no(observation.cloud_engine_nodes_included)
238 ));
239 if let Some(cache) = &observation.cache {
240 lines.push(format!(
241 "cache: fresh={} age={}s stale_after={}s path={}",
242 yes_no(cache.cache_fresh),
243 cache.age_seconds,
244 cache.stale_after_seconds,
245 sanitize_text(&cache.cache_path)
246 ));
247 }
248 lines
249}
250
251fn counts_line(label: &str, counts: &IcNodeStatusCounts) -> String {
252 format!(
253 "{label}: total={} up={} degraded={} down={} disabled={} unknown={} non_up={}",
254 counts.total,
255 counts.up,
256 counts.degraded,
257 counts.down,
258 counts.disabled,
259 counts.unknown,
260 counts.non_up()
261 )
262}
263
264fn assignment_status_line(counts: &IcNodeAssignmentStatusCounts) -> String {
265 format!(
266 "assignments (total/up/non_up): assigned={}/{}/{} unassigned={}/{}/{} api_boundary={}/{}/{} unknown={}/{}/{}",
267 counts.assigned.total,
268 counts.assigned.up,
269 counts.assigned.non_up(),
270 counts.unassigned.total,
271 counts.unassigned.up,
272 counts.unassigned.non_up(),
273 counts.api_boundary.total,
274 counts.api_boundary.up,
275 counts.api_boundary.non_up(),
276 counts.unknown.total,
277 counts.unknown.up,
278 counts.unknown.non_up()
279 )
280}
281
282fn up_non_up(counts: &IcNodeStatusCounts) -> String {
283 format!("{}/{}", counts.up, counts.non_up())
284}
285
286fn render_node_table(nodes: &[IcNodeStatusRow]) -> String {
287 let headers = ["NODE", "STATUS", "TYPE", "SUBNET", "PROVIDER", "ALERT"];
288 let rows = nodes
289 .iter()
290 .map(|node| {
291 [
292 compact(&node.node_id),
293 sanitize_text(&node.status),
294 sanitize_text(&node.node_type),
295 node.subnet_id
296 .as_deref()
297 .map_or_else(|| "-".to_string(), compact),
298 compact(&node.node_provider_id),
299 node.alert_name
300 .as_deref()
301 .map_or_else(|| "-".to_string(), sanitize_text),
302 ]
303 })
304 .collect::<Vec<_>>();
305 render_table(
306 &headers,
307 &rows,
308 &[
309 ColumnAlign::Left,
310 ColumnAlign::Left,
311 ColumnAlign::Left,
312 ColumnAlign::Left,
313 ColumnAlign::Left,
314 ColumnAlign::Left,
315 ],
316 )
317}
318
319fn compact(value: &str) -> String {
320 sanitize_text(value)
321 .chars()
322 .take(COMPACT_PRINCIPAL_CHARS)
323 .collect()
324}