Skip to content

apollo_dashboard: alert on the Chainlink oracle guard trips - #14948

Open
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-05-select-oracle-sourcefrom
asaf/l1-oracle-08-oracle-staleness-alert
Open

apollo_dashboard: alert on the Chainlink oracle guard trips#14948
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-05-select-oracle-sourcefrom
asaf/l1-oracle-08-oracle-staleness-alert

Conversation

@asaf-sw

@asaf-sw asaf-sw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Surfaces two 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 falling outside the freshness window.
    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. The counter covers both directions, since
    a timestamp too far in the future is the poisoning case the guard exists to
    reject, an unbounded updated_at otherwise reading 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 two earn
their own because they name a cause the aggregate cannot.

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

Surfaces two 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 falling outside the freshness window.
  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. The counter covers both directions, since
  a timestamp too far in the future is the poisoning case the guard exists to
  reject, an unbounded `updated_at` otherwise reading 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 two earn
their own because they name a cause the aggregate cannot.

`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>
@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

asaf-sw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 1c03576. Configure here.

EvaluationRate::Default,
format!("{} or vector(0)", sum_increase(guard_trip_metric, GUARD_SAMPLING_WINDOW)),
vec![AlertCondition::new(AlertComparisonOp::GreaterThan, 0.0, AlertLogicalOp::And)],
GUARD_PENDING_DURATION,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pending duration fails to filter singles

Medium Severity

GUARD_PENDING_DURATION is documented to keep a single guard trip quiet, and guard_trip_alert is described as firing while rejections continue, but sum(increase(...[1h])) &gt; 0 stays true for about an hour after one increment. The 10m for only delays the page; it does not require persistence, and the alert can remain firing long after the feed recovers.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1c03576. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants