o2o-site-AEO/docs/ALERTS.md
Mina Choi 412dc3397c [feat] solution/backend: 미니블로그 승인 알림·예약 요청 메일 발송 실패를 Teams 로 — 정상화되면 recovery
카톡·메일 발송이 실패하면 로그 한 줄만 남고 글은 REVIEWED 로 계속 머물렀다. 손님 예약 요청은
사장님께 못 가도 아무도 몰랐다.

- blog_jobs: 카톡 실패(메일 대체) · 메일 실패 · 보낼 곳 없음 → blog_send_failed, 사업장별 dedupe, 성공 시 resolve
- booking_request_service: 전달 실패 → booking_request_failed, 손님 이름·연락처는 싣지 않는다
- docs/ALERTS.md: 두 kind 추가

테스트 2건 추가. 블로그·예약·알림 93 passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:33:39 +09:00

4.4 KiB

장애 알림 (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 가 다음 정상 상태에서 풀릴 때 한 번 없음(매번 새 행)
blog_send_failed 미니블로그 승인 알림 실패 — 카톡 실패(메일로 대체) · 메일 실패 · 보낼 곳 없음. 다음 발송 성공 때 recovery blog_kakao_failed:{place_id} · blog_send_failed:{place_id}
booking_request_failed 손님 예약 요청 메일을 사장님께 못 보냄(손님 이름·연락처는 싣지 않는다). 다음 전달 성공 때 recovery booking_request_failed:{place_id}
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 행 보관 정책(지금은 무기한 보관 — 운영 부하를 보고 정한다).