1use crate::{
8 human_quantity::decimal_cycle_rate_text,
9 ic::{
10 IcBoundaryNodeDataCentersReport, IcCanisterCountReport, IcCanisterFilters,
11 IcCanisterPageReport, IcCanisterReport, IcDailyStatsReport, IcDashboardReportProvenance,
12 IcIcrcIndexedCountReport, IcIcrcTokenValueReport, IcIcrcTotalSupplyReport, IcMetricKind,
13 IcMetricReport,
14 },
15 text_value::{optional_text, sanitize_text, yes_no},
16};
17
18#[must_use]
20pub fn ic_boundary_node_data_centers_report_text(
21 report: &IcBoundaryNodeDataCentersReport,
22) -> String {
23 let mut lines = report_header(&report.provenance);
24 lines.extend([
25 format!("data_center_count: {}", report.data_center_count),
26 format!("total_node_count: {}", report.total_node_count),
27 ]);
28 append_report_footer(&mut lines, &report.provenance);
29
30 if !report.rows.is_empty() {
31 lines.push(String::new());
32 lines.push("boundary_node_data_centers:".to_string());
33 lines.extend(report.rows.iter().map(|row| {
34 format!(
35 " {} name={} owner={} region={} latitude={} longitude={} nodes={}",
36 sanitize_text(&row.dc_id),
37 sanitize_text(&row.name),
38 sanitize_text(&row.owner),
39 sanitize_text(&row.region),
40 sanitize_text(&row.latitude),
41 sanitize_text(&row.longitude),
42 sanitize_text(&row.total_nodes),
43 )
44 }));
45 }
46 lines.join("\n")
47}
48
49#[must_use]
51pub fn ic_daily_stats_report_text(report: &IcDailyStatsReport) -> String {
52 let mut lines = report_header(&report.provenance);
53 lines.extend([
54 format!("start_unix_secs: {}", report.query.start_unix_secs),
55 format!("end_unix_secs: {}", report.query.end_unix_secs),
56 format!("returned_day_count: {}", report.returned_day_count),
57 ]);
58 append_report_footer(&mut lines, &report.provenance);
59
60 if !report.rows.is_empty() {
61 lines.push(String::new());
62 lines.push("daily_stats:".to_string());
63 lines.extend(report.rows.iter().map(|row| {
64 format!(
65 " {} timestamp={} avg_total={} avg_update={} avg_query={} max_total={} max_update={} max_query={} blocks_avg={}",
66 sanitize_text(&row.day),
67 row.timestamp_unix_secs,
68 sanitize_text(&row.average_transactions_per_second),
69 sanitize_text(&row.average_update_transactions_per_second),
70 sanitize_text(&row.average_query_transactions_per_second),
71 sanitize_text(&row.max_total_transactions_per_second),
72 sanitize_text(&row.max_update_transactions_per_second),
73 sanitize_text(&row.max_query_transactions_per_second),
74 sanitize_text(&row.blocks_per_second_average),
75 )
76 }));
77 }
78 lines.join("\n")
79}
80
81#[must_use]
83pub fn ic_metric_report_text(report: &IcMetricReport) -> String {
84 let mut lines = report_header(&report.provenance);
85 lines.extend([
86 format!("metric: {}", report.query.metric),
87 format!("start_unix_secs: {}", report.query.start_unix_secs),
88 format!("end_unix_secs: {}", report.query.end_unix_secs),
89 format!("step_secs: {}", report.query.step_secs),
90 format!("returned_series_count: {}", report.returned_series_count),
91 format!(
92 "returned_observation_count: {}",
93 report.returned_observation_count
94 ),
95 ]);
96 append_report_footer(&mut lines, &report.provenance);
97
98 for series in &report.series {
99 lines.push(String::new());
100 lines.push(format!("{}:", sanitize_text(&series.name)));
101 lines.extend(series.observations.iter().map(|observation| {
102 format!(
103 " {} {}",
104 observation.timestamp_unix_secs,
105 metric_observation_text(report.query.metric, &observation.value)
106 )
107 }));
108 }
109 lines.join("\n")
110}
111
112#[must_use]
114pub fn icrc_indexed_count_report_text(report: &IcIcrcIndexedCountReport) -> String {
115 let mut lines = report_header(&report.provenance);
116 lines.extend([
117 format!(
118 "ledger_canister_id: {}",
119 sanitize_text(&report.ledger_canister_id)
120 ),
121 format!("kind: {}", report.kind),
122 format!("total: {}", report.total),
123 ]);
124 append_report_footer(&mut lines, &report.provenance);
125 lines.join("\n")
126}
127
128#[must_use]
130pub fn icrc_token_value_report_text(report: &IcIcrcTokenValueReport) -> String {
131 let mut lines = report_header(&report.provenance);
132 lines.extend([
133 format!(
134 "ledger_canister_id: {}",
135 sanitize_text(&report.ledger_canister_id)
136 ),
137 format!("start_unix_secs: {}", report.query.start_unix_secs),
138 format!("end_unix_secs: {}", report.query.end_unix_secs),
139 format!("limit: {}", report.query.limit),
140 format!("returned_row_count: {}", report.returned_row_count),
141 format!("limit_reached: {}", yes_no(report.limit_reached)),
142 ]);
143 append_report_footer(&mut lines, &report.provenance);
144
145 if !report.rows.is_empty() {
146 lines.push(String::new());
147 lines.push("token_values:".to_string());
148 lines.extend(report.rows.iter().map(|row| {
149 format!(
150 " {} price={} volume_24h={} price_usd={} volume_24h_usd={} source={} source_url={}",
151 row.timestamp_unix_secs,
152 optional_text(row.price.as_ref()),
153 optional_text(row.volume_24h.as_ref()),
154 optional_text(row.price_usd.as_ref()),
155 optional_text(row.volume_24h_usd.as_ref()),
156 optional_text(row.source.as_ref()),
157 optional_text(row.source_url.as_ref()),
158 )
159 }));
160 }
161 lines.join("\n")
162}
163
164#[must_use]
166pub fn icrc_total_supply_report_text(report: &IcIcrcTotalSupplyReport) -> String {
167 let mut lines = report_header(&report.provenance);
168 lines.extend([
169 format!(
170 "ledger_canister_id: {}",
171 sanitize_text(&report.ledger_canister_id)
172 ),
173 format!("start_unix_secs: {}", report.query.start_unix_secs),
174 format!("end_unix_secs: {}", report.query.end_unix_secs),
175 format!("step_secs: {}", report.query.step_secs),
176 format!(
177 "requested_observation_limit: {}",
178 report.requested_observation_limit
179 ),
180 format!(
181 "returned_observation_count: {}",
182 report.returned_observation_count
183 ),
184 ]);
185 append_report_footer(&mut lines, &report.provenance);
186
187 if !report.observations.is_empty() {
188 lines.push(String::new());
189 lines.push("total_supply_base_units:".to_string());
190 lines.extend(report.observations.iter().map(|observation| {
191 format!(
192 " {} {}",
193 observation.timestamp_unix_secs,
194 sanitize_text(&observation.total_supply_base_units)
195 )
196 }));
197 }
198 lines.join("\n")
199}
200
201fn metric_observation_text(metric: IcMetricKind, value: &str) -> String {
202 if metric == IcMetricKind::CycleBurnRate {
203 decimal_cycle_rate_text(value)
204 } else {
205 sanitize_text(value)
206 }
207}
208
209#[must_use]
211pub fn ic_canister_report_text(report: &IcCanisterReport) -> String {
212 let mut lines = report_header(&report.provenance);
213 lines.extend([
214 format!("canister_id: {}", report.canister_id),
215 format!("dashboard_id: {}", report.dashboard_id),
216 format!("name: {}", text_or_dash(&report.name)),
217 format!(
218 "canister_type: {}",
219 report
220 .canister_type
221 .as_deref()
222 .map_or_else(|| "-".to_string(), text_or_dash)
223 ),
224 format!("subnet_id: {}", report.subnet_id),
225 format!("controller_count: {}", report.controllers.len()),
226 format!("language: {}", text_or_dash(&report.language)),
227 format!("module_hash: {}", text_or_dash(&report.module_hash)),
228 format!(
229 "dashboard_updated_at: {}",
230 sanitize_text(&report.dashboard_updated_at)
231 ),
232 format!(
233 "upgrade_history_available: {}",
234 yes_no(report.upgrades.is_some())
235 ),
236 format!(
237 "upgrade_count: {}",
238 report
239 .upgrade_count
240 .map_or_else(|| "-".to_string(), |count| count.to_string())
241 ),
242 ]);
243 append_report_footer(&mut lines, &report.provenance);
244
245 if !report.controllers.is_empty() {
246 lines.push(String::new());
247 lines.push("controllers:".to_string());
248 lines.extend(
249 report
250 .controllers
251 .iter()
252 .map(|controller| format!(" {controller}")),
253 );
254 }
255
256 if let Some(latest) = report
257 .upgrades
258 .as_ref()
259 .and_then(|upgrades| upgrades.first())
260 {
261 lines.push(String::new());
262 lines.push("latest_upgrade:".to_string());
263 lines.push(format!(" proposal_id: {}", latest.proposal_id));
264 lines.push(format!(
265 " executed_timestamp_seconds: {}",
266 latest.executed_timestamp_seconds
267 ));
268 lines.push(format!(" module_hash: {}", latest.module_hash));
269 }
270
271 lines.join("\n")
272}
273
274#[must_use]
276pub fn ic_canister_count_report_text(report: &IcCanisterCountReport) -> String {
277 let mut lines = report_header(&report.provenance);
278 lines.push(format!("total: {}", report.total));
279 append_filters(&mut lines, &report.filters);
280 append_report_footer(&mut lines, &report.provenance);
281 lines.join("\n")
282}
283
284#[must_use]
286pub fn ic_canister_page_report_text(report: &IcCanisterPageReport) -> String {
287 let mut lines = report_header(&report.provenance);
288 lines.extend([
289 format!("returned_count: {}", report.returned_count),
290 format!("requested_limit: {}", report.requested_limit),
291 format!(
292 "after: {}",
293 report.after.as_deref().map_or("-", |value| value)
294 ),
295 format!(
296 "before: {}",
297 report.before.as_deref().map_or("-", |value| value)
298 ),
299 format!(
300 "previous_cursor: {}",
301 report.previous_cursor.as_deref().map_or("-", |value| value)
302 ),
303 format!(
304 "next_cursor: {}",
305 report.next_cursor.as_deref().map_or("-", |value| value)
306 ),
307 ]);
308 append_filters(&mut lines, &report.filters);
309 append_report_footer(&mut lines, &report.provenance);
310
311 if !report.rows.is_empty() {
312 lines.push(String::new());
313 lines.push("canisters:".to_string());
314 lines.extend(report.rows.iter().map(|row| {
315 format!(
316 " {} name={} type={} subnet={} controllers={} language={} updated={}",
317 row.canister_id,
318 text_or_dash(&row.name),
319 row.canister_type
320 .as_deref()
321 .map_or_else(|| "-".to_string(), text_or_dash),
322 row.subnet_id,
323 row.controllers.len(),
324 text_or_dash(&row.language),
325 sanitize_text(&row.dashboard_updated_at),
326 )
327 }));
328 }
329 lines.join("\n")
330}
331
332fn report_header(provenance: &IcDashboardReportProvenance) -> Vec<String> {
333 vec![
334 format!("network: {}", sanitize_text(&provenance.network)),
335 format!("authority: {}", sanitize_text(&provenance.authority)),
336 ]
337}
338
339fn append_report_footer(lines: &mut Vec<String>, provenance: &IcDashboardReportProvenance) {
340 lines.extend([
341 format!("certified: {}", yes_no(provenance.certified)),
342 format!(
343 "point_in_time_guaranteed: {}",
344 yes_no(provenance.point_in_time_guaranteed)
345 ),
346 format!("fetched_at: {}", sanitize_text(&provenance.fetched_at)),
347 format!(
348 "source_endpoint: {}",
349 sanitize_text(&provenance.source_endpoint)
350 ),
351 ]);
352}
353
354fn append_filters(lines: &mut Vec<String>, filters: &IcCanisterFilters) {
355 if let Some(has_name) = filters.has_name {
356 lines.push(format!("filter.has_name: {}", yes_no(has_name)));
357 }
358 if let Some(subnet_id) = filters.subnet_id.as_deref() {
359 lines.push(format!("filter.subnet_id: {subnet_id}"));
360 }
361 if let Some(controller_id) = filters.controller_id.as_deref() {
362 lines.push(format!("filter.controller_id: {controller_id}"));
363 }
364 if !filters.languages.is_empty() {
365 lines.push(format!(
366 "filter.languages: {}",
367 filters
368 .languages
369 .iter()
370 .map(|value| sanitize_text(value))
371 .collect::<Vec<_>>()
372 .join(",")
373 ));
374 }
375 if !filters.canister_types.is_empty() {
376 lines.push(format!(
377 "filter.canister_types: {}",
378 filters
379 .canister_types
380 .iter()
381 .map(|value| sanitize_text(value))
382 .collect::<Vec<_>>()
383 .join(",")
384 ));
385 }
386 if let Some(query) = filters.query.as_deref() {
387 lines.push(format!("filter.query: {}", sanitize_text(query)));
388 }
389}
390
391fn text_or_dash(value: &str) -> String {
392 if value.is_empty() {
393 "-".to_string()
394 } else {
395 sanitize_text(value)
396 }
397}