Skip to content

feat: alert when an audit sidecar stops delivering - #227

Open
scotwells wants to merge 3 commits into
mainfrom
feat/sidecar-audit-alerting
Open

feat: alert when an audit sidecar stops delivering#227
scotwells wants to merge 3 commits into
mainfrom
feat/sidecar-audit-alerting

Conversation

@scotwells

@scotwells scotwells commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Production lost about 75% of its audit events for three hours today and nothing paged. Two of the three audit collectors stalled, the third kept working, and every alert averaged across all three — so the pipeline looked healthy the whole time. It was spotted by eye on a dashboard.

After this, a single stalled collector pages within about five minutes, and whoever answers gets a runbook that walks them from the alert to the fix.

While checking why nothing fired, four existing rules turned out to be incapable of firing at all — three referencing metrics that no longer exist, one comparing two things that can never match. A fifth was set to page only after roughly 83 minutes of a completely stopped consumer, a number chosen against a metric that never existed and so never checked against real traffic.

  • Two new alerts covering the collectors, evaluated per pod so one bad pod is visible behind two healthy ones
  • Four repaired rules, plus three more carrying the same stale metric name
  • The critical backlog alert now pages in minutes rather than an hour and a half
  • A runbook per new alert: what broke, what to check, how to recover, how to confirm the backlog cleared

Note

The backlog alert fires the moment this merges — a collector is still working through events left over from today's incident. That is correct, and it clears itself as the backlog drains.

Root cause of the outage is fixed separately. This is detection and response only; no application code changes.

Related to #226

Validation

Worth being precise about, since the bug being fixed is rules that quietly never fire — proving a rule stays quiet proves very little on its own.

Confirmed to fire on real failure data:

  • VectorSidecarNATSPublishStalled — went true at 18:50 and 18:52 UTC during today's outage, 5–6 minutes after each collector stalled
  • VectorSidecarBufferBacklog — firing now on genuine leftover backlog

Structurally verified only. The repaired rules cover conditions that have not occurred in 30 days, so there is no real failure to test them against. For each, the metric, labels, and thresholds were checked to be live and reachable — but none has been observed firing:

  • ActivityDataPipelineStalled, ActivityGenerationStalled — the quantities they watch have never touched zero (11.75/s and 11.24/s minimums over 30 days), so the rules are sound but unexercised
  • VectorClickHouseWritesStopped — label sets intersect on the three aggregator pods; collectors drop out on their own, having no ClickHouse stage
  • ActivityPipelineBacklogCritical, NATSConsumerLagHigh — consumer names match a real consumer, and the new 50k threshold sits 25,000x above the highest backlog seen in 30 days

Every new and repaired rule produces zero firings across 30 days of history.

Test plan

  • Both alert files parse and kustomize build config/components/observability is clean
  • Every metric, label value, and regex referenced by a new or edited rule confirmed present in production
  • Both runbook links resolve to files added here, and every command in them was run against production
  • No new or repaired rule fires against 30 days of healthy history
  • Confirm in staging that the rules load and appear in the alert list
  • Exercise the repaired rules against an induced failure in staging — none can be true-positive tested against production history

Warning

Two of these dead expressions also exist in the Jsonnet mixin. #155 needs to fix them there before it moves alert authoring into Jsonnet, or they will come back.

Production lost ~75% of its audit events for over three hours with no
alert. Two of three audit sidecars wedged while the third kept
delivering, which held every aggregate above zero and left the pipeline
looking healthy. Nothing covered the sidecar to NATS leg, and three of
the four rules meant to cover this pipeline could never fire.

Key changes:
- Add VectorSidecarNATSPublishStalled (critical): a sidecar still
  transforming events while its NATS sink consumes none. Evaluated per
  pod, so one stalled replica pages even when its siblings are healthy
- Add VectorSidecarBufferBacklog (warning): >25k events on the sink's
  disk buffer, an independent backstop with ~8x headroom over the
  healthy peak of 3,208
- Repoint ActivityDataPipelineStalled at
  vector_component_sent_events_total; vector_events_out_total has no
  series since the upstream rename
- Repoint the NATS backlog rules at nats_consumer_num_pending with
  stream_name/consumer_name; nats_jetstream_consumer_num_pending is not
  exported
- Rebase ActivityGenerationStalled on events evaluated rather than
  activities generated, aggregating both sides so their label sets
  match. Generation is policy-driven and legitimately zero for hours;
  evaluation is not
- Make VectorClickHouseWritesStopped per pod so a single stalled
  aggregator replica is caught rather than only a total stop

Every expression was validated against production: the sidecar rules
went true 5-6 minutes after each wedge and produced zero firings across
six days of healthy baseline.

Related to #226

Claude-Session: https://claude.ai/code/session_01C4BSNeKuaKVu44MVkLCnS6
@scotwells
scotwells force-pushed the feat/sidecar-audit-alerting branch from 0766082 to b8ca31a Compare August 24, 2026 22:06
The sidecar runbooks reached for the nats CLI and df, neither of which
exists in the clusters these alerts fire in. natsBox.enabled is false, no
nats-box workload is deployed, and the nats and vector images ship no
shell. An on-call responder following these steps hit a dead end at the
point they most needed the answer.

Key changes:
- Read stream state and max_payload from the NATS monitoring endpoint
  (/jsz, /varz) instead of the absent nats CLI
- Note that the reloader sidecar applies a max_payload change on its own,
  leaving the manual signal as a fallback
- Take buffer size from vector_buffer_byte_size, since the vector image
  has neither a shell nor df

Every replacement was run against production before being written down.

Claude-Session: https://claude.ai/code/session_01C4BSNeKuaKVu44MVkLCnS6
ActivityPipelineBacklogCritical fired at 500,000 pending events. This
stream carries roughly 100 events/s, so that is about 83 minutes of a
completely stopped consumer before a critical alert reaches anyone.

The threshold was written against a metric name that did not exist, so
it was never once evaluated against real throughput. Repairing the name
made the rule run; it did not make the number right.

50,000 is roughly 8 minutes of a stopped consumer, one order above the
existing 5k warning and 25,000x the highest backlog observed in 30 days,
so the change buys earlier warning without risking a false page.

Claude-Session: https://claude.ai/code/session_01C4BSNeKuaKVu44MVkLCnS6
@scotwells
scotwells requested a review from kevwilliams August 24, 2026 22:48
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.

1 participant