Incident triage for ML inference-services
Read-only external triage. No SSH, no kubectl. Sources:
- Prometheus via Grafana's anonymous datasource proxy (queries.md).
- Gerrit MCP for
machinelearning/liftwing/inference-servicesandoperations/deployment-charts. - Phabricator MCP — only on task IDs from CL
Bug: T...footers (don't search Phab; tags are unreliable).
Cluster mapping
prometheus alone is ambiguous (k8s-mlserve covers eqiad and codfw). Always pair with site.
| Cluster | site |
prometheus |
|---|---|---|
| ml-serve-eqiad | eqiad |
k8s-mlserve |
| ml-serve-codfw | codfw |
k8s-mlserve |
| ml-staging-codfw | codfw |
k8s-mlstaging |
For health-check sweeps with no specific target, fan out to all three in parallel.
Time window
Every investigation centres on a single timestamp T (now or past). Always use range queries (/api/v1/query_range, step=60); a live check is a range with T = now. Default window: [T - 2h, T + 2h], clamped at now. Widen if the change-point isn't visible — alert for: durations mean the cause often starts 15–60 min earlier.
Pin T before any query — blocking step
Before the first Prometheus call, T must be fully specified: year, month, day, hour, minute, timezone. If any one is missing or guessable from context but not stated, stop and ask the user with AskUserQuestion — do not assume, do not pick the "most plausible" interpretation, do not anchor on task creation time as a substitute. A wrong day or missed UTC↔CEST shift returns empty results that look like "metric missing"; the cost of one clarifying question is far lower than triaging the wrong window.
Sources of T, in order of trust:
- Explicit user statement ("alert at 14:32 UTC on 2026-04-21").
- Alertmanager
startsAtor Grafana URLfrom=param — always UTC, unambiguous. - Bot-rendered alert text (
<jinxer-wm>etc.) — timezone not guaranteed; bare "1:01 PM" is not enough. - Phabricator task
dateCreated/ comment timestamps — these are UTC epoch from the API, but they timestamp the report, not the incident; only use as T when the user explicitly says so.
Wikimedia alert timestamps are UTC when sourced from Alertmanager/Grafana. Bot-rendered chat text may be local. CET = UTC+1, CEST = UTC+2 (DST flips last Sun of Mar / Oct).
If a query returns empty data, recheck T before assuming the metric is gone — wrong-day is the most common cause.
Compute timestamps in Python from the actual T — don't eyeball, don't reuse a number from a previous session, watch the year:
python3 -c "
import datetime as dt
T = dt.datetime(YYYY, MM, DD, HH, MM, tzinfo=dt.timezone.utc) # or dt.datetime.now(dt.timezone.utc)
print(int((T - dt.timedelta(hours=2)).timestamp()), int((T + dt.timedelta(hours=2)).timestamp()))
"
Workflow
Parse: alert name, namespace, deployment, cluster(s), and T. If T is not fully specified (year/month/day/hour/minute/timezone), ask before querying — see "Pin T before any query".
Pick metrics path — different alerts point at different layers. Run the right set first; cross over only if it doesn't explain the signal. Fan independent queries out in parallel. Full PromQL in queries.md.
Alert First-pass metrics LiftWingServiceErrorRateIstio: 5xx by response_codeandresponse_flags, p99 latency, request rate. Pods often look healthy — failure is per-request.KubernetesDeploymentUnavailableReplicaskube_deployment_spec_replicasvs_available, then per-container readiness.KubernetesPodCrashLoopingrestarts_totalincrease,last_terminated_reason,container_oom_events_total.Health sweep Replicas + readiness first; add Istio if 2xx rate looks normal but errors are reported. response_flagsis the fastest split between "model bug" and "infra bug":-= upstream (the model server) returned the error itself;UH/UF/NR/DC= mesh-side (no healthy upstream / connect failure / no route / disconnect).Spec-vs-available is the first split when replicas are unavailable: if
spec > availableand pod count =available, it's scheduling (pod never created); if pod count =specbut readiness is 0, it's container readiness. Different failure modes — don't conflate. From there,kube_pod_container_status_ready == 0per(pod, container)names which container failed.Gerrit —
mcp__gerrit__query_changes_by_date_and_filters, ±1–2 days (widen to 3 if empty), both repos. Filtermessage_substringon the service name. Multi-CL incidents (change → hotfix → rollback) are common — surface all.Project Likely culprit when... machinelearning/liftwing/inference-servicescontainer crashes, model load errors, exceptions in predict operations/deployment-chartsreadiness fails without crashing, pods Pending, replica/limit changes, ImagePull errors Upstream check — if metrics show a runtime failure with no in-window CL (e.g. articlequality / revertrisk hitting MW API timeouts), don't stop at "no deploy correlation." Note the upstream dependency and recommend checking its health for the same window (MW API SLOs, mw-api-* alerts).
Phabricator — for each relevant CL, parse
Bug: T<NNNN>from the commit footer; pull tasks viamcp__phabricator__phabricator_get_task.Current state — always end by querying the same namespace at
t = now:kube_deployment_status_replicas_unavailableandkube_pod_container_status_ready == 0. For past T, this is the "did it recover, regress, or roll forward" coda. For T = now, this is the headline. Fold into the report.
Confidence
No container logs, no K8s events — we see which container failed and which CLs landed nearby, not the actual error. Calibrate, name the gap, feed the rating into the report.
- High — single container failed readiness while peers were healthy, and a CL touching that container's config/code/network merged in window. Name the CL.
- Medium — multiple plausible CLs, or none clean.
- Low — clear metric signal but no in-window CL → likely upstream (MW API, Swift) or transient runtime; or intermittent 5xx with no readiness/restart drops → needs Logstash (LDAP
logstash-access). Distinguish the two in the report and recommend the next data source.
Safety
Read-only external calls only: GETs to grafana.wikimedia.org; Gerrit MCP reads (query_changes*, get_commit_message, get_change_details, get_file_diff); Phab MCP reads (phabricator_get_task, phabricator_get_task_comments). No SSH, no kubectl, no mutating MCP calls. Propose remediation; human executes.
Output
Four sections, in order. Hard data, no preamble, no filler. Tables as real Markdown (pipes outside a fence); keep columns short — drop a column rather than wrap.
- Date — T and the UTC window queried. If T = now, write "now (YYYY-MM-DD HH:MM UTC)".
- Issue description — one-line summary table, then bullet anomalies (one line each, hard facts).
- Root cause — one line + confidence rating. Name CL(s): number, subject, Phab task. If multi-CL, label causal vs reactive.
- Potential solutions — actionable bullets, scoped for a human SRE. Concrete first, investigation second. If already resolved, say so on one line and skip remediation.
If everything is clean: one sentence, stop.
Example
Date: 2026-04-30, window 14:00–17:30 UTC (alert fired 15:12 UTC).
Issue description:
| Cluster | Rev | First unavail | Recovered | Failing | Restarts |
|---|---|---|---|---|---|
| ml-serve-codfw | 00005 | 14:42 rollout; 16:27 qp flip | 14:46 / past window | queue-proxy | 0 |
- 14:42–14:46: rollout churn, normal.
- 14:46 onward: spec wants 5 replicas, only 4 pods exist — 5th never scheduled. Drives the alert (
for: 25m). - 16:27: queue-proxy flipped not-ready on all 4 pods, did not recover. kserve-container and istio-proxy stayed ready.
Root cause: High for the 16:27 queue-proxy flip — CL 1280202 kserve-inference: allow ingress on queue-proxy port 8013 (T424049) modified queue-proxy ingress; only queue-proxy went not-ready across all pods. Medium for the 5th-replica scheduling — no scheduling reason in metrics, needs kube events.
Potential solutions:
- Revert or amend CL 1280202 (likely NetworkPolicy/probe port mismatch on queue-proxy 8013).
- Pull queue-proxy logs from Logstash to confirm the probe failure.
kubectl describe podon the missing 5th replica for theFailedSchedulingreason.- If urgent, scale spec.replicas 5→4 to clear the alert while the queue-proxy fix lands.
Current state: rolled forward to rev 00007, all 4 replicas healthy. Already resolved.