diff --git a/README.md b/README.md index 8296e883..74870da2 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ npm --prefix frontend run dev - API 문서: `http://localhost:8080/swagger-ui/index.html` - OpenAPI JSON: `http://localhost:8080/v3/api-docs` +Backend와 Embedding Provider의 운영 메트릭을 함께 수집하려면 선택형 monitoring profile을 실행합니다. + +```bash +docker compose --profile monitoring up -d prometheus +``` + +- Prometheus Target: `http://localhost:9090/targets` +- 설정과 기존 Prometheus 연결: [monitoring/prometheus/README.md](monitoring/prometheus/README.md) + ### 6. 핵심 흐름 확인 1. 회원가입 또는 로그인 diff --git a/docker-compose.yml b/docker-compose.yml index 7f1250e0..ba37deb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,9 +78,13 @@ services: - "--storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-7d}" ports: - "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090" + # Host에서 bootRun으로 실행한 Backend Management 포트를 Linux에서도 같은 이름으로 찾는다. + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./monitoring/prometheus/rules:/etc/prometheus/rules:ro + - ./monitoring/prometheus/targets:/etc/prometheus/targets:ro - prometheus-data:/prometheus depends_on: embedding-server: diff --git a/docs/design/gimin-#330-prometheus-backend-availability.md b/docs/design/gimin-#330-prometheus-backend-availability.md new file mode 100644 index 00000000..27f1907e --- /dev/null +++ b/docs/design/gimin-#330-prometheus-backend-availability.md @@ -0,0 +1,61 @@ +# Prometheus Backend 수집과 가용성 경보 설계 + +- 관련 이슈: #330 +- 선행 조건: #328의 Backend Management endpoint +- 대상: 번들 Prometheus와 기존 Prometheus 사용 환경 + +## 문제 + +Backend가 `/actuator/prometheus`를 제공해도 Prometheus scrape job이 없으면 시계열은 저장되지 않는다. +현재 번들 Prometheus는 Docker network 안의 Embedding Provider만 수집하며 Spring Backend는 Host에서 +`bootRun`으로 실행한다. 컨테이너에서 Host Management 포트로 접근하는 경로와 Backend의 기본 +가용성 경보가 필요하다. + +## 수집 경계 + +번들 Prometheus는 `file_sd_configs`로 +`monitoring/prometheus/targets/docgrid-backend.yml`을 읽는다. 기본 target은 +`host.docker.internal:8081`이며 `environment=local`, `cluster=docgrid-local` label을 붙인다. + +Compose의 `extra_hosts`는 Linux에서 `host.docker.internal`을 Host gateway로 연결한다. Docker +Desktop도 같은 이름을 지원하므로 운영체제에 따라 Prometheus 본체 설정을 나누지 않는다. 다른 +Host·Port나 production label은 target 파일에서만 변경한다. + +기존 Prometheus를 사용하는 환경은 같은 `job=docgrid-backend`와 +`metrics_path=/actuator/prometheus` 계약을 사용한다. Management 포트의 네트워크 접근 제한은 배포 +환경의 방화벽, Security Group, 컨테이너 network가 담당한다. + +## 경보 + +| 경보 | 조건 | `for` | 목적 | +|---|---|---:|---| +| `DocGridBackendDown` | `up == 0` | 1분 | 일시적인 scrape 실패와 실제 중단 구분 | +| `DocGridDatabasePoolSaturated` | HikariCP active/max > 90% | 2분 | 지속적인 DB pool 고갈 조기 감지 | +| `DocGridBackendHighServerErrorRatio` | 5분간 20건 이상, 5xx > 5% | 3분 | 저트래픽 단일 오류의 오탐 방지 | + +모든 경보에는 `service=docgrid-backend`와 severity를 붙인다. Backend target의 `cluster`와 +`environment` label은 경보 시계열에 유지돼 이후 Alertmanager routing에 사용할 수 있다. + +HTTP 오류율은 `sum by (cluster, environment)`로 인스턴스 전체를 집계한다. 분자와 분모 모두 +`uri!="/mcp"`를 사용한다. MCP Streamable HTTP 요청은 일반 REST와 응답 특성이 달라 별도 오류 +예산 없이 일반 API 경보에 합치지 않는다. + +## 검증 전략 + +`promtool check config`와 `check rules`로 syntax와 rule loading을 확인한다. `promtool test rules`는 +다음 네 경계를 고정한다. + +1. Backend down이 1분 전에는 firing하지 않는다. +2. HikariCP 포화가 2분 유지돼야 firing한다. +3. 5xx 비율이 높아도 최소 요청량을 만족한 cluster만 경보한다. +4. `/mcp` 5xx는 일반 Backend 오류율 경보를 만들지 않는다. + +실제 E2E에서는 Host Backend와 격리 Prometheus를 기동하고 Embedding Provider와 Backend가 모두 +`UP`인지 확인한다. Backend를 중단해 target `DOWN`, 경보 `pending`, `firing`을 순서대로 확인한 뒤 +재기동해 target `UP`과 경보 `inactive` 복구까지 검증한다. + +## 운영 경계 + +현재 경보는 Prometheus에서만 평가하고 화면에서 확인한다. Alertmanager 전달, 알림 그룹화, silence, +외부 채널은 후속 작업에서 추가한다. Job·RAG·Outbox의 도메인 상태와 Queue 정체도 이 변경의 +기본 인프라 경보에는 포함하지 않는다. diff --git a/docs/runbooks/backend-down.md b/docs/runbooks/backend-down.md new file mode 100644 index 00000000..ae1597c3 --- /dev/null +++ b/docs/runbooks/backend-down.md @@ -0,0 +1,40 @@ +# DocGrid Backend Down + +## 의미 + +`DocGridBackendDown`은 Prometheus가 `docgrid-backend` target의 `/actuator/prometheus`를 1분 동안 +수집하지 못했을 때 발생한다. Backend 프로세스 중단 외에도 Management 포트, 네트워크 경로, +Prometheus target 설정 오류가 원인일 수 있다. + +## 확인 + +1. Prometheus `/targets`에서 `docgrid-backend`의 마지막 오류와 마지막 성공 시간을 확인한다. +2. Prometheus가 실행되는 위치에서 Backend Management endpoint에 접근한다. + + ```bash + curl -f http://docgrid-backend.internal:8081/actuator/health/liveness + curl -f http://docgrid-backend.internal:8081/actuator/prometheus + ``` + +3. Backend 프로세스와 최근 종료 원인, OOM, 배포 이벤트를 확인한다. +4. Backend 로그에서 시작 실패, DB 연결 실패, 포트 충돌을 확인한다. +5. target 주소, Management 포트, 방화벽과 Security Group 변경을 확인한다. + +로컬 Compose에서는 `docgrid-backend.internal` 대신 `host.docker.internal`을 사용한다. + +## 복구 + +1. Backend가 중단됐다면 정상 배포 절차로 재시작한다. +2. 시작 실패라면 로그에 나온 설정·DB·포트 원인을 수정한 뒤 다시 시작한다. +3. Backend는 정상인데 scrape만 실패하면 target 파일과 Prometheus에서 Host까지의 네트워크 경로를 + 복구한다. +4. `/actuator/health/liveness`와 `/actuator/prometheus`가 모두 200인지 확인한다. +5. Prometheus `/targets`에서 `docgrid-backend`가 `UP`으로 돌아오는지 확인한다. +6. `/alerts`에서 `DocGridBackendDown`이 inactive로 전환됐는지 확인한다. + +## 복구 완료 조건 + +- Backend liveness와 Prometheus endpoint가 200을 반환한다. +- `up{job="docgrid-backend"}` 값이 `1`이다. +- `DocGridBackendDown`이 inactive 상태다. +- 사용자 API의 정상 요청이 성공한다. diff --git a/docs/test-results/gimin-#330-prometheus-backend-availability.md b/docs/test-results/gimin-#330-prometheus-backend-availability.md new file mode 100644 index 00000000..1cdc5a67 --- /dev/null +++ b/docs/test-results/gimin-#330-prometheus-backend-availability.md @@ -0,0 +1,90 @@ +# Prometheus Backend 수집과 가용성 경보 검증 결과 + +- 관련 이슈: #330 +- 실행 일시: 2026-09-13 (Asia/Seoul) +- 수정 전 기준 Commit: `9b0f701` (`develop`) +- 환경: Prometheus·promtool 3.5.5, Spring Boot 3.5.16, PostgreSQL 17.8, Docker Desktop + +## 검증 목적 + +- Docker의 Prometheus가 Host에서 실행한 Backend Management 포트를 수집해야 한다. +- 기존 Embedding Provider target은 변경 후에도 정상이어야 한다. +- Backend 중단이 1분 동안 지속될 때만 `DocGridBackendDown`이 firing해야 한다. +- Backend 복구 뒤 target과 경보가 정상 상태로 돌아와야 한다. +- DB pool과 HTTP 5xx 규칙의 지속 시간, 최소 표본, MCP 제외 조건을 단위 테스트로 고정해야 한다. + +## 설정과 규칙 검증 + +고정된 `prom/prometheus:v3.5.5` 이미지의 promtool을 사용했다. + +| 검증 | 결과 | +|---|---| +| `promtool check config` | 성공, rule file 2개 로드 | +| `promtool check rules` | 성공, Backend 3개·Embedding 7개 규칙 | +| `promtool test rules` | 성공, 4개 시나리오 | +| `docker compose config` | 성공 | +| `docker compose --profile monitoring config` | 성공, host gateway와 target mount 포함 | + +규칙 테스트는 Backend down 1분, HikariCP 포화 2분, 5xx 최소 20건과 5% 초과, `/mcp` 제외를 +검증했다. 낮은 요청량 cluster는 같은 10% 오류율에서도 경보가 발생하지 않았다. + +Prometheus 이미지의 기본 entrypoint가 `prometheus`이므로 문서의 검증 명령은 +`--entrypoint=promtool`로 명시했다. 실제 컨테이너 실행으로 이 명령 형식까지 확인했다. + +## 실제 수집 + +Backend는 Host의 `8081` Management 포트에서 실행하고, 검증용 Prometheus는 기존 인스턴스를 +건드리지 않도록 `19090`에 격리했다. Prometheus 컨테이너는 기존 `docgrid_docgrid-local` network와 +Host gateway를 함께 사용했다. + +첫 scrape 이후 target 상태는 다음과 같았다. + +```text +docgrid-backend host.docker.internal:8081 local docgrid-local up +embedding-provider embedding-server:8000 up +``` + +Backend target에는 target 파일의 `environment=local`, `cluster=docgrid-local` label이 적용됐고 +기존 Embedding Provider도 계속 `UP`이었다. + +## Backend 중단과 복구 + +| 시각 | 관찰 결과 | +|---|---| +| 17:36:36 | Backend 종료 완료 | +| 17:36:58 | `DocGridBackendDown` activeAt, 상태 `pending` | +| 17:37:03 | target `down`, `connection refused` 기록 | +| 17:38:32 | 경보 `firing`, 값 `0` 확인 | +| 17:38:45 | Backend 재기동 완료 | +| 17:39:03 | target `up`, scrape 오류 없음 | +| 17:39:22 | `DocGridBackendDown` 조회 결과 0건, `inactive` 확인 | + +중단 후 첫 실패 scrape와 rule 평가 주기 안에 pending으로 들어갔고, activeAt부터 설정한 1분이 지난 +뒤 firing했다. Backend 재기동 후 첫 성공 scrape와 다음 평가에서 경보가 사라졌다. + +## 실행 명령 + +```bash +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + check config /etc/prometheus/prometheus.yml + +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + check rules \ + /etc/prometheus/rules/embedding-provider-alerts.yml \ + /etc/prometheus/rules/docgrid-backend-alerts.yml + +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + test rules /etc/prometheus/tests/docgrid-backend-alerts.test.yml + +docker compose config +docker compose --profile monitoring config +``` + +격리 Prometheus와 Backend는 검증 후 종료했고, 테스트 전에 정지 상태였던 PostgreSQL도 다시 +중지했다. diff --git a/monitoring/prometheus/README.md b/monitoring/prometheus/README.md new file mode 100644 index 00000000..03745afd --- /dev/null +++ b/monitoring/prometheus/README.md @@ -0,0 +1,94 @@ +# DocGrid Prometheus + +DocGrid가 함께 제공하는 Prometheus는 Host에서 실행한 Spring Backend와 Docker Compose의 Embedding +Provider를 수집한다. 기본 scrape 주기와 rule 평가 주기는 15초다. + +## 번들 Prometheus 사용 + +Backend를 먼저 실행한다. + +```bash +./backend/gradlew -p backend bootRun +``` + +Management endpoint는 기본적으로 Host의 `8081` 포트에서 열린다. 그다음 monitoring profile을 +실행한다. + +```bash +docker compose --profile monitoring up -d prometheus +``` + +Prometheus는 컨테이너에서 `host.docker.internal:8081`을 수집한다. Compose의 `extra_hosts`가 +Linux의 host gateway를 같은 이름으로 연결하며 Docker Desktop도 같은 주소를 지원한다. + +- Target 상태: +- Alert 상태: + +기본 Backend target과 배포 식별 label은 +`monitoring/prometheus/targets/docgrid-backend.yml`에서 변경한다. + +```yaml +- targets: + - host.docker.internal:8081 + labels: + environment: local + cluster: docgrid-local +``` + +## 기존 Prometheus 사용 + +기존 Prometheus를 운영하는 환경에서는 다음 scrape job을 해당 Prometheus 설정에 추가한다. + +```yaml +scrape_configs: + - job_name: docgrid-backend + metrics_path: /actuator/prometheus + static_configs: + - targets: + - docgrid-backend.internal:8081 + labels: + environment: production + cluster: docgrid-production +``` + +`8081`은 사용자 API 포트가 아닌 Management 포트다. 방화벽, Security Group, 컨테이너 network로 +Prometheus와 운영자만 접근하도록 제한한다. + +Backend 경보를 함께 사용하려면 `monitoring/prometheus/rules/docgrid-backend-alerts.yml`을 기존 +Prometheus의 `rule_files` 경로에 복사한다. + +## 기본 Backend 경보 + +| 경보 | 조건 | 지속 시간 | +|---|---|---:| +| `DocGridBackendDown` | Backend scrape 실패 | 1분 | +| `DocGridDatabasePoolSaturated` | HikariCP active/max가 90% 초과 | 2분 | +| `DocGridBackendHighServerErrorRatio` | 5분간 20건 이상이며 5xx가 5% 초과 | 3분 | + +HTTP 오류율에서는 Streamable HTTP 특성이 다른 `/mcp`를 제외한다. 이 경보들은 현재 Prometheus +화면에서 확인하며 외부 전달은 Alertmanager 설정을 추가한 뒤 활성화된다. + +## 설정 검증 + +로컬에 promtool을 설치하지 않아도 고정된 Prometheus 이미지로 검사할 수 있다. + +```bash +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + check config /etc/prometheus/prometheus.yml + +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + check rules \ + /etc/prometheus/rules/embedding-provider-alerts.yml \ + /etc/prometheus/rules/docgrid-backend-alerts.yml + +docker run --rm --entrypoint=promtool \ + -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ + prom/prometheus:v3.5.5 \ + test rules /etc/prometheus/tests/docgrid-backend-alerts.test.yml + +docker compose config +``` diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index 455c87e9..e0add03d 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -6,6 +6,13 @@ rule_files: - /etc/prometheus/rules/*.yml scrape_configs: + - job_name: docgrid-backend + metrics_path: /actuator/prometheus + file_sd_configs: + # Host 실행과 외부 배포의 차이는 target 파일에서만 관리해 본체 설정을 공유한다. + - files: + - /etc/prometheus/targets/docgrid-backend.yml + - job_name: embedding-provider metrics_path: /metrics static_configs: diff --git a/monitoring/prometheus/rules/docgrid-backend-alerts.yml b/monitoring/prometheus/rules/docgrid-backend-alerts.yml new file mode 100644 index 00000000..68550c82 --- /dev/null +++ b/monitoring/prometheus/rules/docgrid-backend-alerts.yml @@ -0,0 +1,67 @@ +groups: + - name: docgrid-backend + rules: + - alert: DocGridBackendDown + expr: up{job="docgrid-backend"} == 0 + for: 1m + labels: + severity: critical + service: docgrid-backend + annotations: + summary: DocGrid Backend is unavailable + description: Prometheus has failed to scrape the Backend for one minute. + runbook_url: https://github.com/DocGrid/docgrid/blob/develop/docs/runbooks/backend-down.md + + - alert: DocGridDatabasePoolSaturated + expr: | + ( + hikaricp_connections_active{job="docgrid-backend"} + / + clamp_min( + hikaricp_connections_max{job="docgrid-backend"}, + 1 + ) + ) > 0.90 + for: 2m + labels: + severity: warning + service: docgrid-backend + annotations: + summary: DocGrid Backend database pool is saturated + description: More than 90% of the HikariCP pool has been active for two minutes. + + - alert: DocGridBackendHighServerErrorRatio + expr: | + ( + sum by (cluster, environment) ( + rate(http_server_requests_seconds_count{ + job="docgrid-backend", + status=~"5..", + uri!="/mcp" + }[5m]) + ) + / + clamp_min( + sum by (cluster, environment) ( + rate(http_server_requests_seconds_count{ + job="docgrid-backend", + uri!="/mcp" + }[5m]) + ), + 0.001 + ) + ) > 0.05 + and + sum by (cluster, environment) ( + increase(http_server_requests_seconds_count{ + job="docgrid-backend", + uri!="/mcp" + }[5m]) + ) >= 20 + for: 3m + labels: + severity: warning + service: docgrid-backend + annotations: + summary: DocGrid Backend server error ratio is high + description: More than 5% of at least 20 requests returned 5xx during the last five minutes. diff --git a/monitoring/prometheus/targets/docgrid-backend.yml b/monitoring/prometheus/targets/docgrid-backend.yml new file mode 100644 index 00000000..4ed8b909 --- /dev/null +++ b/monitoring/prometheus/targets/docgrid-backend.yml @@ -0,0 +1,5 @@ +- targets: + - host.docker.internal:8081 + labels: + environment: local + cluster: docgrid-local diff --git a/monitoring/prometheus/tests/docgrid-backend-alerts.test.yml b/monitoring/prometheus/tests/docgrid-backend-alerts.test.yml new file mode 100644 index 00000000..c3a6ae9c --- /dev/null +++ b/monitoring/prometheus/tests/docgrid-backend-alerts.test.yml @@ -0,0 +1,95 @@ +rule_files: + - /etc/prometheus/rules/docgrid-backend-alerts.yml + +evaluation_interval: 1m + +tests: + - name: backend down waits for one minute + interval: 1m + input_series: + - series: 'up{job="docgrid-backend",instance="host.docker.internal:8081",environment="local",cluster="docgrid-local"}' + values: '0x3' + alert_rule_test: + - eval_time: 0m + alertname: DocGridBackendDown + exp_alerts: [] + - eval_time: 1m + alertname: DocGridBackendDown + exp_alerts: + - exp_labels: + alertname: DocGridBackendDown + cluster: docgrid-local + environment: local + instance: host.docker.internal:8081 + job: docgrid-backend + service: docgrid-backend + severity: critical + exp_annotations: + summary: DocGrid Backend is unavailable + description: Prometheus has failed to scrape the Backend for one minute. + runbook_url: https://github.com/DocGrid/docgrid/blob/develop/docs/runbooks/backend-down.md + + - name: database pool saturation waits for two minutes + interval: 1m + input_series: + - series: 'hikaricp_connections_active{job="docgrid-backend",instance="backend:8081",environment="production",cluster="docgrid-production",pool="docgrid-db-pool"}' + values: '10x4' + - series: 'hikaricp_connections_max{job="docgrid-backend",instance="backend:8081",environment="production",cluster="docgrid-production",pool="docgrid-db-pool"}' + values: '10x4' + alert_rule_test: + - eval_time: 1m + alertname: DocGridDatabasePoolSaturated + exp_alerts: [] + - eval_time: 2m + alertname: DocGridDatabasePoolSaturated + exp_alerts: + - exp_labels: + alertname: DocGridDatabasePoolSaturated + cluster: docgrid-production + environment: production + instance: backend:8081 + job: docgrid-backend + pool: docgrid-db-pool + service: docgrid-backend + severity: warning + exp_annotations: + summary: DocGrid Backend database pool is saturated + description: More than 90% of the HikariCP pool has been active for two minutes. + + - name: high server error ratio requires enough traffic + interval: 1m + input_series: + - series: 'http_server_requests_seconds_count{job="docgrid-backend",instance="backend-a:8081",environment="production",cluster="docgrid-production",status="500",uri="/documents"}' + values: '0+2x10' + - series: 'http_server_requests_seconds_count{job="docgrid-backend",instance="backend-a:8081",environment="production",cluster="docgrid-production",status="200",uri="/documents"}' + values: '0+18x10' + - series: 'http_server_requests_seconds_count{job="docgrid-backend",instance="backend-b:8081",environment="low-traffic",cluster="docgrid-low-traffic",status="500",uri="/documents"}' + values: '0+0.2x10' + - series: 'http_server_requests_seconds_count{job="docgrid-backend",instance="backend-b:8081",environment="low-traffic",cluster="docgrid-low-traffic",status="200",uri="/documents"}' + values: '0+1.8x10' + alert_rule_test: + - eval_time: 3m + alertname: DocGridBackendHighServerErrorRatio + exp_alerts: [] + - eval_time: 4m + alertname: DocGridBackendHighServerErrorRatio + exp_alerts: + - exp_labels: + alertname: DocGridBackendHighServerErrorRatio + cluster: docgrid-production + environment: production + service: docgrid-backend + severity: warning + exp_annotations: + summary: DocGrid Backend server error ratio is high + description: More than 5% of at least 20 requests returned 5xx during the last five minutes. + + - name: mcp traffic uses a separate error budget + interval: 1m + input_series: + - series: 'http_server_requests_seconds_count{job="docgrid-backend",instance="backend:8081",environment="production",cluster="docgrid-production",status="500",uri="/mcp"}' + values: '0+20x6' + alert_rule_test: + - eval_time: 4m + alertname: DocGridBackendHighServerErrorRatio + exp_alerts: []