Skip to content

[Feature]: expose change stream consumer lag; a consumer 11 days behind presents as completely healthy #1709

Description

@lfriedman-netllama

Prerequisites

  • I searched existing issues

Code of Conduct

  • I agree to follow NVSentinel's Code of Conduct

Feature Summary

Expose change stream consumer lag as a metric in the shared store-client, so every
change stream consumer gets it rather than each module reinventing it. Split out of #1704 at
@lalitadithya's suggestion, since it applies to all consumers and not just
health-events-analyzer.

Problem/Use Case

On a 288 node GB200 fleet, health-events-analyzer fell 11 days and 2.4 million events
behind
its change stream. Nothing surfaced it. The pod presented as completely healthy:

  • Running, zero restarts, uptime 6 days
  • 2m CPU against a 1000m limit, 67Mi against 1Gi, so no throttling or OOM signal
  • no errors in logs, and its own success counters climbing normally
  • rule_matched_total incrementing, so it looked like it was working

It was working. Just on events from 11 days earlier. We only found it because the stale
pattern events it emitted looked like a current hardware fault, and chasing that down led to
decoding the resume token by hand.

The existing metric does not catch this. event-exporter has
ResumeTokenUpdateTimestamp (event-exporter/pkg/metrics/metrics.go:80, set at
worker_pool.go:171), but it records when the token was last written, not where the
stream position is
. Our analyzer was persisting tokens continuously and successfully the
entire time it was 11 days behind, so a freshness gauge on token writes would have read as
perfectly healthy throughout. The two are independent: a consumer can commit progress every
few seconds while falling further behind every minute.

Proposed Solution

Emit the stream position as a gauge from store-client, where the token is already
loaded and stored, so it lands once and covers every consumer:

changestream_position_timestamp_seconds{client="health-events-analyzer"}

Lag is then time() - changestream_position_timestamp_seconds, which alerts cleanly:

time() - changestream_position_timestamp_seconds > 900

For MongoDB this needs no extra query. The resume token's _data encodes the BSON timestamp
in its leading bytes, so the position is derivable from the token the consumer already holds:
the seconds are parseInt(_data[2:10], 16). We decoded ours exactly that way to confirm the
2026-08-21 position. The PostgreSQL provider tracks a monotonic event ID rather than a
timestamp, so it may need its own derivation, or a changestream_events_behind{client} gauge
instead.

Natural home is alongside the existing token load and store in
store-client/pkg/datastore/providers/mongodb/watcher/watch_store.go, so consumers get it
without code changes.

Why this is worth doing generically

Every consumer of this change stream has the same failure mode, and it is silent in all of
them. On our fleet the consumers are health-events-analyzer, fault-quarantine, node-drainer
and fault-remediation, plus event-exporter when enabled. A consumer that silently stops
keeping up does not fail loudly; it just makes decisions on stale data, which for the action
modules would mean quarantining or rebooting nodes based on faults that are days old.

Related: #1594 raises the oplog window for change stream consumers, which is the other half of
this. If a consumer falls behind the oplog window it cannot resume at all, and today there is
no metric that would warn you as it approached that edge.

Acceptance criteria

  • A gauge exposes the change stream position per consumer, labeled by client name.
  • It is emitted from store-client so every consumer gets it without per-module code.
  • The value updates as the stream advances, and does not merely reflect token write time.
  • MongoDB derives the position from the existing resume token, with no extra query per event.
  • The PostgreSQL provider either derives an equivalent or documents why it differs.
  • Documentation includes an example lag alert.
  • Unit tests cover position extraction, including a token from a known timestamp.

Component

Other

Activity

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

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions