운영 번들 자동 로그인 자격증명 유출, 온보딩 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 알림 실채널 수신 확인.
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), /readyz 는 DB 에 실제로 SELECT 1 을 던져 200/503 을 가른다.
절차:
- 외부 모니터가
https://<host>/readyz를 1~5분 간격으로 확인한다. - 2xx 가 아니거나 타임아웃이면 그 모니터 자신의 채널로 알린다 — 이 레포의
TEAMS_WEBHOOK_URL로 보내면 안 된다(webhook 이 죽은 서버 안에 있을 수 있다). - 이 모니터의 실제 설정(어느 서비스·어느 채널)은 이 세션에서 만들지 않았다 — 운영 계정· 외부 서비스 연결은 사용자 승인 후 진행한다.
아직 안 한 것 — 운영 미적용
- 실제 Teams Workflows webhook 생성·채널 지정 — mock 테스트만 했다(tests/test_alert_service.py).
- 외부 uptime 모니터 실제 연결(2절 3번).
- 마이그레이션(
0016_alert_outbox.sql) 서버 적용. alert_outbox오래된 SENT/FAILED 행 보관 정책(지금은 무기한 보관 — 운영 부하를 보고 정한다).