Skip to content

Fix five built-in alert rules that could never fire - #418

Open
dpage wants to merge 2 commits into
mainfrom
fix/issue-406-dead-alert-rules
Open

Fix five built-in alert rules that could never fire#418
dpage wants to merge 2 commits into
mainfrom
fix/issue-406-dead-alert-rules

Conversation

@dpage

@dpage dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Five built-in alert rules were incapable of firing, each for a different
reason, and all five failed silently: a metric query error is logged at
debug level in evaluateRuleForAllConnections and otherwise discarded,
so a broken rule looks exactly like a quiet one. Every root cause was
reproduced against a live PostgreSQL 18 running the real collector
SchemaManager schema before it was fixed.

  • wal_archive_failed selected FROM metrics.pg_stat_archiver, which
    the collector has never created; applying the schema to a fresh
    database yields 36 metrics.* tables and no views, and the archiver
    counters are consolidated onto metrics.pg_stat_wal. Every evaluation
    raised 42P01, so WAL archiving failure has never been detectable.
    The query now reads metrics.pg_stat_wal.

  • transaction_wraparound evaluated a query that joined
    pg_stat_all_tables and pg_settings, discarded both, and returned
    the literal 50.0 against a threshold of 75; seeding tables at 1 and
    5e9 live tuples still returned exactly 50.0. metrics.pg_stat_all_tables
    has no frozen-xid column, so the value now comes from
    metrics.pg_database.age_datfrozenxid as a percentage of the 2^31-1
    wraparound limit, matching the XID Age tile and the server's
    performance summary. Template databases are excluded.

  • high_max_connections and connection_utilization required a
    metrics.pg_settings row newer than one hour, but the settings probe
    is change-tracked and skips the write when the hash is unchanged, with
    no heartbeat; a stable server stores one snapshot at onboarding and
    nothing after, and both metrics returned no data found once that row
    passed 61 minutes. Both now take the newest row per connection, as the
    historical variants already did. The same window was quietly defeating
    table_last_autovacuum_hours, which fell through its COALESCE
    defaults and ignored tuned autovacuum_vacuum_threshold and scale
    factor, so that query is fixed alongside them.

  • cpu_usage_high keyed on processor_time_percent, which
    system_stats populates only on Windows; on Linux it is NULL and the
    COALESCE yielded 0, so a 95% busy host reported 0. A shared
    expression now prefers the Windows column, falls back to
    100 - idle_mode_percent, and finally sums the per-mode buckets,
    clamped to 0-100.

  • checkpoint_warning compared "more than 50 requested checkpoints"
    against the largest increase between two consecutive samples of a 600
    second probe, and returned no row at all when only one sample fell
    inside its 15 minute window. Both this metric and the archiver metric
    now sum positive per-sample deltas over an hour, which survives a
    statistics reset and reports 0 rather than vanishing on a single
    sample.

Collector migration 8 realigns the seeded rules with those semantics. It
rewrites descriptions and units unconditionally, but only rewrites the
checkpoint threshold (50 to 12 per hour) where the row still carries the
old shipped default, so operator tuning survives the upgrade. The v1
seed data carries the same values for fresh installs.

Interaction with the other open alerter PRs

Based on origin/main, and deliberately confined to
metric_registry.go and the seeded rules; nothing here touches
engine/cleanup.go or engine/thresholds.go, so there is no overlap
with #412.

#410 asserts the current defective behaviour, and this change
legitimately invalidates four of its TestAudit* tests. None of them
should be deleted or weakened; they should be inverted to assert the
fixed behaviour:

  • TestAuditC2ArchiverTableDoesNotExist and
    TestAuditC2ArchiverRuleErrorIsSwallowed: the first subtest, that the
    collector never creates metrics.pg_stat_archiver, stays true and
    should be kept. The assertions that the metric errors, and that the
    error is swallowed, no longer hold; the metric now returns a value
    from metrics.pg_stat_wal. TestDeadRuleArchiverMetricReadsPgStatWal
    in this PR pins the replacement behaviour.

  • TestAuditC3TransactionWraparoundReturnsConstant: age_percent no
    longer returns 50.0. Invert it to assert the value tracks
    age_datfrozenxid, as TestDeadRuleTransactionWraparoundFires does.

  • TestAuditC4PgSettingsMetricsExpireAfterOneHour: both metrics now
    resolve from a snapshot older than an hour. Invert it to assert
    survival, as TestDeadRulePgSettingsMetricsSurviveStaleSnapshot does.
    The collector's TestPgSettingsProbe_StoreUnchanged is unaffected,
    because the probe's change-tracking behaviour is unchanged; only the
    alerter's assumption about it changed.

  • TestAuditC5CPUUsageNullProcessorTimeReadsZero: a NULL
    processor_time_percent no longer reads 0. Invert it to assert the
    Linux fallback, as TestDeadRuleCPUUsageFiresOnLinux and
    TestDeadRuleCPUUsageWindowsAndIdleFallbacks do.

TestAuditC7HistoricalSQLCoverage is unaffected: age_percent and both
delta metrics still have no historicalSQL, and backfilling those is
out of scope here. The other TestAudit* tests cover behaviour this PR
does not touch.

Test plan

  • New alerter/src/internal/database/dead_alert_rules_integration_test.go
    (13 tests) seeds each rule's scenario and asserts the metric now
    returns a value that crosses the rule's shipped default threshold, and
    also covers the stats-reset, single-sample, Windows, idle-fallback and
    bucket-sum branches.
  • New collector/src/database/migration_v8_test.go (4 tests) covers the
    fresh-install values, the upgrade path over rewound legacy rows, the
    preservation of a tuned threshold, and the migration's error path.
  • collector/src/database/migration_v7_test.go and schema_test.go
    updated for the new migration count; Migrate compares against the
    highest recorded version, so the v7 rewind now deletes rows >= 7.
  • queries_integration_test.go's pg_sys_cpu_usage_info fixture gained
    the Linux columns the portable CPU expression reads.
  • cd alerter && make coverage: all tests pass, internal/database at
    86.4%. The registry change is a package-level map and const literal
    with no coverable statements; it is exercised end-to-end by the new
    integration tests.
  • cd collector && make coverage: all tests pass, src/database at
    81.2%; migration 8's Up is at 100% (5/5 statements).
  • make lint clean for both collector and alerter.
  • make test-all from the root: collector, alerter and the rest of the
    server suite pass. The two pre-existing server/internal/tools memory
    embedding failures (expected 3 dimensions, not 4000) are unrelated
    and are the subject of fix(server): widen memory embedding test fixture to halfvec(4000) #382.

Closes #406

Each of the five rules failed for a different reason, and every one of
them failed silently, because a metric query error is logged at debug
level by evaluateRuleForAllConnections and otherwise ignored; a broken
rule is indistinguishable from a quiet one.

wal_archive_failed selected FROM metrics.pg_stat_archiver, a relation
the collector has never created: the archiver counters are consolidated
onto metrics.pg_stat_wal, so every evaluation raised 42P01 and WAL
archiving failures have never been detectable. The query now reads
metrics.pg_stat_wal.

transaction_wraparound evaluated a query that joined pg_stat_all_tables
and pg_settings, discarded both, and returned the literal 50.0 against
a threshold of 75. metrics.pg_stat_all_tables carries no frozen-xid
column, so the value now comes from metrics.pg_database.age_datfrozenxid
and is expressed as a percentage of the 2^31-1 wraparound limit, which
matches the definition the dashboard's XID Age tile and the server's
performance summary already use. Template databases are excluded.

high_max_connections and connection_utilization both required a
metrics.pg_settings row written within the last hour, but the settings
probe is change-tracked and skips the write whenever the settings hash
is unchanged, with no max-age heartbeat; a stable server therefore
receives one snapshot at onboarding and nothing after, and both metrics
died within the hour. They now take the newest row per connection, as
the historical variants already did. The same one-hour window was
silently defeating table_last_autovacuum_hours, which fell back through
its COALESCE defaults and ignored tuned autovacuum settings, so that
query is fixed alongside them.

cpu_usage_high keyed on processor_time_percent, which the system_stats
extension populates only on Windows; on Linux the column is NULL and
the COALESCE yielded 0, so the rule read an idle host on a saturated
one. A shared expression now prefers the Windows column, falls back to
100 minus idle_mode_percent, and finally sums the per-mode buckets.

checkpoint_warning compared "more than 50 requested checkpoints" against
the largest increase between two consecutive samples of a 600 second
probe, which made the threshold unreachable, and its window held two
samples only about half the time, so the rule evaluated intermittently.
Both this metric and the archiver metric now sum positive per-sample
deltas over an hour, which survives a statistics reset and reports zero
rather than vanishing when only one sample lands in the window.

Collector migration 8 realigns the seeded rules with those semantics:
it rewrites the descriptions and units unconditionally, but only
rewrites the checkpoint threshold where the row still carries the old
shipped default of 50, so operator tuning survives the upgrade.

Closes #406
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8fde5245-983f-49cd-93fa-6bebfa40da18

📥 Commits

Reviewing files that changed from the base of the PR and between 19c645d and 174fb39.

📒 Files selected for processing (10)
  • .claude/golang-expert/metrics-queries.md
  • alerter/src/internal/database/dead_alert_rules_integration_test.go
  • alerter/src/internal/database/metric_registry.go
  • alerter/src/internal/database/queries_integration_test.go
  • collector/src/database/migration_v7_test.go
  • collector/src/database/migration_v8_test.go
  • collector/src/database/schema.go
  • collector/src/database/schema_test.go
  • docs/changelog.md
  • docs/user-guide/alerts/rule-reference.md

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 112 complexity · 35 duplication

Metric Results
Complexity 112
Duplication 35

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

The metrics.* tables do carry a cascading foreign key to connections
now, added by addConstraintIfMissing in the consolidated migration, so
the note claiming they deliberately carry none was wrong. The
orphan-filtering convention stays, because the joins cost nothing and
the queries that predate the constraint still rely on them.
@dpage

dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dpage

dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dpage

dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Five built-in alert rules can never fire

1 participant