EyvaX DevOps

EyvaX Backend Server Runbook

FastAPI-based backend service handling authentication, AI chat, health tracking, and user management.

Tech Stack: Python 3.12, FastAPI, MongoDB (Motor async), Redis, ClickHouse, Kafka Repo: software-bluesemi/eyvax-backend-server Namespace: eyvax-backend Deployment: eyvax-backend-server ArgoCD App: eyvax-backend-server Sentry Project: eyvax-backend-server


Table of Contents

  1. Infrastructure Overview
  2. Health Check Endpoints
  3. Database Issues
  4. Authentication Issues
  5. Circuit Breaker Patterns
  6. External Service Failures
  7. Rate Limiting Issues
  8. Performance Issues
  9. Deployment & Rollback
  10. CI/CD Failure Patterns
  11. Sentry Investigation
  12. PagerDuty Operations
  13. Real Incident Patterns
  14. Escalation

Infrastructure Overview

K8s Resource Configuration

Setting Value
Namespace eyvax-backend
Base Replicas 2
HPA Min/Max 2 / 5
HPA CPU Target 70%
HPA Memory Target 80%
HPA ScaleDown Stabilization 300s
HPA ScaleUp Stabilization 60s
PDB minAvailable 1
Rolling Update Strategy maxSurge=1, maxUnavailable=0 (zero-downtime)

Container: eyvax-backend-server (port 8000)

Resource Request Limit
CPU 100m 1000m
Memory 768Mi 2Gi

Container: redis-cache sidecar (port 6379)

Resource Request Limit
CPU 50m 100m
Memory 64Mi 128Mi

Probe Configuration

Probe Endpoint InitialDelay Period Timeout FailureThreshold Max Wait
Startup GET /health:8000 45s 10s 5s 18 225s
Readiness GET /ready:8000 10s 10s 3s 3 40s
Liveness GET /live:8000 30s 30s 5s 3 120s

GKE Cluster Commands

# Set cluster context
gcloud container clusters get-credentials eyvax-dev-zone --region asia-south1 --project eyva-440707

# Check pods
kubectl get pods -n eyvax-backend -l app=eyvax-backend-server -o wide

# Expected healthy output:
# NAME                                    READY   STATUS    RESTARTS   AGE    IP             NODE
# eyvax-backend-server-xxx-yyy            2/2     Running   0          2d     10.x.x.x       gke-xxx
# eyvax-backend-server-xxx-zzz            2/2     Running   0          2d     10.x.x.x       gke-xxx
#
# Unhealthy indicators:
# READY 1/2 → one container (redis sidecar?) not ready
# STATUS OOMKilled → memory limit (2Gi) exceeded
# STATUS CrashLoopBackOff → application crash, check logs
# RESTARTS > 3 → crash-looping

# Check deployment status
kubectl get deployment eyvax-backend-server -n eyvax-backend

# Expected healthy:
# NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
# eyvax-backend-server     2/2     2            2           14d
#
# READY 0/2 → all pods failing
# UP-TO-DATE < desired → stuck rolling update
# AVAILABLE 0 → service completely down

# Check HPA
kubectl get hpa -n eyvax-backend

# Expected:
# NAME                         REFERENCE                       TARGETS          MINPODS   MAXPODS   REPLICAS   AGE
# eyvax-backend-server-hpa     Deployment/eyvax-backend-server 30%/70%, 45%/80% 2         5         2          14d
#
# If TARGETS show <unknown>/70% → metrics-server issue

# Check resource usage
kubectl top pods -n eyvax-backend -l app=eyvax-backend-server

# Expected (healthy):
# NAME                                    CPU(cores)   MEMORY(bytes)
# eyvax-backend-server-xxx-yyy            50m          400Mi
# eyvax-backend-server-xxx-zzz            45m          380Mi
#
# Warning if CPU > 800m (80% of limit) or Memory > 1.6Gi (80% of limit)

ArgoCD

# Check sync status
kubectl get application eyvax-backend-server -n argocd -o jsonpath='{.status.sync.status} {.status.health.status}'

# Expected: "Synced Healthy"
# "OutOfSync Healthy" → manifest changes not yet applied
# "Synced Degraded" → pods not healthy
# "Synced Progressing" → rollout in progress

# Force sync
argocd app sync eyvax-backend-server

Secrets Management

Secrets are managed via GCP Secret Manager → External Secrets Operator → K8s Secrets.

# Check ExternalSecret status
kubectl get externalsecret -n eyvax-backend

# Expected: STATUS=SecretSynced
# If STATUS=SecretSyncError → check GCP SM permissions

# Force secret refresh
kubectl annotate externalsecret <name> -n eyvax-backend force-sync=$(date +%s) --overwrite

Health Check Endpoints

Response Schemas

GET /health — Startup probe + comprehensive check

curl -s https://api.eyvax.com/health | python3 -m json.tool

Healthy response (HTTP 200):

{
    "status": "healthy",
    "timestamp": 1709424000.123,
    "version": "1.0.0",
    "checks": {
        "database": {
            "status": "healthy",
            "type": "mongodb"
        },
        "database_write": {
            "status": "healthy"
        }
    }
}

Unhealthy response (HTTP 503):

{
    "status": "unhealthy",
    "timestamp": 1709424000.123,
    "version": "1.0.0",
    "checks": {
        "database": {
            "status": "unhealthy",
            "type": "mongodb"
        },
        "database_write": {
            "status": "unhealthy"
        }
    }
}

GET /ready — Readiness probe

Healthy (HTTP 200):

{"status": "ready"}

Not ready (HTTP 503):

{"status": "not ready", "reason": "database not connected"}

GET /live — Liveness probe

Always returns HTTP 200 if process is running:

{"status": "alive", "timestamp": 1709424000.123}

Health Check Decision Tree

IF /health returns HTTP 503:
  → Parse response JSON
  IF checks.database.status == "unhealthy":
    → MongoDB issue — see Database Issues section
  IF checks.database_write.status == "unhealthy":
    → MongoDB write issue — possible read replica lag or auth problem
  → In all cases, check pod logs: kubectl logs deploy/eyvax-backend-server -n eyvax-backend --tail=100

IF /ready returns HTTP 503:
  → Pod not ready to serve traffic
  → Kubernetes will stop sending traffic to this pod
  IF reason == "database not connected":
    → MongoDB connectivity issue — see Database Issues
  → Check if pod is still starting up (within 225s startup window)

IF /live returns non-200 or times out:
  → Process is dead or hung
  → Kubernetes will kill and restart the pod automatically
  → If repeated restarts → check CrashLoopBackOff below

Database Issues

MongoDB Connection Failure

Symptoms:

Decision Tree:

IF /health shows database unhealthy:
  → Step 1: Check logs
    kubectl logs deploy/eyvax-backend-server -n eyvax-backend --tail=100 | grep -i "mongo\|ServerSelection"

  IF logs contain "ServerSelectionTimeoutError":
    → Step 2: Check MongoDB Atlas status
      # Visit https://cloud.mongodb.com or check status page
    IF Atlas is up and accessible:
      → Step 3: Check circuit breaker
        curl -s https://api.eyvax.com/admin/circuit-breakers | python3 -m json.tool
      IF database_init circuit breaker is OPEN:
        → Reset it:
          curl -s -X POST https://api.eyvax.com/admin/circuit-breakers/database_init/reset
        → Wait 30s, re-check /health
        IF healthy → auto-resolve PD incident
        ELSE → rolling restart: kubectl rollout restart deploy/eyvax-backend-server -n eyvax-backend
      ELSE (breaker CLOSED but still unhealthy):
        → Rolling restart: kubectl rollout restart deploy/eyvax-backend-server -n eyvax-backend
        → Wait 225s (startup probe window)
        IF still unhealthy → escalate P2

    IF Atlas is down:
      → Escalate P1 immediately (external dependency, cannot auto-fix)

  IF logs contain "connection pool exhausted" or "Too many open connections":
    → Connection pool exhaustion
    → Rolling restart to reset connections:
      kubectl rollout restart deploy/eyvax-backend-server -n eyvax-backend
    → Monitor: mongodb_connections_current metric should drop

  IF logs contain "Authentication failed":
    → Credential mismatch
    → Check ExternalSecret: kubectl get externalsecret -n eyvax-backend
    → Force sync: kubectl annotate externalsecret <name> -n eyvax-backend force-sync=$(date +%s) --overwrite
    → Escalate P2 if sync fails

Diagnosis Commands:

# Check MongoDB connectivity from inside the pod
kubectl exec deploy/eyvax-backend-server -n eyvax-backend -c eyvax-backend-server -- \
  python3 -c "import asyncio; from motor.motor_asyncio import AsyncIOMotorClient; import os; c=AsyncIOMotorClient(os.environ['MONGODB_URL']); print(asyncio.run(c.admin.command('ping')))"

# Expected: {'ok': 1.0}
# Failure: ServerSelectionTimeoutError or ConnectionRefusedError

# Check circuit breaker states
curl -s https://api.eyvax.com/admin/circuit-breakers | python3 -m json.tool

# Expected: each breaker shows "state": "CLOSED", "failure_count": 0
# Problem: "state": "OPEN" → underlying service is failing

ClickHouse Unavailable

Symptoms:

Decision Tree:

IF ClickHouse errors in logs:
  → ClickHouse is non-critical — backend server continues without it
  → Analytics features degrade but core API works
  IF error rate is low (<1%) → monitor, no action needed
  IF error rate is high → check ClickHouse cluster health
    kubectl exec deploy/eyvax-backend-server -n eyvax-backend -- \
      python3 -c "import clickhouse_connect; c=clickhouse_connect.get_client(host='$CH_HOST'); print(c.query('SELECT 1').result_rows)"
  → No auto-resolution — escalate to data team if persistent

Authentication Issues

JWT Token Validation Failures

Error Codes: ERR_AUTH_1001 (missing), ERR_AUTH_1002 (invalid), ERR_AUTH_1003 (expired), ERR_AUTH_1010 (refresh token invalid)

Decision Tree:

IF high volume of ERR_AUTH_1001/1002/1003 errors:
  → Check if this is baseline noise or a spike
  → Baseline: 35-1000+ auth errors/day is NORMAL (expired tokens, invalid client requests)

  IF error count is within 2x of daily baseline:
    → Normal — no action needed
    → These are client-side issues (expired tokens, bad requests)

  IF error count is 5x+ above baseline:
    → Possible JWT_SECRET_KEY rotation or mismatch
    → Check: kubectl exec deploy/eyvax-backend-server -n eyvax-backend -- env | grep JWT_SECRET_KEY
    → Verify same key across all pods
    → If key was recently rotated → all existing tokens are invalid, expected spike
    → Escalate P3 if not explained by rotation

  IF ERR_AUTH_1010 (refresh token) spike:
    → Refresh token secret may have changed
    → Escalate P2 — users cannot re-authenticate

Diagnosis:

# Decode a JWT token to check expiry and claims
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Check JWT secret is configured
kubectl exec deploy/eyvax-backend-server -n eyvax-backend -c eyvax-backend-server -- env | grep JWT_SECRET_KEY
# Should return a non-empty value. If empty → escalate immediately

# Check OTP verification issues
kubectl logs deploy/eyvax-backend-server -n eyvax-backend --tail=200 | grep -i "otp\|sms\|2factor\|mandrill"

Circuit Breaker Patterns

How Circuit Breakers Work

The backend server uses circuit breakers to protect against cascading failures. Each breaker has three states:

CLOSED (normal) → OPEN (after N failures) → HALF_OPEN (after recovery_timeout) → CLOSED (after N successes)
                                          → OPEN (if failures resume in HALF_OPEN)

Circuit Breaker Configuration

Breaker Name Failure Threshold Recovery Timeout Success Threshold Per-Call Timeout
database_init 5 failures 60s 3 successes 30s
database_operations 5 failures 60s 3 successes 30s
ai_provider 5 failures 60s 3 successes 30s
external_email 5 failures 60s 3 successes 30s
external_sms 5 failures 60s 3 successes 30s
external_weather 5 failures 60s 3 successes 30s
clickhouse 5 failures 60s 3 successes 30s
redis_operations 5 failures 60s 3 successes 30s

Error Responses When Breaker is Open

Admin Endpoints

# View all circuit breaker states
curl -s https://api.eyvax.com/admin/circuit-breakers | python3 -m json.tool

# Expected (healthy): all breakers show state=CLOSED, failure_count=0
# Problem: state=OPEN means the underlying dependency is failing

# Reset a specific breaker
curl -s -X POST https://api.eyvax.com/admin/circuit-breakers/{breaker_name}/reset

# Reset ALL breakers
curl -s -X POST https://api.eyvax.com/admin/circuit-breakers/reset-all

Decision Tree: Circuit Breaker Issues

IF error response contains "CIRCUIT_BREAKER_OPEN" or "ERR_SYSTEM_7004":
  → Step 1: Identify which breaker is open
    curl -s https://api.eyvax.com/admin/circuit-breakers | python3 -c "
import json, sys
breakers = json.load(sys.stdin)
for name, state in breakers.items():
  if state.get('state') != 'CLOSED':
    print(f'  OPEN: {name} (failures: {state.get(\"failure_count\", \"?\")})')
"

  IF only ONE breaker is OPEN:
    → Step 2: Check if the underlying dependency is actually down
      IF breaker is database_init or database_operations:
        → Check MongoDB: see Database Issues section
      IF breaker is ai_provider:
        → Check AI service: curl -s https://eyvax-ai.eyva.io/health
        → If eyvax-ai is healthy, reset breaker:
          curl -X POST https://api.eyvax.com/admin/circuit-breakers/ai_provider/reset
      IF breaker is clickhouse:
        → Non-critical, reset and monitor
      IF breaker is external_email, external_sms, external_weather:
        → Non-critical external services, reset and monitor
      IF breaker is redis_operations:
        → Check Redis sidecar: kubectl exec deploy/eyvax-backend-server -n eyvax-backend -c redis-cache -- redis-cli ping
        → Expected: PONG. If no response → restart pod

    → Step 3: Reset the breaker
      curl -X POST https://api.eyvax.com/admin/circuit-breakers/{name}/reset
    → Step 4: Monitor for 60s (recovery_timeout)
      IF breaker stays CLOSED → auto-resolve PD
      IF breaker re-opens within 60s → underlying dependency is actually down, escalate

  IF MULTIPLE breakers are OPEN:
    → Likely a cascading failure
    → Check eyvax-ai health first (most common root cause)
    → If eyvax-ai is healthy, check MongoDB
    → Reset all breakers: curl -X POST https://api.eyvax.com/admin/circuit-breakers/reset-all
    → If breakers keep re-opening → escalate P2

External Service Failures

AI Provider Failures (OpenAI/Anthropic/Gemini/OpenRouter)

Symptoms:

LLM Provider Fallback Chain:

The backend uses generate_answer_with_fallback() which tries providers in priority order:

  1. OpenRouter
  2. Gemini Flash
  3. Claude Haiku
  4. GPT Full

If a provider returns a retryable error (5xx, 429, rate_limit, overloaded, insufficient_quota), the system automatically falls back to the next provider.

Decision Tree:

IF AI chat errors in logs:
  IF error is 429 (rate limit) or "insufficient_quota":
    → System should auto-fallback to next provider
    → If fallback is working → no action needed, monitor
    → If ALL providers failing:
      → Check provider status pages
      → Escalate P2 if sustained > 5 min

  IF error is 5xx from provider:
    → Auto-fallback should handle this
    → Monitor fallback success rate in logs

  IF error is "CIRCUIT_BREAKER_OPEN" for ai_provider:
    → All providers failed 5 times consecutively
    → Wait 60s for auto-recovery (HALF_OPEN)
    → Or reset: curl -X POST https://api.eyvax.com/admin/circuit-breakers/ai_provider/reset

External API Timeouts (Email/SMS/Weather)

Non-critical services with circuit breaker protection. Backend continues functioning without them.

IF external_email/sms/weather breaker OPEN:
  → Non-critical, notifications will be delayed
  → Wait for 60s automatic recovery
  → If persistent > 10 min → check external service status pages
  → No escalation needed unless impacting core user flows

Rate Limiting Issues

Configuration

Error Codes

Code Meaning Action
ERR_RATE_LIMIT_5001 Per-minute rate limit exceeded Normal, client should retry after 60s
ERR_RATE_LIMIT_5002 Too many concurrent requests Normal, client should queue requests
ERR_RATE_LIMIT_5003 Daily AI quota exceeded No fix — user must wait for midnight UTC reset

Decision Tree

IF rate limit errors spiking:
  IF error is ERR_RATE_LIMIT_5001 (per-minute):
    → Normal rate limiting
    → If from many different IPs → traffic spike, may need to scale
    → If from single IP → possible bot/abuse, monitor

  IF error is ERR_RATE_LIMIT_5003 (daily quota):
    → User exhausted their AI quota for the day
    → No auto-resolution — quota resets at midnight UTC
    → If legitimate user needs more → escalate to support for tier upgrade

  → Rate limiting errors are NEVER a reason to escalate or page
  → They are a safety mechanism working as intended

Performance Issues

High API Latency

SLO Thresholds:

Decision Tree:

IF BackendServerHighLatency alert fires (P95 > 500ms):
  → Step 1: Check which endpoints are slow
    # Check which endpoints have high latency

  IF /api/v1/ai/chat is slow but other endpoints normal:
    → AI provider latency issue
    → Check eyvax-ai health: curl -s https://eyvax-ai.eyva.io/health
    → If eyvax-ai slow → see ai-service.md
    → Not actionable at backend level — AI endpoints have 5s SLO

  IF all endpoints slow:
    → Step 2: Check resource usage
      kubectl top pods -n eyvax-backend -l app=eyvax-backend-server
    IF CPU > 800m (80% of 1000m limit):
      → HPA should be scaling. Check: kubectl get hpa -n eyvax-backend
      → If already at max replicas (5) → escalate P2
    IF Memory > 1.6Gi (80% of 2Gi limit):
      → OOM risk — rolling restart:
        kubectl rollout restart deploy/eyvax-backend-server -n eyvax-backend

  IF MongoDB queries are slow:
    → Check slow query logs
    → Escalate to backend team for index optimization

Deployment & Rollback

Normal Deployment Flow

Code merged to main → GitHub Actions CI → Docker image built →
Image pushed to Artifact Registry → deployment.yaml updated with new tag →
ArgoCD detects change → Rolling update (maxSurge=1, maxUnavailable=0)

Checking Deployment Status

# Check rollout status
kubectl rollout status deployment/eyvax-backend-server -n eyvax-backend

# Expected (success): "deployment "eyvax-backend-server" successfully rolled out"
# Expected (in progress): "Waiting for deployment ... rollout to finish: 1 out of 2 new replicas have been updated..."
# Stuck (> 225s): likely startup probe failing on new pods

# Check current image
kubectl get deployment eyvax-backend-server -n eyvax-backend -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected: asia-south1-docker.pkg.dev/eyva-440707/apps/eyvax-backend-server:<commit-hash>

Rollback

# Rollback to previous revision
kubectl rollout undo deployment/eyvax-backend-server -n eyvax-backend

# Verify rollback
kubectl rollout status deployment/eyvax-backend-server -n eyvax-backend
# Expected: "deployment "eyvax-backend-server" successfully rolled out"

# NOTE: This requires human approval per auto-resolution safety boundaries

Stuck Rolling Update Decision Tree

IF rollout stuck (new pods not becoming ready):
  → Step 1: Check new pod status
    kubectl get pods -n eyvax-backend -l app=eyvax-backend-server
    # Look for pods in Pending/ContainerCreating/CrashLoopBackOff

  IF new pods CrashLoopBackOff:
    → Check logs: kubectl logs <new-pod> -n eyvax-backend --previous --tail=50
    IF "ImportError" or "ModuleNotFoundError" → bad image, rollback
    IF "MONGODB_URL" missing → secret not mounted, check ExternalSecret
    IF startup crash → rollback to previous image

  IF new pods running but not ready (readiness probe failing):
    → /ready returning 503 → database not connected
    → Check MongoDB connectivity from new pod
    → May resolve once MongoDB connection establishes (wait up to 225s)

  IF pods stuck in Pending:
    → Resource quota or node capacity issue
    → kubectl describe pod <pending-pod> -n eyvax-backend → check Events
    → If "insufficient cpu/memory" → node scaling needed, escalate

  IF stuck > 10 min:
    → Rollback: kubectl rollout undo deployment/eyvax-backend-server -n eyvax-backend
    → Escalate P2 for investigation

CI/CD Failure Patterns

CI Pipeline Structure

The CI pipeline runs on push to main:

  1. Secrets Scan — TruffleHog scans for leaked secrets
  2. Lint — flake8 + bandit security scan
  3. Test — pytest with coverage
  4. Build & Push Docker Image — builds and pushes to Artifact Registry, updates deployment.yaml

Decision Tree: CI Failure Blocking Deployment

IF code merged to main but deployment not updated:
  → Step 1: Check CI status
    gh run list --repo software-bluesemi/eyvax-backend-server --limit 3

  IF latest CI run status = "failure":
    → Step 2: Check which step failed
      gh run view <run-id> --log-failed

    IF Test step failed:
      → Pre-existing test issue or new test regression
      → Check if failure is in test code or source code
      → Fix tests on a new branch, create PR, merge
      → CI will re-run on main and auto-deploy

    IF Lint step failed:
      → Code style or security issue
      → Fix lint issues on a new branch

    IF Build step failed:
      → Docker build error (missing dependencies, syntax error)
      → Check Dockerfile and requirements.txt
      → Escalate to backend team

    IF Secrets Scan failed:
      → Leaked secret detected — CRITICAL
      → Rotate the exposed secret immediately
      → Escalate P1

  IF latest CI run status = "success" but deployment not updated:
    → Check ArgoCD sync status
    → Force sync: argocd app sync eyvax-backend-server

Sentry Investigation

Querying Sentry Issues

# List unresolved issues (last 24h)
curl -s -H "Authorization: Bearer $SENTRY_API_KEY" \
  "https://sentry.io/api/0/projects/bluesemi-qv/eyvax-backend-server/issues/?query=is:unresolved&sort=date&statsPeriod=24h&limit=20" | \
  python3 -c "
import json, sys
issues = json.load(sys.stdin)
if isinstance(issues, list):
  for i in issues:
    print(f'[{i[\"level\"]}] {i[\"count\"]}x | {i[\"title\"][:80]} | Last: {i[\"lastSeen\"][:19]} | ID: {i[\"id\"]}')
else:
  print('API error:', issues)
"

# NOTE: emd-glucose events may appear here due to shared Sentry DSN
# Check server_name tag in events to identify actual source service

Common Error Categories

Error Pattern Category Action
ERR_AUTH_1001/1002/1003 Auth noise Normal baseline (35-1000+/day), no action
ServerSelectionTimeoutError MongoDB See Database Issues
CIRCUIT_BREAKER_OPEN Circuit breaker See Circuit Breaker Patterns
429 Too Many Requests Rate limiting Normal, see Rate Limiting
ERR_EXTERNAL_4003 AI provider failure Check AI provider status
Logger._log() got an unexpected keyword argument structlog/stdlib mismatch Events from emd-glucose pods, see emd-glucose.md
ClickHouse connection failed ClickHouse Non-critical, monitor

Resolving Sentry Issues

# Resolve a specific issue
curl -s -X PUT -H "Authorization: Bearer $SENTRY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "resolved"}' \
  "https://sentry.io/api/0/issues/<ISSUE_ID>/"

# Bulk resolve all unresolved issues (use with caution)
curl -s -X PUT -H "Authorization: Bearer $SENTRY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "resolved"}' \
  "https://sentry.io/api/0/projects/bluesemi-qv/eyvax-backend-server/issues/?query=is:unresolved"

PagerDuty Operations

Managing Incidents

# List open incidents
curl -s -H "Authorization: Token token=$PAGERDUTY_API_KEY" \
  "https://api.pagerduty.com/incidents?statuses[]=triggered&statuses[]=acknowledged&limit=25" | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for i in data.get('incidents', []):
  print(f'[{i[\"status\"]}] {i[\"title\"][:80]} | ID: {i[\"id\"]}')
"

# Acknowledge an incident
curl -s -X PUT "https://api.pagerduty.com/incidents" \
  -H "Authorization: Token token=$PAGERDUTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "From: $PAGERDUTY_EMAIL" \
  -d '{"incidents": [{"id": "<ID>", "type": "incident_reference", "status": "acknowledged"}]}'

# Resolve an incident
curl -s -X PUT "https://api.pagerduty.com/incidents" \
  -H "Authorization: Token token=$PAGERDUTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "From: $PAGERDUTY_EMAIL" \
  -d '{"incidents": [{"id": "<ID>", "type": "incident_reference", "status": "resolved"}]}'

Real Incident Patterns

OpenAI 429 Rate Limit — March 2026

What happened: OpenAI returned 429 (rate limit exceeded) errors, but the is_retryable_llm_error() function only checked for 5xx errors. The 429 wasn't caught as retryable, so no fallback to alternative providers occurred. Users saw AI chat failures.

Root cause: generate_insight() in answer.py used a hardcoded two-provider try/except instead of the full fallback chain.

Fix (PR #135):

Detection pattern: Sentry errors with openai.RateLimitError or HTTP 429 from OpenAI. If you see these, check if the fallback chain is working.

Sentry DSN Misconfiguration — Ongoing

What happened: emd-glucose service shares the same Sentry DSN as eyvax-backend-server. All emd-glucose errors appear in the backend-server Sentry project.

How to identify: Check the server_name tag in Sentry events. If it shows emd-glucose-api-* pod names, the error is from emd-glucose, not backend-server.

Impact: Sentry issue counts for backend-server are inflated. Always check server_name before investigating.


Environment Variables

Variable Required Source Description
MONGODB_URL Yes GCP Secret Manager MongoDB Atlas connection string
JWT_SECRET_KEY Yes GCP Secret Manager JWT signing key (must not be default)
SENTRY_DSN Yes (prod) GCP Secret Manager Sentry error tracking DSN
REDIS_HOST No ConfigMap Default: 127.0.0.1 (sidecar)
REDIS_PORT No ConfigMap Default: 6379
AI_TIERED_COMPUTE No ConfigMap Default: true
AI_ENABLE_CACHE No ConfigMap Default: true

Escalation

If unable to resolve within 15 minutes:

  1. Check Sentry for detailed error traces
  2. Review recent deployments (kubectl rollout history)
  3. Check if multiple services are affected (cascade — see incident-response.md)
  4. Escalate to backend team via PagerDuty
  5. If P1, follow escalation matrix in incident-response.md