수집 1회에 3~4장, 발행 1회에 2장씩 와서 장애 알림이 묻혔다. 빌드·되돌리기 실패와 수집 예외는 기존 build_failed · job_dead 알림이 이미 보내고 있어 같은 사고가 두 번씩 갔다. - collect_service: 시작·완료 요약·예외 알림 제거 — 채널 크롤링 실패만 남긴다(채널별 결과는 잡 결과 channels 에 그대로) - vision_service: 실패가 있을 때만 - build_service · rollback_service: activity 알림 되돌림 — build_failed · recovery 가 그대로 담당 - docs/ALERTS.md: activity 행을 실제 발송 범위로 관련 테스트 17개 파일 297 passed · 2 failed(test_search_console_service — 변경 전에도 실패) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
4.0 KiB
Markdown
63 lines
4.0 KiB
Markdown
# 장애 알림 (2026-09-15)
|
|
|
|
구현: `services/alert_service.py`(적재·재시도·중복 억제) · `services/teams_webhook.py`(전송) ·
|
|
`worker/runner.py` · `services/build_service.py` · `services/rollback_service.py`(발생 지점) ·
|
|
`scheduler/jobs.py`(발송·큐 정체 스윕). 전용 컨테이너 없음 — 기존 API·워커 프로세스가 한다.
|
|
|
|
## 무엇을 알리나
|
|
|
|
| kind | 언제 | dedupe_key |
|
|
|---|---|---|
|
|
| `job_dead` | 잡이 재시도를 소진해 DEAD | `job_dead:{JobType}:{place_id 또는 job_id}` |
|
|
| `build_failed` | BUILD·ROLLBACK 이 **게이트 반려가 아닌** 렌더·인프라 실패로 끝남 | `build_failed:{place_id}` |
|
|
| `partial_failure` | 노래 등 곁가지 생성 실패(발행 자체는 계속) | `song_failed:{place_id}` |
|
|
| `queue_stuck` | dead-letter 누적·좀비 실행·PENDING 30분 이상 정체 | `queue_health` |
|
|
| `recovery` | 위 dedupe_key 가 다음 정상 상태에서 풀릴 때 한 번 | 없음(매번 새 행) |
|
|
| `activity` | 채널 크롤링 실패 · 사진 분석 일부 실패(`services/activity_feed.py`) | 없음(매번 보낸다) |
|
|
|
|
★ **게이트 반려는 알리지 않는다.** 사장님이 fact 를 안 채웠거나 고유 콘텐츠가 없어서 막힌 건
|
|
운영자가 손댈 일이 아니다 — `build_service._fail(reason, gate=None)` 일 때만 `build_failed`.
|
|
|
|
## 중복 억제·재시도
|
|
|
|
`send_alert(kind, title, detail, dedupe_key)` — 같은 dedupe_key 로 "안 풀린"(resolved_at
|
|
NULL) 알림이 이미 있으면 새로 만들지 않는다. `resolve_alert(dedupe_key, ...)` 가 그 알림을
|
|
풀고 복구 알림을 한 번 보낸다. 실제 전송은 `scheduler.jobs.sweep_alert_outbox`(1분마다) —
|
|
실패하면 `crud/job_crud.compute_backoff` 와 같은 백오프로 최대 5회 재시도 후 `FAILED`(소진)로
|
|
멈춘다. `TEAMS_WEBHOOK_URL` 이 비어 있으면 적재만 되고 전송은 안 나간다(서버 동작엔 영향 없음).
|
|
|
|
`detail` 은 저장 **전에** `alert_service._scrub` 이 쿼리스트링 키·Bearer 토큰·`password=` 류·
|
|
이메일을 마스킹한다 — 외부 API 예외 메시지가 URL 에 키를 실어 보내는 경우가 있다.
|
|
|
|
## 설정
|
|
|
|
```
|
|
TEAMS_WEBHOOK_URL= # Teams Workflows 수신 webhook. 비우면 알림이 DB(alert_outbox)에
|
|
# 쌓이기만 하고 안 나간다 — 서버는 그대로 뜬다.
|
|
ALERT_DEDUPE_WINDOW_MIN=60
|
|
```
|
|
|
|
`GSC_ALERT_WEBHOOK_URL`(search_console_alerts.py)과는 **다른 값**이다 — 색인 감시 전용과
|
|
이 잡 큐·발행 알림은 목적이 달라 의도적으로 분리했다(services/teams_webhook.py 머리주석).
|
|
|
|
## 서버·DB 전체 장애 — 이 알림 체계로는 못 잡는다
|
|
|
|
`alert_service`·`scheduler`가 도는 프로세스 자체가 죽으면(서버 다운·DB 완전 단절) 이 체계는
|
|
자기 장애를 자기가 못 알린다. **외부 감시가 필요하다** — uptime 모니터 등에서 주기적으로
|
|
`GET /readyz` 를 찌른다(`router/router.py`). `/healthz` 와 다르다: `/healthz` 는 프로세스
|
|
생존만(항상 200), `/readyz` 는 **DB 에 실제로 `SELECT 1` 을 던져** 200/503 을 가른다.
|
|
|
|
절차:
|
|
1. 외부 모니터가 `https://<host>/readyz` 를 1~5분 간격으로 확인한다.
|
|
2. 2xx 가 아니거나 타임아웃이면 **그 모니터 자신의 채널**로 알린다 — 이 레포의
|
|
`TEAMS_WEBHOOK_URL` 로 보내면 안 된다(webhook 이 죽은 서버 안에 있을 수 있다).
|
|
3. 이 모니터의 실제 설정(어느 서비스·어느 채널)은 이 세션에서 만들지 않았다 — 운영 계정·
|
|
외부 서비스 연결은 사용자 승인 후 진행한다.
|
|
|
|
## 아직 안 한 것 — 운영 미적용
|
|
|
|
- 실제 Teams Workflows webhook 생성·채널 지정 — mock 테스트만 했다(tests/test_alert_service.py).
|
|
- 외부 uptime 모니터 실제 연결(2절 3번).
|
|
- 마이그레이션(`0016_alert_outbox.sql`) 서버 적용.
|
|
- `alert_outbox` 오래된 SENT/FAILED 행 보관 정책(지금은 무기한 보관 — 운영 부하를 보고 정한다).
|