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