Home / Stories / Alert reaction
Story · Incidents Live

An alert fires at night. In the morning, the fix is waiting for review.

Silencing an alert is easy. Finding out why it fired, and changing the configuration so it does not fire again, is the part that gets postponed. The assistant picks up each new alert, investigates it with read-only tools, and asks one question before it writes anything: could a change in git have prevented this?

Starts fromA firing alert from PagerDuty, Alertmanager, or Prometheus alert history, polled every 15 minutes.
Looks atThe alert rule, the workload's manifest in git, live pod state and events, metric history, and container logs where you allow it.
You getA draft MR when a manifest change addresses the cause; otherwise an issue with the diagnosis and the exact commands to finish it.
01 · The alert comes in

From wherever your alerts already live.

Every 15 minutes the assistant collects alerts from PagerDuty, from Alertmanager (what is firing right now, with silences), and from Prometheus alert history (which catches alerts that flap faster than a page).

Each alert is handled once. If the same rule fires on twenty filesystems, those collapse into one "this is widespread" case instead of twenty investigations.

02 · Find what it is about

From alert labels to a file in git.

The alert's target (kind, namespace, name) is resolved from its labels, and a snapshot of the live object and its git manifest is fetched before the investigation starts. If the alert body is thin, the rule definition itself is read.

If no tracked repository declares the workload, the outcome is already known: an issue, never an edit to an unrelated file.

03 · Look at the pod

Describe, restart history, events.

The investigation is a loop of read-only tool calls against the cluster. The pod's last state says OOMKilled, exit code 137, seven restarts. Events confirm the pattern: back-off, restart, back-off.

The cluster identity behind these tools can read. It cannot change anything.

04 · Check the history

A range query shows the shape of the problem.

Memory working set over the last two days: it climbs every night during the catalogue reload, touches the 512 MiB limit, and the container is killed. Daytime use is well below.

That is not a leak (it drops back each time) and not random. It is a known peak that no longer fits.

05 · Read the logs, if allowed

The previous container's last lines.

Where you have enabled log access for a cluster, the assistant reads a short tail of the previous container, the one that died. Here the last line before the kill is the catalogue reload with a much larger item count than a year ago.

Without log access it does not guess. It says what it could not see and hands you the command to look.

06 · The deciding question

Could a manifest change have prevented this?

Root cause: the catalogue grew, the reload's peak outgrew a memory limit set long ago. A limit is a manifest value, so the answer is yes, and the output is a draft MR.

Had the cause been outside git (a node problem, a workload in a repository you do not track, a bug that needs a code change), the answer would be no, and speculative "bump it just in case" fixes are explicitly not allowed.

07 · Gates before it ships

Right workload, right kind of fix, real numbers.

Deterministic checks run on the drafted fix before anyone sees it:

  • it edits the workload that alerted, not a similarly named one,
  • the kind of change can address this class of alert (memory for an OOM, not a probe tweak),
  • the file is actually applied by Flux, not an orphan,
  • new memory values go through the same measured-usage clamps as right-sizing.

Anything that fails is held for human review with the reason named.

08 · Two possible outcomes

An MR when git can fix it. An issue when it cannot.

The MR carries the alert, the evidence trail and the reasoning. The issue carries what was observed, the copy-paste commands to finish the diagnosis, and the likely fix for each outcome.

The loop does not end at the proposal. An issue whose alert stays quiet for long enough is closed with a note. A merged fix whose alert keeps firing is escalated with a link back to the MR.

KubePodCrashLooping · critical
namespace=shop pod=orders-api-7d9f8c-x2kqp container=api
Pod is restarting 3.5 times / 10 minutes · firing since 02:14
PagerDutyincidents
Alertmanagerfiring now
Prometheusalert history
Assistantevery 15 min
not processed yet · 3rd firing for this workload in 7 days
investigation · step 1–2 · read-only
 prometheus_alert_rule("KubePodCrashLooping")
  expr: max_over_time(kube_pod_container_status_waiting_reason{
        reason="CrashLoopBackOff"}[5m]) >= 1
 read_workload_manifest_from_git(Deployment, shop, orders-api)
  found: shop-deploy · apps/shop/orders-api/deployment.yaml
  resources.limits.memory: 512Mi   requests.memory: 384Mi
  last change to resources: 14 months ago
investigation · step 3 · read-only
 kubernetes_describe_object(Pod, shop, orders-api-7d9f8c-x2kqp)
  lastState.terminated:
    reason: OOMKilled   exitCode: 137
  restartCount: 7
 workload_restart_history(shop, orders-api)
  7 restarts / 48h · all OOMKilled · all between 02:00 and 02:20
 kubernetes_list_events(shop, orders-api)
  BackOff   Back-off restarting failed container api   ×23
  Killing   Container api exceeded its memory limit    ×7
prometheus_query_range · working set · orders-api · 48h
limit 512Mi 256Mi 02:10 reload 02:10 reload OOMKilled OOMKilled
daytime ≈ 300Mi · nightly reload hits the limit · drops back after restart: not a leak
kubernetes_pod_logs · previous container · tail 40opt-in per cluster
02:10:03 INFO  scheduler: starting nightly catalogue reload
02:10:04 INFO  catalogue: fetching snapshot from products-db
02:10:41 INFO  catalogue: decoded 1,940,212 items (prev 1,211,480)
02:10:58 INFO  catalogue: building search index
        (container terminated: OOMKilled)
catalogue grew ~60% · the reload holds old and new copies while it swaps
Could a manifest change have prevented this?root cause: reload peak outgrew a 14-month-old memory limit
Yes → draft MRRaise the memory limit and request for orders-api in deployment.yaml, sized from the measured peak.
No → issueWhat was observed, the kubectl commands to finish the diagnosis, and the likely fix for each thing they might show.
emit-time gates · deterministic
  • edit target = alert subject shop/orders-api ↔ shop/orders-api
  • fix axis fits alert class memory ↔ OOM crash loop
  • file is reconciled by Flux in kustomization closure
  • memory values vs measured usage peak floor · 5× cap · req ≤ lim
  • not a repeat of a fix you rejected in development
all gates passed · proposal goes to review as a draft MR
Draft · Raise memory for shop/orders-api after nightly OOMKills
        resources:
          requests:
-           memory: 384Mi
+           memory: 512Mi
          limits:
-           memory: 512Mi
+           memory: 768Mi
or · Issue: KubeletDown on node-pool-b (managed node)
no tracked manifest owns this · diagnosis + commands to run, no diff
later · alert quiet for 3 sweeps → issue closed with a note
merged fix still firing → escalated, linked back to the MR

The merge request you get.

Illustrative, shaped like the real output: the alert, the investigation trail and the sizing reasoning travel with the one-file change.

Draft

Raise memory for shop/orders-api after nightly OOMKills

from gitops-ai/alert-orders-api-oomkilled-5c81e0a2 into main · 1 file · +2 −2
category:resourcessource:alertrisk:mediumxrev::LGTM
OverviewInvestigationChangesAfter merge
Alert

KubePodCrashLooping (critical) on shop/orders-api, container api, firing since 02:14. Third firing in 7 days.

Root cause

The container is OOMKilled during the nightly catalogue reload (02:10). The catalogue grew from about 1.2M to 1.9M items; the reload briefly holds the old and new copies, and the peak now exceeds the 512 Mi limit set 14 months ago. Daytime usage (≈300 Mi) is unaffected. Memory drops back after each restart, so this is not a leak.

Change

Request 384 Mi → 512 Mi, limit 512 Mi → 768 Mi. The observed peak is capped by the old limit, so the true need is not known exactly: the new limit leaves 1.5× the last observed peak, and the post-merge checks below confirm it.

Not in this MR

Streaming the reload instead of double-buffering it would remove the peak altogether. That is an application change and is noted here for the service owners, not proposed.

#Tool (read-only)Finding
1prometheus_alert_ruleCrash-loop rule, 5 min window
2read_workload_manifest_from_gitapps/shop/orders-api/deployment.yaml, limit 512Mi
3kubernetes_describe_objectlastState OOMKilled, exit 137, 7 restarts
4workload_restart_historyAll 7 restarts between 02:00 and 02:20
5kubernetes_list_eventsBackOff ×23, memory-limit kills ×7
6prometheus_query_rangeNightly climb to the limit, daytime ≈300Mi
7kubernetes_pod_logs (previous)Killed while building the index after reload

Second pass (OOM class): confirmed under-resourced rather than leaking; memory returns to baseline after every restart.

     containers:
       - name: api
         resources:
           requests:
             cpu: 250m
-            memory: 384Mi
+            memory: 512Mi
           limits:
-            memory: 512Mi
+            memory: 768Mi
After merge, expect
  • Flux rolls orders-api; pods restart once with the new limits.
  • Tonight's 02:10 reload completes: no OOMKilled in kubectl get pod -n shop -l app.kubernetes.io/name=orders-api after 02:30.
  • Peak working set during the reload stays under 768 Mi; note the real peak for the next sizing pass.
  • KubePodCrashLooping for shop/orders-api stays resolved.
If it fires again

The assistant watches for it: a merged fix whose alert keeps firing gets an escalation issue and a comment on this MR.

R
independent reviewer second opinion, no pipeline context

Restart times match the reload window in the cited logs; the change touches only the alerted container's memory. Request stays below limit. LGTM.

What keeps it honest.

An alert is a symptom. Most of the rules below exist to stop a confident-sounding fix for the wrong thing.

Read-only investigationEvery tool in the loop reads. Log access is off unless you enable it per cluster.
No evidence, no fixIf the diagnosis needs something the tools cannot see, the result is an issue with a copy-paste command and the likely fix per outcome, not a guess in a diff.
First firing is not a patternA single occurrence gets the direct fix. Structural changes, like a new autoscaler, wait until the alert has proven it recurs.
Right target onlyAn alert without a matching workload never edits a topically related file. A fix aimed at a different workload than the one that alerted is held for review.
Fix fits the alertA change that cannot address this class of alert, whatever the model's reasoning, is held for review.
Closed at the causeIssues close only after the alert has been quiet across several sweeps, and only when every alert source could be read.

Where this stands.

Live
  • PagerDuty, Alertmanager and Prometheus alert-history sources
  • Read-only tool-loop investigation: rules, manifests, describe, events, metrics, opt-in logs
  • Second pass for OOM, crash loop, volume filling and stuck rollouts
  • Draft MR or diagnosis issue, with emit-time gates
  • Escalation when a merged fix does not stop the alert
  • Auto-closing issues whose alert has stayed quiet
In development
  • Checking your earlier feedback on the same workload before proposing
  • Leading indicators: acting on an autoscaler near its max or a volume filling up before the alert fires
  • Linking alerts that fire together from one cause (annotation mode)
Roadmap
  • Collapsing a cascade of related alerts into a single proposal for the root
← Previous storyFlux v1 to v2 migration