apollo_dashboard: alert on the Chainlink oracle guard trips - #14948
apollo_dashboard: alert on the Chainlink oracle guard trips#14948asaf-sw wants to merge 1 commit into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
666c587 to
fb8da64
Compare
0455769 to
263e2f4
Compare
fb8da64 to
3959ddb
Compare
3959ddb to
c5be2f0
Compare
263e2f4 to
81c5c6c
Compare
c5be2f0 to
18d139e
Compare
81c5c6c to
421b4d4
Compare
18d139e to
e731706
Compare
421b4d4 to
4592d64
Compare
matanl-starkware
left a comment
There was a problem hiding this comment.
@matanl-starkware reviewed 5 files and all commit messages, and made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on asaf-sw).
crates/apollo_dashboard/src/alert_scenarios/chainlink_oracle.rs line 44 at r2 (raw file):
"Chainlink oracle feed outside the freshness window", &CHAINLINK_ORACLE_STALE_FEED_COUNT, AlertSeverity::WorkingHours,
The severity should be defined per env (by replacer)
Code quote:
AlertSeverity::WorkingHours,4592d64 to
0ac9247
Compare
e731706 to
a3c6dff
Compare
0ac9247 to
e00c341
Compare
a3c6dff to
38afebd
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e00c341. Configure here.
e00c341 to
3d1b2df
Compare
3f29042 to
44fc36a
Compare
3d1b2df to
b2f16f6
Compare
ce424ff to
8c0f75d
Compare
c42e211 to
695ba3e
Compare
8c0f75d to
bed0d19
Compare
695ba3e to
0c9e86b
Compare
bed0d19 to
59d62fe
Compare
0c9e86b to
3699473
Compare
59d62fe to
b104573
Compare
3699473 to
eeac62e
Compare
b104573 to
d45e396
Compare
eeac62e to
08bbb1d
Compare
d45e396 to
851f7c7
Compare
08bbb1d to
f315ddb
Compare
851f7c7 to
ccdea0c
Compare
f315ddb to
4f379db
Compare
Surfaces three of the Chainlink client's guards to on-call, on top of the config switch that makes the client selectable. - P4 when a feed reading is rejected for being older than the staleness bound. This is Ohad's "alert if price isn't fresh from the last day": the feeds guarantee an update every 24h, so a rejection means the feed stopped publishing or its `updated_at` is implausible. - P4 when a feed reading is rejected for being dated ahead of the block being priced. Same class of failure, hence the same severity. It gets its own alert because the client counts the two directions separately, and this is the poisoning case the guard exists to reject: an unbounded `updated_at` otherwise reads as permanently fresh. - P3 when a rate is rejected for falling outside the configured sanity bounds. Higher, because this is the wrong-feed and poisoned-feed detector, and the one guard consensus cannot substitute for: validators check only that they agree with each other, and every node reads the same chain state, so a plausible but wrong price produces unanimous agreement. The other two guards get no alert. Every guard trip also increments the pair's `error_count`, which already carries a paging alert, so invalid-answer and contract-call trips would page twice for a case that alert covers. These three earn their own because they name a cause the aggregate cannot. Every query groups by the guard counters' `currency_pair` label, so the page names the rejected reading. The ETH/STRK rate reads both USD feeds, so an aggregate over the label would say a feed went stale without saying which. The `or vector(0)` fallback carries no `currency_pair` label, so once a counter exists it joins the per-pair instances as an unlabeled one holding at zero. `or vector(0)` keeps each query defined before its counter first exists, since `sum` over an empty vector returns empty rather than zero and the generated rules page on their no-data state. That is also why these apply to observers where the sibling oracle alerts do not: `NotApplicable` wraps the whole expression in an `is_observer` gate that discards the fallback wherever that metric is absent. Observers read the same feeds, so a trip is environment-wide either way. The counters stay flat while a feed is served over HTTP, so these alerts are silent until a feed is switched to Chainlink. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ccdea0c to
a8f5a92
Compare
4f379db to
a016325
Compare
matanl-starkware
left a comment
There was a problem hiding this comment.
@matanl-starkware made 2 comments.
Reviewable status: 1 of 5 files reviewed, 3 unresolved discussions (waiting on asaf-sw).
crates/apollo_dashboard/src/alert_scenarios/chainlink_oracle.rs line 1 at r4 (raw file):
use apollo_l1_gas_price::metrics::{
I'm not sure we need a different file for chainlink alerts.
Let's try to consolidate all Oracle-related alerts into a single file, preferably the same alerts.
Code quote:
use apollo_l1_gas_price::metrics::{crates/apollo_dashboard/src/alert_scenarios/chainlink_oracle.rs line 37 at r4 (raw file):
/// bound. The feeds guarantee an update every 24h, so a rejection means the feed stopped publishing /// or its `updated_at` is implausible. pub(crate) fn get_chainlink_oracle_stale_feed_alert() -> Alert {
I should re-review this PR after the metrics are combined (all errors together)
Code quote:
get_chainlink_oracle_stale_feed_alert

Surfaces two of the Chainlink client's guards to on-call, on top of the config
switch that makes the client selectable.
This is Ohad's "alert if price isn't fresh from the last day": the feeds
guarantee an update every 24h, so a rejection means the feed stopped publishing
or its
updated_atis implausible. The counter covers both directions, sincea timestamp too far in the future is the poisoning case the guard exists to
reject, an unbounded
updated_atotherwise reading as permanently fresh.Higher, because this is the wrong-feed and poisoned-feed detector, and the one
guard consensus cannot substitute for: validators check only that they agree
with each other, and every node reads the same chain state, so a plausible but
wrong price produces unanimous agreement.
The other two guards get no alert. Every guard trip also increments the pair's
error_count, which already carries a paging alert, so invalid-answer andcontract-call trips would page twice for a case that alert covers. These two earn
their own because they name a cause the aggregate cannot.
or vector(0)keeps each query defined before its counter first exists, sincesumover an empty vector returns empty rather than zero and the generated rulespage on their no-data state. That is also why these apply to observers where the
sibling oracle alerts do not:
NotApplicablewraps the whole expression in anis_observergate that discards the fallback wherever that metric is absent.Observers read the same feeds, so a trip is environment-wide either way.
The counters stay flat while a feed is served over HTTP, so these alerts are
silent until a feed is switched to Chainlink.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com