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. The harder part is to find out why it fired, then change the configuration so it does not fire again. Teams often postpone that part. The assistant picks up each new alert and investigates it with read-only tools. Before it writes anything, it asks one question: could a change in git have prevented this?

Starts fromA firing alert from PagerDuty, Alertmanager or Prometheus alert history. The assistant polls 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 merge request (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 and Alertmanager (what is firing right now, with silences). It also reads Prometheus alert history, which catches alerts that flap faster than a page.

The assistant handles each alert once. If the same rule fires on twenty filesystems, the assistant merges them into one "this is widespread" case. It does not run twenty investigations.

02 · Find what it is about

From alert labels to a file in git.

The assistant resolves the alert's target (kind, namespace, name) from its labels. Before the investigation starts, it fetches a snapshot of the live object and its git manifest. If the alert body is thin, it reads the rule definition itself.

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 (the system stopped it for using too much memory), exit code 137, seven restarts. Events confirm the pattern of 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.

Over the last two days, the memory working set climbs every night during the catalogue reload. It touches the 512 MiB limit, and then the system kills the container. 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 shows the catalogue reload. The reload has 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, so the reload's peak outgrew a memory limit that someone set long ago. A limit is a manifest value, so the answer is yes, and the output is a draft MR.

Suppose the cause is outside git: a node problem, a workload in a repository you do not track, or a bug that needs a code change. Then the answer is no. The assistant does not allow speculative "bump it just in case" fixes.

07 · Gates before it ships

Right workload, right kind of fix, real numbers.

Before anyone sees the drafted fix, rule-based checks run on it. These checks are code, not an AI model (LLM):

  • 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.

If a check fails, the assistant holds the fix for human review and names the reason.

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. The assistant closes an issue with a note when its alert stays quiet for long enough. If a merged fix does not stop its alert, the assistant escalates and links 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. 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 old limit caps the observed peak, so the true need is not known exactly. The new limit leaves 1.5× the last observed peak. 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. The MR notes it for the service owners and does not propose it.

#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, not 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. If a merged fix does not stop its alert, the assistant opens an escalation issue and comments 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, not a guess in a diff. The issue has a copy-paste command and the likely fix per outcome.
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. If a fix targets a different workload than the one that alerted, the assistant holds it for review.
Fix fits the alertIf a change cannot address this class of alert, the assistant holds it for review, whatever reasoning the AI model gives.
Closed at the causeIssues close only when the alert has been quiet across several sweeps and the assistant could read every alert source.

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