알림 설계¶
이 문서는 이슈 #2356의 옵트인 아웃바운드 알림 기능을 설명합니다. 백엔드 상태, 서킷, 예산 이벤트를 푸시로 알리는 기능을 다루며, 설정 범위, 라우터가 이미 만들어 내는 이벤트, 전달 계약을 설명합니다. 구현은 src/services/alerting/에 있고, 이벤트 생산자는 src/health.rs, src/core/circuit_breaker/state.rs, src/control_plane/policy_store.rs에 있습니다.
범위¶
목표:
- 백엔드가 라우팅에서 제외될 때, 서킷이 열리거나 닫힐 때, 키의 월 예산이 소진되거나 설정된 비율에 가까워질 때 운영자에게 알립니다.
- 요청 경로와 전달을 분리합니다. 수신 측이 느리거나 연결할 수 없더라도 추론 지연이 늘어나서는 안 됩니다.
- 라우터가 이미 계산하는 신호를 재사용합니다. 새로운 프로브나 별도 집계를 추가하지 않습니다.
비목표:
- Prometheus, OTLP, 프로세스 로그, Admin API를 대체하지 않습니다. 알림은 이들 옆에 붙는 푸시 편의 기능이며, 기준 데이터가 아닙니다.
- 팀별 채널이나 에스컬레이션 같은 라우팅 규칙은 다루지 않습니다. 활성화된 모든 이벤트가 하나의 웹훅 엔드포인트로 전달됩니다.
- 프롬프트나 완성 본문을 전송하지 않습니다. 알림 페이로드는 메타데이터만 담습니다.
설정¶
alerting: 섹션은 선택 사항이며, 기본값은 꺼짐입니다.
alerting:
enabled: false
webhook:
url: "https://hooks.example.com/continuum"
timeout: 5s
events:
backend_unhealthy: true
backend_recovered: true
circuit_opened: true
circuit_closed: true
budget_exhausted: true
budget_fraction: 0.9
cooldown: 5m
max_queue: 256
retry:
max_attempts: 3
initial_delay: 500ms
max_delay: 30s
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
enabled | bool | false | false이면 섹션은 파싱과 검증만 되고, 태스크를 만들지 않으며 요청도 보내지 않습니다 |
webhook.url | string | 없음 | https 엔드포인트입니다. 활성화 시 필수입니다 |
webhook.timeout | duration | 5s | 전달 시도 한 번의 제한 시간입니다 |
webhook.allow_private_network | bool | false | 루프백이나 사설 네트워크 수신 측을 허용합니다(보안 절 참고) |
events.backend_unhealthy | bool | true | 헬스 체커가 정상에서 비정상으로 전환할 때 발생합니다 |
events.backend_recovered | bool | true | 비정상에서 정상으로 전환할 때 발생합니다 |
events.circuit_opened | bool | true | 백엔드 서킷이 closed를 벗어날 때 발생합니다 |
events.circuit_closed | bool | true | 서킷이 closed로 돌아올 때 발생합니다 |
events.budget_exhausted | bool | true | 허브에서 동기화된 키 예산이 소진될 때 발생합니다. control-plane 빌드가 필요합니다 |
events.budget_fraction | float | 0.9 | 키 예산의 이 비율에 도달하면 경고를 한 번 보냅니다. 0이면 끕니다 |
cooldown | duration | 5m | 이벤트와 대상 단위로 적용됩니다. 구간 안의 추가 발생은 세기만 하고 보내지 않습니다 |
max_queue | integer | 256 | 요청 경로와 전달 태스크 사이의 유한 큐입니다 |
retry.max_attempts | integer | 3 | 첫 시도를 포함한 이벤트당 시도 횟수입니다 |
retry.initial_delay | duration | 500ms | 첫 백오프 지연입니다 |
retry.max_delay | duration | 30s | 백오프 증가의 상한입니다 |
webhook.headers(후속 필드)의 비밀값은 ${ENV_VAR} 치환을 사용하며, 다른 자격 증명 필드와 같이 Admin API와 설정 내보내기에서 마스킹됩니다.
이벤트 소스¶
| 이벤트 | 소스 | 발생 조건 |
|---|---|---|
backend_unhealthy | src/health.rs | 연속 프로브 실패가 health_checks.unhealthy_threshold에 도달할 때 |
backend_recovered | src/health.rs | 연속 프로브 성공이 health_checks.healthy_threshold에 도달할 때 |
circuit_opened, circuit_closed | src/core/circuit_breaker/ | open으로, 또는 다시 closed로 바뀌는 전환. circuit_breaker_transitions_total(src/core/circuit_breaker/metrics.rs)을 증가시키는 지점에서 기록 |
budget_exhausted | src/control_plane/policy_store.rs(허브 스냅숏과 로컬 증분) | 동기화된 키의 월 예산이 소진될 때 |
budget_fraction | src/control_plane/policy_store.rs(허브 스냅숏과 로컬 증분) | 동기화된 키가 설정된 비율을 처음 넘을 때 |
control-plane 기능이 없으면 로컬 지출 기준이 없으므로 두 예산 이벤트는 발생하지 않습니다. 설정 타입은 항상 컴파일하고 런타임만 게이팅한다는 프로젝트 규칙에 따라, 시작 로그가 예산 이벤트가 비활성임을 알립니다.
전달 계약¶
- 단일 태스크: 백그라운드 태스크 하나가 큐를 소유하고 모든 HTTP POST를 수행합니다. 이벤트 생산자는 블로킹하지 않는
try_send로 넘기고 계속 진행합니다. - 유한 큐:
max_queue에 도달하면try_send가 실패하므로 새 이벤트를 버리고,alert_dropped_total을 증가시킵니다. 큐가 가득 차도 요청 처리는 지연되지 않습니다. - 재시도: 2xx가 아닌 응답은
retry.max_delay로 제한된 지수 백오프로 재시도합니다. 2xx를 받으면 이벤트를 종료합니다. 재시도를 모두 쓰면alert_deliveries_total{result="failed"}를 증가시킵니다. - 쿨다운: 이벤트와 대상(백엔드 이름 또는 키 id)마다 타임스탬프를 하나 유지합니다. 구간 안의 발생은 세기만 하고, 구간이 끝나면 요약에 억제 횟수를 담아 보냅니다.
- 리로드: 섹션은 핫 리로드에서 다시 읽습니다. 새 URL은 리로드 뒤에 큐에 들어온 이벤트부터 적용되며, 이미 큐에 있는 이벤트는 생성 당시의 URL을 유지합니다.
- 종료: 종료 시그널을 받은 뒤 전달 태스크는 하드코딩된 5초 동안 대기 중인 이벤트를 비우며, 서버는 이를 기다리지 않으므로 프로세스가 종료될 때 아직 남아 있는 이벤트는 버려집니다.
페이로드¶
모든 요청은 POST이며 Content-Type: application/json을 사용합니다.
{
"event": "circuit_opened",
"occurred_at": "2026-10-28T10:00:00Z",
"router_id": "continuum-router-1",
"subject": { "backend": "openai-primary" },
"detail": { "previous_state": "closed", "state": "open", "failure_status_code": 503 },
"suppressed": 0
}
event는 위에 열거한 활성 이벤트 이름 중 하나입니다.router_id는 라우터가 허브에 등록된 경우 control-plane 라우터 id이고, 그렇지 않으면 호스트 이름입니다.subject는 백엔드 또는 키 id를 담으며, 키 값은 담지 않습니다.detail은 소스 이벤트가 이미 가진 필드만 담습니다. 예를 들어 프로바이더 상태 코드나 소진된 예산 구간입니다.suppressed는 쿨다운으로 합쳐진 발생 횟수입니다.
보안¶
- URL은
https여야 하며, 엄격한 SSRF 검증기(src/infrastructure/common/url_validator.rs)를 통과해야 합니다. 이 검증기는 루프백, 사설, 링크 로컬, 클라우드 메타데이터 주소와 그 주소로 해석되는 호스트 이름을 거부합니다.webhook.allow_private_network: true(새 필드, 기본값false)를 설정하면 클러스터 내부 수신 측을 위해 주소 검사를 해제합니다. 검증만으로는 이름 해석과 연결 사이의 간격을 막을 수 없으므로, 전달 클라이언트는 검증된 주소를 고정해 연결합니다. - 자격 증명이 포함된 URL은 로드 오류입니다. 자격 증명은 환경 변수 치환을 거쳐
webhook.headers에 둡니다. - 페이로드에는 프롬프트 본문, 완성 본문, API 키, 허브 토큰이 절대 포함되지 않습니다.
- 수신 측 오류는 URL과 요청 헤더 없이 기록합니다.
메트릭¶
| 메트릭 | 타입 | 레이블 |
|---|---|---|
alert_deliveries_total | counter | event, result (delivered, failed) |
alert_queue_depth | gauge | 없음 |
alert_dropped_total | counter | 없음 |
alert_coalesced_total | counter | event |
테스트 계획¶
- 페이로드 직렬화, 쿨다운 집계, 큐 축출에 대한 단위 테스트를 둡니다.
- 로컬 HTTP 서버를 사용하는 테스트에서 500 응답 후 재시도, 이후 2xx 성공, 재시도 소진 시 실패 집계, 큐 적재가 네트워크를 건드리지 않음을 확인합니다.
- 설정 검증 테스트에서 활성화 상태의 URL 누락, 비 https URL, 자격 증명 포함, 범위를 벗어난 비율이 로드 오류인지 확인합니다.
- 설정 도우미 드리프트 테스트가
sections_count와### alerting카탈로그 항목을 스키마와 동기화합니다. 이 문서와config.yaml.example의alerting:블록은 섹션이 바뀔 때 함께 갱신합니다.
클라이언트 알림(#2416)과의 관계¶
속도 제한·쿼터 웹훅 설계(웹훅 알림: 설계 기록, 이슈 #2416)는 키를 소유한 클라이언트 애플리케이션에 이벤트를 보냅니다. 알림 기능은 운영 이벤트를 운영자에게 보냅니다. 수신 측, 구독, 페이로드가 다르므로 두 섹션은 분리하지만, 보장 수준이 같도록 전달 구현(유한 큐, 백오프 재시도, SSRF 검증, 전달 메트릭)은 공유해야 합니다.
적용 순서¶
구현은 아래 순서로 반영되었습니다.
- 설정 타입과 검증을 추가했습니다.
enabled가 true가 되기 전까지는 비활성입니다. - 알림 태스크와 헬스·서킷 이벤트를 추가했습니다.
control-plane게이트와 함께 예산 이벤트를 추가했습니다.config.yaml.example과 설정 도우미 문서의 섹션을 같은 변경에서 기록했습니다.