use crate::{InternalError, InternalErrorClass, InternalErrorOrigin, ids::CanisterRole};
use std::{cell::RefCell, collections::HashMap};
const UNSCOPED_ROLE_LABEL: &str = "unscoped";
const UNKNOWN_ROLE_LABEL: &str = "unknown";
thread_local! {
static CANISTER_OPS_METRICS: RefCell<HashMap<CanisterOpsMetricKey, u64>> =
RefCell::new(HashMap::new());
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[remain::sorted]
pub enum CanisterOpsMetricOperation {
Create,
Delete,
Install,
Reinstall,
Restore,
Snapshot,
Upgrade,
}
impl CanisterOpsMetricOperation {
#[must_use]
pub const fn metric_label(self) -> &'static str {
match self {
Self::Create => "create",
Self::Delete => "delete",
Self::Install => "install",
Self::Reinstall => "reinstall",
Self::Restore => "restore",
Self::Snapshot => "snapshot",
Self::Upgrade => "upgrade",
}
}
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[remain::sorted]
pub enum CanisterOpsMetricOutcome {
Completed,
Failed,
Skipped,
Started,
}
impl CanisterOpsMetricOutcome {
#[must_use]
pub const fn metric_label(self) -> &'static str {
match self {
Self::Completed => "completed",
Self::Failed => "failed",
Self::Skipped => "skipped",
Self::Started => "started",
}
}
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[remain::sorted]
pub enum CanisterOpsMetricReason {
AlreadyExists,
Cycles,
InvalidState,
ManagementCall,
MissingWasm,
NewAllocation,
NotFound,
Ok,
PolicyDenied,
PoolReuse,
PoolTopup,
StatePropagation,
Topology,
TopologyPropagation,
Unknown,
}
impl CanisterOpsMetricReason {
#[must_use]
pub const fn metric_label(self) -> &'static str {
match self {
Self::AlreadyExists => "already_exists",
Self::NewAllocation => "new_allocation",
Self::Cycles => "cycles",
Self::InvalidState => "invalid_state",
Self::ManagementCall => "management_call",
Self::MissingWasm => "missing_wasm",
Self::NotFound => "not_found",
Self::Ok => "ok",
Self::PolicyDenied => "policy_denied",
Self::PoolReuse => "pool_reuse",
Self::PoolTopup => "pool_topup",
Self::StatePropagation => "state_propagation",
Self::Topology => "topology",
Self::TopologyPropagation => "topology_propagation",
Self::Unknown => "unknown",
}
}
#[must_use]
pub(crate) const fn from_error(err: &InternalError) -> Self {
match (err.class(), err.origin()) {
(InternalErrorClass::Infra, InternalErrorOrigin::Infra) => Self::ManagementCall,
(InternalErrorClass::Domain, InternalErrorOrigin::Domain)
| (InternalErrorClass::Access, _) => Self::PolicyDenied,
(InternalErrorClass::Domain, InternalErrorOrigin::Config)
| (
InternalErrorClass::Invariant
| InternalErrorClass::Ops
| InternalErrorClass::Workflow,
_,
) => Self::InvalidState,
_ => Self::Unknown,
}
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct CanisterOpsMetricKey {
pub operation: CanisterOpsMetricOperation,
pub role: String,
pub outcome: CanisterOpsMetricOutcome,
pub reason: CanisterOpsMetricReason,
}
pub struct CanisterOpsMetrics;
impl CanisterOpsMetrics {
pub fn record(
operation: CanisterOpsMetricOperation,
role: &CanisterRole,
outcome: CanisterOpsMetricOutcome,
reason: CanisterOpsMetricReason,
) {
Self::record_role_label(operation, role.as_str(), outcome, reason);
}
pub fn record_unscoped(
operation: CanisterOpsMetricOperation,
outcome: CanisterOpsMetricOutcome,
reason: CanisterOpsMetricReason,
) {
Self::record_role_label(operation, UNSCOPED_ROLE_LABEL, outcome, reason);
}
pub fn record_unknown_role(
operation: CanisterOpsMetricOperation,
outcome: CanisterOpsMetricOutcome,
reason: CanisterOpsMetricReason,
) {
Self::record_role_label(operation, UNKNOWN_ROLE_LABEL, outcome, reason);
}
fn record_role_label(
operation: CanisterOpsMetricOperation,
role: &str,
outcome: CanisterOpsMetricOutcome,
reason: CanisterOpsMetricReason,
) {
CANISTER_OPS_METRICS.with_borrow_mut(|counts| {
let key = CanisterOpsMetricKey {
operation,
role: role.to_string(),
outcome,
reason,
};
let entry = counts.entry(key).or_insert(0);
*entry = entry.saturating_add(1);
});
}
#[must_use]
pub fn snapshot() -> Vec<(CanisterOpsMetricKey, u64)> {
CANISTER_OPS_METRICS
.with_borrow(std::clone::Clone::clone)
.into_iter()
.collect()
}
#[cfg(test)]
pub fn reset() {
CANISTER_OPS_METRICS.with_borrow_mut(HashMap::clear);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn snapshot_map() -> HashMap<CanisterOpsMetricKey, u64> {
CanisterOpsMetrics::snapshot().into_iter().collect()
}
#[test]
fn canister_ops_metrics_accumulate_by_operation_role_outcome_and_reason() {
CanisterOpsMetrics::reset();
let role = CanisterRole::new("app");
CanisterOpsMetrics::record(
CanisterOpsMetricOperation::Create,
&role,
CanisterOpsMetricOutcome::Started,
CanisterOpsMetricReason::Ok,
);
CanisterOpsMetrics::record(
CanisterOpsMetricOperation::Create,
&role,
CanisterOpsMetricOutcome::Failed,
CanisterOpsMetricReason::Topology,
);
CanisterOpsMetrics::record(
CanisterOpsMetricOperation::Create,
&role,
CanisterOpsMetricOutcome::Failed,
CanisterOpsMetricReason::Topology,
);
let map = snapshot_map();
assert_eq!(
map.get(&CanisterOpsMetricKey {
operation: CanisterOpsMetricOperation::Create,
role: "app".to_string(),
outcome: CanisterOpsMetricOutcome::Started,
reason: CanisterOpsMetricReason::Ok,
}),
Some(&1)
);
assert_eq!(
map.get(&CanisterOpsMetricKey {
operation: CanisterOpsMetricOperation::Create,
role: "app".to_string(),
outcome: CanisterOpsMetricOutcome::Failed,
reason: CanisterOpsMetricReason::Topology,
}),
Some(&2)
);
}
}