Runbooks
French version: ../runbooks.md.
Each runbook gives the symptom (alert or signal), the diagnosis, the steps and the verification. The alert names are
those of deploy/observability/prometheus/rules/eai-slo-cost.yaml (copy deploy/observability/k8s/prometheusrule-eai.yaml),
of the chart (templates/control-plane/monitoring.yaml) and of the Wazuh rules deploy/wazuh/rules/eai_audit_rules.xml.
Their runbook_url points to the headings below: keep the heading names.
| Runbook | Alerts and signals |
|---|---|
| provider-outage | EaiProviderFailing, EaiGatewayErrorRatio, EaiSloFastBurn, EaiControlPlaneHighErrorRate |
| cost-spike | EaiCostSpike, EaiQuotaRejectionsHigh |
| key-leak | Wazuh 100805, secret scanner, notice of a provider |
| prompt-injection | Wazuh 100804, EaiSecurityEventsSpike |
| gpu-saturation | EaiGpuSaturated, EaiVllmTtftSlo, EaiGpuXidError, EaiGpuMemoryFull |
| control-plane-down | EaiControlPlaneDown, EaiControlPlaneHighLatency, EaiControlPlaneLatencySlo |
| worker-down | EaiWorkerDown, EaiWorkerJobFailing, EaiStaleHeartbeats |
| media-backlog | EaiMediaQueueBacklog |
| database-unavailable | /readyz answers 503 with database: false |
| kill-switch-active | EaiKillSwitchActive, Wazuh 100801 |
| Audit chain | EaiAuditChainBroken, EaiAuditChainNotVerified: Alerting |
| MCP Runtime | EaiMcpRuntimeFailures, EaiMcpRuntimeInstancesFailed, EaiMcpCatalogStale, EaiMcpCallErrorRatio: MCP Runtime |
provider-outage
Symptom: EaiProviderFailing (a provider fails above 50 percent for 5 minutes), EaiGatewayErrorRatio (more
than 10 percent of gateway requests fail), EaiSloFastBurn or EaiControlPlaneHighErrorRate (5xx ratio above
2 percent).
Diagnosis
- Find the provider: label
providerof the alert, Gateway dashboard, metriclitellm_deployment_failure_responses_total. - Tell a provider outage from a local fault: if every provider fails, check LiteLLM (
kubectl -n enterprise-ai logs deploy/enterprise-ai-litellm) and the egress (hubble observe --namespace enterprise-ai --verdict DROPPED).
Steps
- LiteLLM fallbacks switch automatically (
router_settings.fallbacks). Check that the fallback stays compliant (classification, region) in the OPA policies. - If the outage lasts, disable the provider:
POST /v1/providers/{pid}/disable(auditedprovider.disable, Wazuh 100801), or add it tokill_switch.providersof the OPA data. Tell the tenants.
Verification: the error ratio of EaiGatewayErrorRatio goes back under 10 percent. Recovery:
POST /v1/providers/{pid}/enable, remove it from the kill switch, watch the error rate for 30 minutes.
cost-spike
Symptom: EaiCostSpike (hourly gateway cost three times the 7 day average for 30 minutes) or
EaiQuotaRejectionsHigh (more than 30 quota or budget rejections per minute).
Diagnosis
- FinOps dashboard: tenant, provider, model and API key behind the cost (
eai_gateway_cost_total,eai_gateway_tokens_total). - Look for an agent loop (
agent_autonomysettingsmax_stepsandmax_depthof the OPA data) or a compromised key (calls from an unusual address inGET /v1/audit/events).
Steps
- Lower
model_invocation.max_cost_per_requestin the OPA data, or the budget of the tenant. - Suspend the agent:
POST /v1/agents/{agent_id}/suspend(/killto stop it at once). - Revoke a compromised key:
DELETE /v1/api-keys/{key_id}; or set the tenant kill switch.
Verification: eai:gateway_cost:rate1h goes back to its usual level. Reconcile afterwards with the invoice:
POST /v1/finops/reconciliation/run.
key-leak
Symptom: Wazuh rule 100805 (credential revoked or rotated), an alert of the secret scanner, a notice of a provider.
Diagnosis: identify the credential (tenant API key, provider key, OpenBao token, Transit key) and the first use
that is not legitimate in GET /v1/audit/events?actor=<id>.
Steps
- Revoke at once: tenant API key (
DELETE /v1/api-keys/{key_id}, orPOST /v1/api-keys/{key_id}/rotate), provider key in the console of the provider. - Replace it in OpenBao (
bao kv put kv/eai/prod/<path> ...); External Secrets resynchronizes the Secret (refresh one hour), or restart LiteLLM. - Suspected Transit key:
bao write -f transit/keys/<key>/rotate, then rewrap the data. - Declare the incident (
POST /v1/incidents); personal data involved: GDPR notification within 72 hours.
Verification: the old credential is refused (401); GET /v1/audit/verify shows a valid chain.
prompt-injection
Symptom: Wazuh rule 100804 (audit actions security.prompt_injection.blocked or .detected),
EaiSecurityEventsSpike for the type of the security event.
Diagnosis: find the run, the agent and the source (RAG document, tool, user) with
GET /v1/audit/correlations?trace_id=<trace>.
Steps
- Suspend the agent (
POST /v1/agents/{agent_id}/suspend). - Quarantine the document (remove its ACL) or block the tool in the policy.
- Check that no irreversible action ran without approval (
agent_autonomypolicy, approval requests). - Raise the guard of the tenant to
block(prompt_injection_modeof the tenant settings, defaultEAI_PROMPT_INJECTION_DEFAULT_MODE). - Add the case to the promptfoo and garak tests of the CI.
Verification: the same input is now refused (security.prompt_injection.blocked in the audit).
gpu-saturation
Symptom: EaiGpuSaturated (GPU above 95 percent with more than 20 queued requests for 10 minutes),
EaiVllmTtftSlo (time to first token p95 above 2 seconds), EaiGpuXidError, EaiGpuMemoryFull.
Diagnosis
- GPU dashboard: use, memory, queue
vllm:num_requests_waitingper model. - Did KEDA reach
maxReplicasof the model?kubectl -n enterprise-ai get scaledobject. If not, are GPU nodes free? (GPU Operator, XID errors).
Steps
- Raise
maxReplicasof the model invllm.models, or add GPU nodes. - Enable a LiteLLM fallback to a smaller model or to an allowed provider.
- Limit the rate of the tenant in APISIX.
- Repeated XID error: cordon the node (
kubectl cordon <node>) and open a hardware ticket.
Verification: vllm:num_requests_waiting goes back under the threshold of vllm.keda.threshold; the alert
resolves.
control-plane-down
Symptom: EaiControlPlaneDown (no healthy target for 5 minutes), EaiControlPlaneHighLatency or
EaiControlPlaneLatencySlo (p95 above 1 second).
Diagnosis
- Kubernetes:
kubectl -n enterprise-ai get pods -l app.kubernetes.io/component=control-planeand the logs of a pod. Packages:systemctl status navin-platform-apiandjournalctl -u navin-platform-api -n 200. unsafe production settings:in the logs: a production rule is broken.password authentication failedor a timeout: see database-unavailable.curl -fsS <api-url>/readyztells the state of the database and of the migrations.
Steps
- Fix the setting named by the log (Configuration reference), then restart
(
kubectl rollout restart deploy/enterprise-ai-control-plane, orsystemctl restart navin-platform-api). - High latency only: check the HPA (
kubectl -n enterprise-ai get hpa) and the CPU limits; raisecontrolPlane.autoscaling.maxReplicas.
Verification: /readyz answers 200 ready; the alert resolves.
worker-down
Symptom: EaiWorkerDown (no tick for 5 minutes), EaiWorkerJobFailing (more than 2 failures of a job in
30 minutes), EaiStaleHeartbeats (active runs with a stale heartbeat).
Diagnosis
- Is a worker running?
kubectl -n enterprise-ai get pods -l app.kubernetes.io/component=control-plane-worker, orsystemctl status navin-platform-worker. - A worker that stays in
activatingor in a failed init: a migration is pending (python -m eai.migrate --check). - A failing job: label
job_nameofeai_worker_job_failures_total, then the logs of the worker.
Steps
- Pending migration: apply it (
navin-platform-db migrate, or the migration Job of the chart). - Restart the worker. Only one replica is active (PostgreSQL advisory lock
EAI_WORKER_LOCK_KEY): a second one waits. - Stale heartbeats: once the worker ticks again, its
heartbeatsjob flags the runs past the stale threshold (EAI_HEARTBEAT_STALE_SECONDS) and moves them totimed_outpastEAI_WORKER_HEARTBEAT_TIMEOUT_SECONDS.
Verification: eai_worker_last_tick_timestamp_seconds{worker="worker"} is recent; eai_heartbeat_stale_runs
goes back to 0.
media-backlog
Symptom: EaiMediaQueueBacklog (more than 100 queued media jobs, or the oldest older than 15 minutes).
Diagnosis: eai_media_queue_depth and eai_media_queue_oldest_age_seconds; are media workers running
(kubectl -n enterprise-ai get pods, systemctl status navin-platform-media-worker) and did KEDA scale them?
Steps
- Start or scale the media workers (
maxReplicasofworkers.items,lagThreshold). - Jobs stuck in
running: themedia_requeuejob requeues a job whose lease expired (EAI_MEDIA_JOB_LEASE_SECONDS) and fails it afterEAI_MEDIA_JOB_MAX_ATTEMPTSclaims.
Verification: the queue depth decreases; the alert resolves.
database-unavailable
Symptom: /readyz answers 503 with "database": false; API and worker logs show connection errors.
Diagnosis
- Kubernetes:
kubectl -n eai-data get cluster eai-postgres(CloudNativePG) shows the primary and the replicas. Packages:systemctl status postgresqlandsudo navin-platform-db status. - TLS errors: check the CA of
sslrootcertand thesslmodeofEAI_DATABASE_URL.
Steps
- CloudNativePG promotes a replica automatically; wait for the new primary, the Service
eai-postgres-rwfollows it. - Disk full: grow the volume (
storage.sizeof the cluster) or free the WAL archive. - Data loss: restore (PostgreSQL, High availability and disaster recovery).
Verification: /readyz answers 200 with database: true and migrations: true.
kill-switch-active
Symptom: EaiKillSwitchActive (a provider, model or tenant kill switch active for 5 minutes), Wazuh 100801
(provider.disable, model.disable, agent.kill, agent.suspend).
Diagnosis: label kind of eai_kill_switches_active; GET /v1/soc/kill-switches lists the switches and their
verification receipts; the audit event names who set it.
Steps
- Confirm that the switch is intended (incident in progress, containment order of the SOC).
- Lift it through the same channel once the cause is fixed:
POST /v1/providers/{pid}/enable, remove the entry ofkill_switchin the OPA data, or the four eyes recovery of the SOC for a switch set by a containment order (POST /v1/soc/recoveries, thenPOST /v1/soc/recoveries/{recovery_id}/approveby a second person).
Verification: eai_kill_switches_active goes back to 0 for that kind.