o2o-site-AEO/docs/ALERTS.md
Mina Choi b4085a0e0f [fix] solution: 온보딩 생성·크롤링 진단·장애 알림 묶음
운영 번들 자동 로그인 자격증명 유출, 온보딩 COPY 잡이 Gemini 429 로 죽던 것,
크롤링 실패가 로그에만 남던 것을 한 번에 정리한다. 실측(2026-09-15 밤, 킹서버):
사진분석 배치가 Gemini 분당 쿼터를 다 써서 같은 키를 쓰는 온보딩 COPY 잡도 같이
429 를 맞고 DEAD 로 갔다 — 확인된 fact 만으로도 편집·발행이 되는데 잡을 죽일
이유가 없었다.

- solution/frontend: `VITE_AUTO_LOGIN_ID`·`PW` 를 운영 진입점에 안 넘긴다(자동 로그인은
  dev 서버 전용) + `Step5Generating` 겉모습을 이전 카드 스타일로, 데이터는 실제 잡
  진행(useGenerationJob) 그대로
- solution/backend: copy_service — Gemini 호출 실패해도 잡을 안 죽이고 fact 만으로 계속.
  db_session_manager — 유니크 제약 충돌(정상 경로) 로그를 ERROR → WARN.
  worker/runner + alert_service + teams_webhook — 잡 dead-letter·발행 실패·큐 정체를
  Teams 로 알림(영구 저장 + 재시도 + dedupe). `/readyz` 추가.
  collect_diagnostics(신규) — 크롤링 채널별 실패를 jobs.result 에 구조화해서 싣는다.
- postgres-init: 0015(users token_version) · 0016(alert_outbox) 마이그레이션

검증: 백엔드 pytest 759 passed. tsc(solution/frontend) 통과. Teams 알림 실채널 수신 확인.
2026-09-16 16:25:02 +09:00

3.8 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 가 다음 정상 상태에서 풀릴 때 한 번 없음(매번 새 행)

게이트 반려는 알리지 않는다. 사장님이 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), /readyzDB 에 실제로 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 행 보관 정책(지금은 무기한 보관 — 운영 부하를 보고 정한다).