1use crate::{
8 human_quantity::decimal_cycle_rate_text,
9 ic::{
10 IcBoundaryNodeDataCentersReport, IcCanisterCountReport, IcCanisterFilters,
11 IcCanisterPageReport, IcCanisterReport, IcDailyStatsReport, IcDashboardReportProvenance,
12 IcMetricKind, IcMetricReport,
13 },
14 text_value::{sanitize_text, yes_no},
15};
16
17#[must_use]
19pub fn ic_boundary_node_data_centers_report_text(
20 report: &IcBoundaryNodeDataCentersReport,
21) -> String {
22 let mut lines = report_header(&report.provenance);
23 lines.extend([
24 format!("data_center_count: {}", report.data_center_count),
25 format!("total_node_count: {}", report.total_node_count),
26 ]);
27 append_report_footer(&mut lines, &report.provenance);
28
29 if !report.rows.is_empty() {
30 lines.push(String::new());
31 lines.push("boundary_node_data_centers:".to_string());
32 lines.extend(report.rows.iter().map(|row| {
33 format!(
34 " {} name={} owner={} region={} latitude={} longitude={} nodes={}",
35 sanitize_text(&row.dc_id),
36 sanitize_text(&row.name),
37 sanitize_text(&row.owner),
38 sanitize_text(&row.region),
39 sanitize_text(&row.latitude),
40 sanitize_text(&row.longitude),
41 sanitize_text(&row.total_nodes),
42 )
43 }));
44 }
45 lines.join("\n")
46}
47
48#[must_use]
50pub fn ic_daily_stats_report_text(report: &IcDailyStatsReport) -> String {
51 let mut lines = report_header(&report.provenance);
52 lines.extend([
53 format!("start_unix_secs: {}", report.query.start_unix_secs),
54 format!("end_unix_secs: {}", report.query.end_unix_secs),
55 format!("returned_day_count: {}", report.returned_day_count),
56 ]);
57 append_report_footer(&mut lines, &report.provenance);
58
59 if !report.rows.is_empty() {
60 lines.push(String::new());
61 lines.push("daily_stats:".to_string());
62 lines.extend(report.rows.iter().map(|row| {
63 format!(
64 " {} timestamp={} avg_total={} avg_update={} avg_query={} max_total={} max_update={} max_query={} blocks_avg={}",
65 sanitize_text(&row.day),
66 row.timestamp_unix_secs,
67 sanitize_text(&row.average_transactions_per_second),
68 sanitize_text(&row.average_update_transactions_per_second),
69 sanitize_text(&row.average_query_transactions_per_second),
70 sanitize_text(&row.max_total_transactions_per_second),
71 sanitize_text(&row.max_update_transactions_per_second),
72 sanitize_text(&row.max_query_transactions_per_second),
73 sanitize_text(&row.blocks_per_second_average),
74 )
75 }));
76 }
77 lines.join("\n")
78}
79
80#[must_use]
82pub fn ic_metric_report_text(report: &IcMetricReport) -> String {
83 let mut lines = report_header(&report.provenance);
84 lines.extend([
85 format!("metric: {}", report.query.metric),
86 format!("start_unix_secs: {}", report.query.start_unix_secs),
87 format!("end_unix_secs: {}", report.query.end_unix_secs),
88 format!("step_secs: {}", report.query.step_secs),
89 format!("returned_series_count: {}", report.returned_series_count),
90 format!(
91 "returned_observation_count: {}",
92 report.returned_observation_count
93 ),
94 ]);
95 append_report_footer(&mut lines, &report.provenance);
96
97 for series in &report.series {
98 lines.push(String::new());
99 lines.push(format!("{}:", sanitize_text(&series.name)));
100 lines.extend(series.observations.iter().map(|observation| {
101 format!(
102 " {} {}",
103 observation.timestamp_unix_secs,
104 metric_observation_text(report.query.metric, &observation.value)
105 )
106 }));
107 }
108 lines.join("\n")
109}
110
111fn metric_observation_text(metric: IcMetricKind, value: &str) -> String {
112 if metric == IcMetricKind::CycleBurnRate {
113 decimal_cycle_rate_text(value)
114 } else {
115 sanitize_text(value)
116 }
117}
118
119#[must_use]
121pub fn ic_canister_report_text(report: &IcCanisterReport) -> String {
122 let mut lines = report_header(&report.provenance);
123 lines.extend([
124 format!("canister_id: {}", report.canister_id),
125 format!("dashboard_id: {}", report.dashboard_id),
126 format!("name: {}", text_or_dash(&report.name)),
127 format!(
128 "canister_type: {}",
129 report
130 .canister_type
131 .as_deref()
132 .map_or_else(|| "-".to_string(), text_or_dash)
133 ),
134 format!("subnet_id: {}", report.subnet_id),
135 format!("controller_count: {}", report.controllers.len()),
136 format!("language: {}", text_or_dash(&report.language)),
137 format!("module_hash: {}", text_or_dash(&report.module_hash)),
138 format!(
139 "dashboard_updated_at: {}",
140 sanitize_text(&report.dashboard_updated_at)
141 ),
142 format!(
143 "upgrade_history_available: {}",
144 yes_no(report.upgrades.is_some())
145 ),
146 format!(
147 "upgrade_count: {}",
148 report
149 .upgrade_count
150 .map_or_else(|| "-".to_string(), |count| count.to_string())
151 ),
152 ]);
153 append_report_footer(&mut lines, &report.provenance);
154
155 if !report.controllers.is_empty() {
156 lines.push(String::new());
157 lines.push("controllers:".to_string());
158 lines.extend(
159 report
160 .controllers
161 .iter()
162 .map(|controller| format!(" {controller}")),
163 );
164 }
165
166 if let Some(latest) = report
167 .upgrades
168 .as_ref()
169 .and_then(|upgrades| upgrades.first())
170 {
171 lines.push(String::new());
172 lines.push("latest_upgrade:".to_string());
173 lines.push(format!(" proposal_id: {}", latest.proposal_id));
174 lines.push(format!(
175 " executed_timestamp_seconds: {}",
176 latest.executed_timestamp_seconds
177 ));
178 lines.push(format!(" module_hash: {}", latest.module_hash));
179 }
180
181 lines.join("\n")
182}
183
184#[must_use]
186pub fn ic_canister_count_report_text(report: &IcCanisterCountReport) -> String {
187 let mut lines = report_header(&report.provenance);
188 lines.push(format!("total: {}", report.total));
189 append_filters(&mut lines, &report.filters);
190 append_report_footer(&mut lines, &report.provenance);
191 lines.join("\n")
192}
193
194#[must_use]
196pub fn ic_canister_page_report_text(report: &IcCanisterPageReport) -> String {
197 let mut lines = report_header(&report.provenance);
198 lines.extend([
199 format!("returned_count: {}", report.returned_count),
200 format!("requested_limit: {}", report.requested_limit),
201 format!(
202 "after: {}",
203 report.after.as_deref().map_or("-", |value| value)
204 ),
205 format!(
206 "before: {}",
207 report.before.as_deref().map_or("-", |value| value)
208 ),
209 format!(
210 "previous_cursor: {}",
211 report.previous_cursor.as_deref().map_or("-", |value| value)
212 ),
213 format!(
214 "next_cursor: {}",
215 report.next_cursor.as_deref().map_or("-", |value| value)
216 ),
217 ]);
218 append_filters(&mut lines, &report.filters);
219 append_report_footer(&mut lines, &report.provenance);
220
221 if !report.rows.is_empty() {
222 lines.push(String::new());
223 lines.push("canisters:".to_string());
224 lines.extend(report.rows.iter().map(|row| {
225 format!(
226 " {} name={} type={} subnet={} controllers={} language={} updated={}",
227 row.canister_id,
228 text_or_dash(&row.name),
229 row.canister_type
230 .as_deref()
231 .map_or_else(|| "-".to_string(), text_or_dash),
232 row.subnet_id,
233 row.controllers.len(),
234 text_or_dash(&row.language),
235 sanitize_text(&row.dashboard_updated_at),
236 )
237 }));
238 }
239 lines.join("\n")
240}
241
242fn report_header(provenance: &IcDashboardReportProvenance) -> Vec<String> {
243 vec![
244 format!("network: {}", sanitize_text(&provenance.network)),
245 format!("authority: {}", sanitize_text(&provenance.authority)),
246 ]
247}
248
249fn append_report_footer(lines: &mut Vec<String>, provenance: &IcDashboardReportProvenance) {
250 lines.extend([
251 format!("certified: {}", yes_no(provenance.certified)),
252 format!(
253 "point_in_time_guaranteed: {}",
254 yes_no(provenance.point_in_time_guaranteed)
255 ),
256 format!("fetched_at: {}", sanitize_text(&provenance.fetched_at)),
257 format!(
258 "source_endpoint: {}",
259 sanitize_text(&provenance.source_endpoint)
260 ),
261 ]);
262}
263
264fn append_filters(lines: &mut Vec<String>, filters: &IcCanisterFilters) {
265 if let Some(has_name) = filters.has_name {
266 lines.push(format!("filter.has_name: {}", yes_no(has_name)));
267 }
268 if let Some(subnet_id) = filters.subnet_id.as_deref() {
269 lines.push(format!("filter.subnet_id: {subnet_id}"));
270 }
271 if let Some(controller_id) = filters.controller_id.as_deref() {
272 lines.push(format!("filter.controller_id: {controller_id}"));
273 }
274 if !filters.languages.is_empty() {
275 lines.push(format!(
276 "filter.languages: {}",
277 filters
278 .languages
279 .iter()
280 .map(|value| sanitize_text(value))
281 .collect::<Vec<_>>()
282 .join(",")
283 ));
284 }
285 if !filters.canister_types.is_empty() {
286 lines.push(format!(
287 "filter.canister_types: {}",
288 filters
289 .canister_types
290 .iter()
291 .map(|value| sanitize_text(value))
292 .collect::<Vec<_>>()
293 .join(",")
294 ));
295 }
296 if let Some(query) = filters.query.as_deref() {
297 lines.push(format!("filter.query: {}", sanitize_text(query)));
298 }
299}
300
301fn text_or_dash(value: &str) -> String {
302 if value.is_empty() {
303 "-".to_string()
304 } else {
305 sanitize_text(value)
306 }
307}