use candid::Decode;
use icydb::metrics::{EventCounters, EventReport};
use crate::{
cli::{CanisterTarget, MetricsArgs},
config::{
ConfiguredEndpoint, METRICS_ENDPOINT, METRICS_RESET_ENDPOINT, require_configured_endpoint,
},
icp::require_created_canister,
table::{ColumnAlign, append_indented_table},
};
use super::{call_query, call_update, render::yes_no};
type MetricsEntityRow = [String; 6];
const METRICS_ENTITY_HEADERS: [&str; 6] = ["entity", "load", "save", "delete", "success", "errors"];
const METRICS_ENTITY_ALIGNMENTS: [ColumnAlign; 6] = [
ColumnAlign::Left,
ColumnAlign::Right,
ColumnAlign::Right,
ColumnAlign::Right,
ColumnAlign::Right,
ColumnAlign::Right,
];
pub(super) fn run_metrics_command(args: MetricsArgs) -> Result<(), String> {
let target = args.target();
let endpoint = metrics_endpoint(args.reset());
require_configured_endpoint(target.canister_name(), endpoint)?;
require_created_canister(target.environment(), target.canister_name())?;
if args.reset() {
return run_metrics_reset(target);
}
let candid_arg = metrics_candid_arg(args.window_start_ms());
let candid_bytes = call_query(
target.environment(),
target.canister_name(),
endpoint.method(),
candid_arg.as_str(),
)?;
let response = decode_metrics_report(candid_bytes.as_slice())?;
match response {
Ok(report) => {
print!("{}", render_metrics_report(&report));
Ok(())
}
Err(err) => Err(metrics_method_error("metrics", endpoint, target, err)),
}
}
const fn metrics_endpoint(reset: bool) -> ConfiguredEndpoint {
if reset {
METRICS_RESET_ENDPOINT
} else {
METRICS_ENDPOINT
}
}
fn run_metrics_reset(target: &CanisterTarget) -> Result<(), String> {
let candid_bytes = call_update(
target.environment(),
target.canister_name(),
METRICS_RESET_ENDPOINT.method(),
"()",
)?;
let response = Decode!(candid_bytes.as_slice(), Result<(), icydb::Error>)
.map_err(|err| err.to_string())?;
match response {
Ok(()) => {
println!(
"Reset metrics on canister '{}' in environment '{}'.",
target.canister_name(),
target.environment(),
);
Ok(())
}
Err(err) => Err(metrics_method_error(
"metrics reset",
METRICS_RESET_ENDPOINT,
target,
err,
)),
}
}
fn decode_metrics_report(
candid_bytes: &[u8],
) -> Result<Result<icydb::metrics::EventReport, icydb::Error>, String> {
Decode!(
candid_bytes,
Result<icydb::metrics::EventReport, icydb::Error>
)
.map_err(|err| err.to_string())
}
fn metrics_method_error(
label: &str,
endpoint: ConfiguredEndpoint,
target: &CanisterTarget,
err: icydb::Error,
) -> String {
format!(
"IcyDB {label} method '{}' failed on canister '{}' in environment '{}': {err}",
endpoint.method(),
target.canister_name(),
target.environment(),
)
}
pub(super) fn metrics_candid_arg(window_start_ms: Option<u64>) -> String {
match window_start_ms {
Some(value) => format!("(opt ({value} : nat64))"),
None => "(null)".to_string(),
}
}
pub(super) fn render_metrics_report(report: &EventReport) -> String {
let mut output = String::new();
append_metrics_report_header(&mut output, report);
if let Some(counters) = report.counters() {
append_metrics_counters(&mut output, counters);
} else {
output.push_str(" counters: none\n");
}
output.push('\n');
let entity_rows = report
.entity_counters()
.iter()
.map(|entity| {
[
entity.path().to_string(),
entity.load_calls().to_string(),
entity.save_calls().to_string(),
entity.delete_calls().to_string(),
entity.exec_success().to_string(),
entity_exec_errors(entity).to_string(),
]
})
.collect::<Vec<_>>();
append_metrics_entity_table(&mut output, entity_rows.as_slice());
output
}
fn append_metrics_report_header(output: &mut String, report: &EventReport) {
output.push_str("IcyDB metrics\n");
output.push_str(
format!(
" active window start ms: {}\n requested window start ms: {}\n window filter matched: {}\n entities: {}\n",
report.active_window_start_ms(),
optional_u64(report.requested_window_start_ms()),
yes_no(report.window_filter_matched()),
report.entity_counters().len(),
)
.as_str(),
);
}
fn append_metrics_counters(output: &mut String, counters: &EventCounters) {
let ops = counters.ops();
output.push_str(
format!(
" window: {}..{} ({} ms)\n calls: load={} save={} delete={}\n execution: success={} errors={} aborted={}\n rows: loaded={} saved={} deleted={} scanned={} filtered={} emitted={}\n sql writes: insert={} insert_select={} update={} delete={} matched={} mutated={} returning={}\n cache: query_plan_hits={} query_plan_misses={} sql_hits={} sql_misses={}\n",
counters.window_start_ms(),
counters.window_end_ms(),
counters.window_duration_ms(),
ops.load_calls(),
ops.save_calls(),
ops.delete_calls(),
ops.exec_success(),
ops_exec_errors(ops),
ops.exec_aborted(),
ops.rows_loaded(),
ops.rows_saved(),
ops.rows_deleted(),
ops.rows_scanned(),
ops.rows_filtered(),
ops.rows_emitted(),
ops.sql_insert_calls(),
ops.sql_insert_select_calls(),
ops.sql_update_calls(),
ops.sql_delete_calls(),
ops.sql_write_matched_rows(),
ops.sql_write_mutated_rows(),
ops.sql_write_returning_rows(),
ops.cache_shared_query_plan_hits(),
ops.cache_shared_query_plan_misses(),
ops.cache_sql_compiled_command_hits(),
ops.cache_sql_compiled_command_misses(),
)
.as_str(),
);
}
fn optional_u64(value: Option<u64>) -> String {
value.map_or_else(|| "none".to_string(), |value| value.to_string())
}
fn append_metrics_entity_table(output: &mut String, rows: &[MetricsEntityRow]) {
output.push_str("entities\n");
if rows.is_empty() {
output.push_str(" None\n");
return;
}
append_indented_table(
output,
" ",
&METRICS_ENTITY_HEADERS,
rows,
&METRICS_ENTITY_ALIGNMENTS,
);
}
const fn ops_exec_errors(ops: &icydb::metrics::EventOps) -> u64 {
ops.exec_error_corruption()
.saturating_add(ops.exec_error_incompatible_persisted_format())
.saturating_add(ops.exec_error_not_found())
.saturating_add(ops.exec_error_internal())
.saturating_add(ops.exec_error_conflict())
.saturating_add(ops.exec_error_unsupported())
.saturating_add(ops.exec_error_invariant_violation())
}
const fn entity_exec_errors(entity: &icydb::metrics::EntitySummary) -> u64 {
entity
.exec_error_corruption()
.saturating_add(entity.exec_error_incompatible_persisted_format())
.saturating_add(entity.exec_error_not_found())
.saturating_add(entity.exec_error_internal())
.saturating_add(entity.exec_error_conflict())
.saturating_add(entity.exec_error_unsupported())
.saturating_add(entity.exec_error_invariant_violation())
}