Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions docs/design/gimin-#338-observability-incident-drill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 관측성 파이프라인 실제 장애 주입 검증 설계

## 배경

DocGrid는 Backend와 Embedding Provider 메트릭, 비동기 Queue Snapshot, Prometheus 경보 규칙과
Alertmanager 라우팅을 제공한다. 기존 검증은 각 계층의 계약과 합성 시계열 전달을 빠르게 확인했지만,
실제 DB 상태와 Provider 프로세스에서 시작한 신호가 전체 경로를 지나가는지 한 번에 재현하지 않았다.

또한 Queue Gauge는 Prometheus scrape callback에서 DB를 읽지 않고 전용 Scheduler가 만든 Snapshot을
사용하도록 설계됐다. 단위 테스트만으로 이 경계를 확인하면 실제 HTTP scrape와 PostgreSQL 사이에 다른
호출이 추가됐을 가능성을 배제하기 어렵다.

## 목표와 성공 조건

다음 세 시나리오를 하나의 공통 Harness에서 독립적으로 실행한다.

1. 실제 `/actuator/prometheus` 반복 호출 중 운영 Snapshot aggregate SQL이 증가하지 않는다.
2. 실제 문서 업로드로 생성한 Queue가 운영 규칙의 지속 시간 뒤 firing되고 Worker 재개 후 INDEXED와
resolved 상태로 수렴한다.
3. 실제 BGE-M3 컨테이너 중단이 root-cause 경보로 전달되고 같은 배포의 종속 warning을 억제하며,
Provider 재시작 뒤 resolved가 전달된다.

각 시나리오는 다음 공통 조건을 만족해야 PASS다.

- 고유 Compose project, 동적 host port와 새 PostgreSQL volume을 사용한다.
- 외부 알림 시크릿 대신 실제 HTTP 요청을 받는 로컬 webhook receiver를 사용한다.
- timeout 안에 관측 상태가 나타나지 않으면 실패하고 진단 로그를 보존한다.
- 실행 종료 시 Backend 프로세스, 컨테이너, DB volume과 로컬 build 이미지를 정리한다.
- 환경·설정·UTC 시각·구간별 시간을 JSON으로 남긴다.

## 구조

```text
실제 Backend ──/actuator/prometheus──┐
├─ Prometheus ─ Alertmanager ─ local webhook
실제 BGE-M3 ─────────/metrics────────┘
└─ stop/start 장애 주입

PostgreSQL ─ Queue row / pg_stat_statements
└─ Observer Backend + Indexing Worker
```

공통 Compose에는 PostgreSQL 17 + pgvector, Redis, BGE-M3, Prometheus 3.5.5, Alertmanager 0.33.1과
timestamp를 기록하는 webhook receiver가 포함된다. Backend는 동일한 실행 jar를 host의 서로 다른 동적
포트에서 실행한다. Queue 실험의 Observer와 Worker는 같은 DB와 Local Storage를 공유한다.

## 명령 인터페이스

이 작업은 제품 HTTP API를 추가하지 않는다. 저장소 루트에서 다음 명령을 제공한다.

```bash
./monitoring/drills/run.sh scrape-load [--output-dir PATH]
./monitoring/drills/run.sh queue-recovery [--output-dir PATH]
./monitoring/drills/run.sh provider-outage [--output-dir PATH]
./monitoring/drills/run.sh all [--output-dir PATH]
```

`all`은 세 시나리오를 `scrape-load → queue-recovery → provider-outage` 순으로 실행하지만, 각 시나리오는
자신의 Stack과 데이터를 새로 만든다. 중간 실패 시 뒤 시나리오는 실행하지 않고 실패 JSON과 해당
Stack의 최근 로그를 남긴다.

## 시나리오별 경계

### Scrape와 DB 부하

1. Snapshot 주기를 10분으로 설정해 최초 갱신 뒤 측정 창 안에 예약 갱신이 없도록 한다.
2. `pg_stat_statements`에서 세 aggregate SQL의 최초 호출 합계가 3인지 확인한다.
3. 통계를 reset하고 20개 Thread에서 Management endpoint를 300회 호출한다.
4. 모든 응답에 Queue Gauge가 존재하는지 확인하고 p50·p95·max를 계산한다.
5. 측정 뒤 aggregate SQL 호출 합계와 Snapshot 성공 Counter 변화가 모두 0인지 확인한다.

이 방식은 Scheduler SQL과 scrape 유발 SQL을 구분한다. Snapshot Scheduler를 제거하거나 mocking하지
않으며 실제 Backend와 PostgreSQL을 사용한다.

### Queue 정체와 Worker 복구

1. Worker가 꺼진 Observer Backend에서 실제 ADMIN 로그인과 multipart 문서 업로드를 실행한다.
2. 이 문서의 Job만 10분 전 PENDING으로 옮겨 `oldest age > 300s`를 만든다.
3. Queue 실험에 섞일 수 있는 동일 업로드의 Outbox Event만 PROCESSED로 종결한다.
4. 실제 Gauge와 Prometheus target을 확인한 시점부터 운영 규칙의 `for: 1m`, `for: 5m`을 측정한다.
5. 두 firing webhook 뒤 Worker Backend를 시작한다.
6. Job `INDEXED`, 저장된 Embedding 양수, claimable Gauge 0과 두 resolved webhook을 확인한다.

### Provider 장애와 억제

1. BGE-M3 readiness와 Prometheus `up=1`을 확인한 뒤 컨테이너를 stop한다.
2. `up=0`, pending, 운영 `for: 1m` 이후 firing과 critical webhook을 순서대로 기록한다.
3. 실제 root-cause alert가 Alertmanager에 존재할 때 동일 cluster/environment의 종속 warning 하나를
Alertmanager API로 주입한다.
4. warning의 상태가 `suppressed`이고 warning용 `group_wait` 30초보다 긴 35초 동안 firing webhook이
0건인지 확인한다.
5. Provider를 start하고 Docker가 다시 할당한 동적 host port를 조회한다.
6. readiness, Prometheus alert 해제와 resolved webhook을 확인한다.

Provider 중단과 root-cause 경보는 실제다. 종속 warning 주입은 inhibition 자체를 다른 Queue 대기시간과
분리하기 위한 제한된 합성 입력이며 결과 문서에 이 경계를 명시한다.

## 시간 설정

Prometheus의 scrape/evaluation 15초, 모든 운영 규칙의 `for`, Alertmanager `group_wait`은 제품 설정과
같다. `group_interval`만 resolved 전달 실험을 반복 가능한 시간에 끝내기 위해 5분에서 30초로 줄인다.
따라서 firing 측정은 운영 경로를 그대로 나타내지만 resolved webhook 시간은 테스트 설정 결과다.

## 보안과 데이터 경계

- 고정된 로컬 DB 암호와 JWT secret은 격리 Stack에서만 사용한다.
- ADMIN access token은 Python 프로세스 메모리 안에서만 사용하며 command argument와 로그에 쓰지 않는다.
- Slack·Discord·SMTP 자격 증명은 요구하지 않는다.
- 실험용 문서와 DB는 종료 시 삭제한다.
- BGE-M3 모델 cache만 외부 Docker volume에 유지해 반복 다운로드를 막는다.

## 검증 자동화

기존 `monitoring/verify.sh`는 장시간 장애 주입을 CI마다 반복하지 않는다. 대신 다음을 빠르게 검사한다.

- Drill Alertmanager 설정을 고정 버전 `amtool`로 파싱
- Python 실행기 AST 파싱과 Shell 문법
- 동적 mount를 포함한 Drill Compose 렌더링
- 기존 Prometheus 규칙·rule test·Alertmanager 전달 E2E

실제 장시간 실험은 명시적 명령으로 실행하고 결과 JSON을 `docs/test-results/evidence/issue-338/`에
보존한다.

Alertmanager 전달 E2E의 종속 warning은 root-cause가 Alertmanager에 먼저 등록된 뒤 firing되게 해,
동시 도착 순서가 inhibition 검증 결과를 바꾸지 않도록 한다.

closes #338
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"receivedAt":"2026-09-13T12:39:02.781656+00:00","payload":{"receiver":"local-webhook","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","instance":"embedding-server:8000","job":"embedding-provider","service":"embedding-provider","severity":"critical"},"annotations":{"description":"Prometheus has failed to scrape the Embedding Provider for one minute.","summary":"Embedding Provider metric endpoint is unavailable"},"startsAt":"2026-09-13T12:38:52.76Z","endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://34d39fe91415:9090/graph?g0.expr=up%7Bjob%3D%22embedding-provider%22%7D+%3D%3D+0&g0.tab=1","fingerprint":"dcc4408eec1a5b0a"}],"notification_reason":"first notification","groupLabels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-provider","severity":"critical"},"commonLabels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","instance":"embedding-server:8000","job":"embedding-provider","service":"embedding-provider","severity":"critical"},"commonAnnotations":{"description":"Prometheus has failed to scrape the Embedding Provider for one minute.","summary":"Embedding Provider metric endpoint is unavailable"},"externalURL":"http://d444d44ceae8:9093","version":"4","groupKey":"{}/{severity=\"critical\"}:{alertname=\"EmbeddingProviderDown\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-provider\", severity=\"critical\"}","truncatedAlerts":0}}
{"receivedAt":"2026-09-13T12:40:32.789920+00:00","payload":{"receiver":"local-webhook","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","instance":"embedding-server:8000","job":"embedding-provider","service":"embedding-provider","severity":"critical"},"annotations":{"description":"Prometheus has failed to scrape the Embedding Provider for one minute.","summary":"Embedding Provider metric endpoint is unavailable"},"startsAt":"2026-09-13T12:38:52.76Z","endsAt":"2026-09-13T12:40:07.76Z","generatorURL":"http://34d39fe91415:9090/graph?g0.expr=up%7Bjob%3D%22embedding-provider%22%7D+%3D%3D+0&g0.tab=1","fingerprint":"dcc4408eec1a5b0a"}],"notification_reason":"all alerts resolved","groupLabels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-provider","severity":"critical"},"commonLabels":{"alertname":"EmbeddingProviderDown","cluster":"docgrid-drill","environment":"local-drill","instance":"embedding-server:8000","job":"embedding-provider","service":"embedding-provider","severity":"critical"},"commonAnnotations":{"description":"Prometheus has failed to scrape the Embedding Provider for one minute.","summary":"Embedding Provider metric endpoint is unavailable"},"externalURL":"http://d444d44ceae8:9093","version":"4","groupKey":"{}/{severity=\"critical\"}:{alertname=\"EmbeddingProviderDown\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-provider\", severity=\"critical\"}","truncatedAlerts":0}}
42 changes: 42 additions & 0 deletions docs/test-results/evidence/issue-338/provider-outage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"scenario": "provider-outage",
"status": "PASS",
"measuredAt": "2026-09-13T12:40:32.921836+00:00",
"gitCommit": "9f92f04c638bc7281911886a16c06a0bb244a3da",
"host": {
"system": "Darwin",
"machine": "arm64"
},
"images": {
"postgres": "pgvector/pgvector:0.8.1-pg17",
"prometheus": "prom/prometheus:v3.5.5",
"alertmanager": "quay.io/prometheus/alertmanager:v0.33.1"
},
"conditions": {
"scrapeInterval": "15s",
"evaluationInterval": "15s",
"productionRuleFor": "1m",
"criticalGroupWait": "10s",
"testGroupInterval": "30s",
"productionGroupInterval": "5m"
},
"timestamps": {
"providerStoppedAt": "2026-09-13T12:37:23.423467+00:00",
"upZeroObservedAt": "2026-09-13T12:37:38.950811+00:00",
"alertPendingAt": "2026-09-13T12:37:53.042109+00:00",
"alertFiringAt": "2026-09-13T12:38:53.234353+00:00",
"firingWebhookReceivedAt": "2026-09-13T12:39:02.781656+00:00",
"recoveryStartedAt": "2026-09-13T12:39:38.312330+00:00",
"providerReadyAt": "2026-09-13T12:39:43.637544+00:00",
"resolvedWebhookReceivedAt": "2026-09-13T12:40:32.789920+00:00"
},
"measurements": {
"stopToConditionSeconds": 15.527,
"stopToFiringSeconds": 89.811,
"stopToWebhookSeconds": 99.358,
"restartToReadySeconds": 5.325,
"restartToResolvedWebhookSeconds": 54.478,
"dependentWarningInhibited": true,
"dependentWarningWebhookDeliveries": 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"receivedAt":"2026-09-13T12:31:41.284908+00:00","payload":{"receiver":"local-webhook","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"annotations":{"description":"Claimable embedding jobs have been waiting without a live worker for one minute.","summary":"DocGrid embedding queue has no active workers"},"startsAt":"2026-09-13T12:31:31.265Z","endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://9e014350d14a:9090/graph?g0.expr=max+by+%28cluster%2C+environment%29+%28docgrid_embedding_claimable_jobs%29+%3E+0+and+max+by+%28cluster%2C+environment%29+%28docgrid_embedding_active_workers%29+%3D%3D+0&g0.tab=1","fingerprint":"66839df19bb4ff97"}],"notification_reason":"first notification","groupLabels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"commonLabels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"commonAnnotations":{"description":"Claimable embedding jobs have been waiting without a live worker for one minute.","summary":"DocGrid embedding queue has no active workers"},"externalURL":"http://3cf7ddb745e3:9093","version":"4","groupKey":"{}/{severity=\"critical\"}:{alertname=\"DocGridEmbeddingWorkersUnavailable\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-worker\", severity=\"critical\"}","truncatedAlerts":0}}
{"receivedAt":"2026-09-13T12:36:01.278503+00:00","payload":{"receiver":"local-webhook","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","dependency":"embedding-provider","environment":"local-drill","service":"embedding-worker","severity":"warning"},"annotations":{"description":"The oldest claimable embedding job has remained eligible for more than five minutes.","summary":"DocGrid embedding queue is stalled"},"startsAt":"2026-09-13T12:35:31.265Z","endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://9e014350d14a:9090/graph?g0.expr=max+by+%28cluster%2C+environment%29+%28docgrid_embedding_claimable_jobs%29+%3E+0+and+max+by+%28cluster%2C+environment%29+%28docgrid_embedding_oldest_claimable_age_seconds%29+%3E+300&g0.tab=1","fingerprint":"f4a15170e154fd71"}],"notification_reason":"first notification","groupLabels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"warning"},"commonLabels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","dependency":"embedding-provider","environment":"local-drill","service":"embedding-worker","severity":"warning"},"commonAnnotations":{"description":"The oldest claimable embedding job has remained eligible for more than five minutes.","summary":"DocGrid embedding queue is stalled"},"externalURL":"http://3cf7ddb745e3:9093","version":"4","groupKey":"{}:{alertname=\"DocGridEmbeddingQueueStalled\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-worker\", severity=\"warning\"}","truncatedAlerts":0}}
{"receivedAt":"2026-09-13T12:36:41.302049+00:00","payload":{"receiver":"local-webhook","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"annotations":{"description":"Claimable embedding jobs have been waiting without a live worker for one minute.","summary":"DocGrid embedding queue has no active workers"},"startsAt":"2026-09-13T12:31:31.265Z","endsAt":"2026-09-13T12:36:31.265Z","generatorURL":"http://9e014350d14a:9090/graph?g0.expr=max+by+%28cluster%2C+environment%29+%28docgrid_embedding_claimable_jobs%29+%3E+0+and+max+by+%28cluster%2C+environment%29+%28docgrid_embedding_active_workers%29+%3D%3D+0&g0.tab=1","fingerprint":"66839df19bb4ff97"}],"notification_reason":"all alerts resolved","groupLabels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"commonLabels":{"alertname":"DocGridEmbeddingWorkersUnavailable","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"critical"},"commonAnnotations":{"description":"Claimable embedding jobs have been waiting without a live worker for one minute.","summary":"DocGrid embedding queue has no active workers"},"externalURL":"http://3cf7ddb745e3:9093","version":"4","groupKey":"{}/{severity=\"critical\"}:{alertname=\"DocGridEmbeddingWorkersUnavailable\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-worker\", severity=\"critical\"}","truncatedAlerts":0}}
{"receivedAt":"2026-09-13T12:37:01.280511+00:00","payload":{"receiver":"local-webhook","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","dependency":"embedding-provider","environment":"local-drill","service":"embedding-worker","severity":"warning"},"annotations":{"description":"The oldest claimable embedding job has remained eligible for more than five minutes.","summary":"DocGrid embedding queue is stalled"},"startsAt":"2026-09-13T12:35:31.265Z","endsAt":"2026-09-13T12:36:31.265Z","generatorURL":"http://9e014350d14a:9090/graph?g0.expr=max+by+%28cluster%2C+environment%29+%28docgrid_embedding_claimable_jobs%29+%3E+0+and+max+by+%28cluster%2C+environment%29+%28docgrid_embedding_oldest_claimable_age_seconds%29+%3E+300&g0.tab=1","fingerprint":"f4a15170e154fd71"}],"notification_reason":"all alerts resolved","groupLabels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","environment":"local-drill","service":"embedding-worker","severity":"warning"},"commonLabels":{"alertname":"DocGridEmbeddingQueueStalled","cluster":"docgrid-drill","dependency":"embedding-provider","environment":"local-drill","service":"embedding-worker","severity":"warning"},"commonAnnotations":{"description":"The oldest claimable embedding job has remained eligible for more than five minutes.","summary":"DocGrid embedding queue is stalled"},"externalURL":"http://3cf7ddb745e3:9093","version":"4","groupKey":"{}:{alertname=\"DocGridEmbeddingQueueStalled\", cluster=\"docgrid-drill\", environment=\"local-drill\", service=\"embedding-worker\", severity=\"warning\"}","truncatedAlerts":0}}
47 changes: 47 additions & 0 deletions docs/test-results/evidence/issue-338/queue-recovery.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"scenario": "queue-recovery",
"status": "PASS",
"measuredAt": "2026-09-13T12:37:01.517068+00:00",
"gitCommit": "9f92f04c638bc7281911886a16c06a0bb244a3da",
"host": {
"system": "Darwin",
"machine": "arm64"
},
"images": {
"postgres": "pgvector/pgvector:0.8.1-pg17",
"prometheus": "prom/prometheus:v3.5.5",
"alertmanager": "quay.io/prometheus/alertmanager:v0.33.1"
},
"conditions": {
"queueAgeAtStart": ">10m",
"scrapeInterval": "15s",
"evaluationInterval": "15s",
"workersUnavailableRuleFor": "1m",
"queueStalledRuleFor": "5m",
"criticalGroupWait": "10s",
"warningGroupWait": "30s",
"testGroupInterval": "30s",
"productionGroupInterval": "5m"
},
"timestamps": {
"conditionObservedAt": "2026-09-13T12:30:20.850153+00:00",
"workersUnavailableFiringAt": "2026-09-13T12:31:33.077816+00:00",
"workersUnavailableWebhookAt": "2026-09-13T12:31:41.284908+00:00",
"queueStalledFiringAt": "2026-09-13T12:35:32.444496+00:00",
"queueStalledWebhookAt": "2026-09-13T12:36:01.278503+00:00",
"recoveryStartedAt": "2026-09-13T12:36:01.565641+00:00",
"jobIndexedAt": "2026-09-13T12:36:10.164030+00:00",
"workersUnavailableResolvedWebhookAt": "2026-09-13T12:36:41.302049+00:00",
"queueStalledResolvedWebhookAt": "2026-09-13T12:37:01.280511+00:00"
},
"measurements": {
"conditionToWorkersUnavailableFiringSeconds": 72.228,
"conditionToWorkersUnavailableWebhookSeconds": 80.435,
"conditionToQueueStalledFiringSeconds": 311.594,
"conditionToQueueStalledWebhookSeconds": 340.428,
"workerStartToIndexedSeconds": 8.598,
"workerStartToAllResolvedWebhooksSeconds": 59.715,
"storedEmbeddings": 1,
"finalJobStatus": "INDEXED"
}
}
Loading
Loading