Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion rs/nns/governance/src/governance/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,10 +865,15 @@ mod metrics_tests {
use crate::{
encode_metrics,
governance::Governance,
pb::v1::{Motion, Proposal, ProposalData, Tally, Topic, proposal},
pb::v1::{
IcpPriceHistory, MaturityModulation, Motion, Proposal, ProposalData, SampledPrice,
Tally, Topic, proposal,
},
test_utils::{MockEnvironment, StubCMC, StubIcpLedger},
};

const ONE_DAY_SECONDS: u64 = 86_400;

Comment on lines 872 to +876
#[test]
fn test_metrics_total_voting_power() {
let mut governance = Governance::new(
Expand Down Expand Up @@ -1020,6 +1025,87 @@ mod metrics_tests {
// We assert that decided proposals are filtered out from metrics
assert!(!s.contains("proposal_id=\"2\""));
}

#[test]
fn test_metrics_maturity_modulation_and_icp_price_history() {
const METRIC_MATURITY_MODULATION: &str =
"governance_maturity_modulation_updated_at_timestamp_seconds";
const METRIC_MISSING_DAYS: &str = "governance_icp_xdr_price_history_missing_days_in_window";

let current_day: u64 = 20_000;
let now_seconds = current_day * ONE_DAY_SECONDS;

// Case 1: both states are None — neither gauge should be emitted.
let governance = Governance::new(
Default::default(),
Arc::new(MockEnvironment::new(Default::default(), now_seconds)),
Arc::new(StubIcpLedger {}),
Arc::new(StubCMC {}),
Box::new(MockRandomness::new()),
);
let mut writer = ic_metrics_encoder::MetricsEncoder::new(vec![], 0);
encode_metrics(&governance, &mut writer).unwrap();
let body = writer.into_inner();
let s = String::from_utf8_lossy(&body);
assert!(
!s.contains(METRIC_MATURITY_MODULATION),
"expected no maturity modulation gauge when state is None, got:\n{s}",
);
assert!(
!s.contains(METRIC_MISSING_DAYS),
"expected no missing-days gauge when state is None, got:\n{s}",
);

// Case 2: both states are Some.
let mut governance = Governance::new(
Default::default(),
Arc::new(MockEnvironment::new(Default::default(), now_seconds)),
Arc::new(StubIcpLedger {}),
Arc::new(StubCMC {}),
Box::new(MockRandomness::new()),
);
let updated_at_days_since_epoch = current_day - 2;
governance.heap_data.maturity_modulation = Some(MaturityModulation {
current_value_permyriad: Some(42),
updated_at_days_since_epoch: Some(updated_at_days_since_epoch),
});
// Three of the last 365 days have entries; one stale entry outside the window must not
// count toward `days_present`.
governance.heap_data.icp_price_history = Some(IcpPriceHistory {
icp_xdr_rates: vec![
SampledPrice {
timestamp_seconds: (current_day - 500) * ONE_DAY_SECONDS,
xdr_permyriad_per_icp: 50_000,
},
SampledPrice {
timestamp_seconds: (current_day - 2) * ONE_DAY_SECONDS,
xdr_permyriad_per_icp: 50_000,
},
SampledPrice {
timestamp_seconds: (current_day - 1) * ONE_DAY_SECONDS,
xdr_permyriad_per_icp: 50_000,
},
SampledPrice {
timestamp_seconds: current_day * ONE_DAY_SECONDS,
xdr_permyriad_per_icp: 50_000,
},
],
});
let mut writer = ic_metrics_encoder::MetricsEncoder::new(vec![], 0);
encode_metrics(&governance, &mut writer).unwrap();
let body = writer.into_inner();
let s = String::from_utf8_lossy(&body);
let expected_ts = updated_at_days_since_epoch * ONE_DAY_SECONDS;
assert!(
s.contains(&format!("{METRIC_MATURITY_MODULATION} {expected_ts} 0")),
"expected maturity modulation gauge with value {expected_ts}, got:\n{s}",
);
let expected_missing_days = 365_u64 - 3;
assert!(
s.contains(&format!("{METRIC_MISSING_DAYS} {expected_missing_days} 0")),
"expected missing-days gauge with value {expected_missing_days}, got:\n{s}",
);
}
}

mod neuron_archiving_tests {
Expand Down
41 changes: 40 additions & 1 deletion rs/nns/governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ use std::{
time::{Duration, SystemTime},
};
use storage::VOTING_POWER_SNAPSHOTS;
use timer_tasks::encode_timer_task_metrics;
use timer_tasks::{ONE_DAY_SECONDS, encode_timer_task_metrics};

Comment on lines 143 to 145
#[cfg(any(test, feature = "canbench-rs"))]
pub mod test_utils;
Expand Down Expand Up @@ -702,6 +702,45 @@ pub fn encode_metrics(
"Time since the latest voting power snapshot, in seconds. If no snapshot has been taken yet, this will be infinity.",
)?;

// Maturity Modulation / ICP-XDR price history freshness. Skipped entirely when the underlying
// state is unset so that alerts do not fire on freshly-installed canisters before the initial
// backfill completes.

if let Some(updated_at_days_since_epoch) = governance
.heap_data
.maturity_modulation
.as_ref()
.and_then(|mm| mm.updated_at_days_since_epoch)
{
w.encode_gauge(
"governance_maturity_modulation_updated_at_timestamp_seconds",
(updated_at_days_since_epoch * ONE_DAY_SECONDS) as f64,
"Timestamp (seconds since the Unix epoch) of the day for which the cached maturity modulation was last computed.",
)?;
}

if let Some(icp_price_history) = governance.heap_data.icp_price_history.as_ref() {
const ICP_XDR_PRICE_HISTORY_WINDOW_DAYS: u64 = 365;
let current_day = governance.env.now() / ONE_DAY_SECONDS;
let oldest_day_in_window =
current_day.saturating_sub(ICP_XDR_PRICE_HISTORY_WINDOW_DAYS - 1);
let days_present_in_window = icp_price_history
.icp_xdr_rates
.iter()
.filter(|p| {
let day = p.timestamp_seconds / ONE_DAY_SECONDS;
day >= oldest_day_in_window && day <= current_day
})
.count() as u64;
let missing_days_in_window =
ICP_XDR_PRICE_HISTORY_WINDOW_DAYS.saturating_sub(days_present_in_window);
w.encode_gauge(
"governance_icp_xdr_price_history_missing_days_in_window",
missing_days_in_window as f64,
"Number of days in [today-364, today] with no ICP/XDR rate entry in icp_price_history.",
)?;
}

// Periodically Calculated (almost entirely detailed neuron breakdowns/rollups)

if let Some(metrics) = &governance.heap_data.metrics {
Expand Down
1 change: 1 addition & 0 deletions rs/nns/governance/src/timer_tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use update_icp_xdr_rate_related_data::UpdateIcpXdrRateRelatedData;

pub(crate) use update_icp_xdr_rate_related_data::{
MATURITY_MODULATION_MAX_PERMYRIAD_MISSION_70, MATURITY_MODULATION_MIN_PERMYRIAD_MISSION_70,
ONE_DAY_SECONDS,
Comment on lines 17 to +19
};

use crate::{canister_state::GOVERNANCE, storage::VOTING_POWER_SNAPSHOTS};
Expand Down
10 changes: 10 additions & 0 deletions rs/nns/governance/unreleased_changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ on the process that this file is part of, see

## Added

* Added two Prometheus gauges to the `/metrics` endpoint to expose maturity modulation
freshness:
- `governance_maturity_modulation_updated_at_timestamp_seconds` — the day for which the
cached maturity modulation was last computed, multiplied by 86400.
- `governance_icp_xdr_price_history_missing_days_in_window` — the number of days in
`[today-364, today]` with no entry in `icp_price_history.icp_xdr_rates`.

Both gauges are skipped entirely until the underlying state is populated, so a
freshly-installed canister does not trip alerts before the initial backfill completes.

## Changed

## Deprecated
Expand Down
Loading