diff --git a/book/300_architecture/311_백엔드_구조_원칙.md b/book/300_architecture/311_백엔드_구조_원칙.md index 7623b46..72fc950 100644 --- a/book/300_architecture/311_백엔드_구조_원칙.md +++ b/book/300_architecture/311_백엔드_구조_원칙.md @@ -144,6 +144,8 @@ utils - **원인 직접 수정 우선**: 증상을 감추는 광범위 예외 폴백/임시 우회 코드를 금지한다. 재현 가능한 근본 원인(입력/경로/설정/데이터)을 먼저 수정한다. - **상태코드 의미 보존**: 인증 실패(401/403), 권한 문제(403), 입력 오류(400)는 원래 의미대로 반환하고 500으로 왜곡하지 않는다. - **폴백 남용 금지**: 폴백은 사용자 보호용 최후 수단으로만 사용한다. 개선 신호를 가리는 무조건 폴백(`catch-all fallback`)을 금지한다. +- **실패와 0건 결과 혼동 금지**: 외부 조회 실패, timeout, refresh 실패를 `[]`, `None`, `"no data"` 같은 정상 결과로 치환하지 않는다. "실패"와 "실제 데이터 없음"은 서로 다른 타입과 로그로 분리한다. +- **스케줄러 성공 오판 금지**: 배치/스케줄러 래퍼는 하위 실패를 삼키지 않고 다시 올린다. 운영 로그의 success는 실제 성공과 같아야 한다. - **관측 가능한 실패 보장**: 운영/카나리 경로에는 `strict_observe` 모드를 두어 일부 요청은 폴백 없이 실패를 노출하고, 구조화된 오류 로그/메트릭을 반드시 남긴다. - **로그 없는 폴백 금지**: 폴백이 발생하면 `request_id`, `route`, `template/version`, `fallback_reason`, `upstream_error`를 필수 기록한다. diff --git a/journey/README.md b/journey/README.md index 4b66745..cca4229 100644 --- a/journey/README.md +++ b/journey/README.md @@ -19,6 +19,11 @@ - Slack 응답 및 스킬 호출 문제 – `troubleshooting/250723_happybell80_Slack응답및스킬호출문제.md` - 프론트엔드 로빙 연결 및 UX 개선 – `troubleshooting/250730_happybell80_프론트엔드로빙연결.md` +### 운영 / 브리핑 + +- 네이버 이메일 분석 미전송과 실패 은닉 해결 – `troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_해결.md` +- 네이버 이메일 분석 실패 은닉 리서치 – `research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md` + ### Intent / 리뷰 큐 - Intent 리뷰 큐 API 구현 – `troubleshooting/251116_admin_intent_review_queue_api_implementation.md` @@ -57,4 +62,4 @@ --- -> 새로운 문서가 생기면 관련 섹션에 링크만 추가합니다. 상세 내용은 원본 문서를 참조하세요. \ No newline at end of file +> 새로운 문서가 생기면 관련 섹션에 링크만 추가합니다. 상세 내용은 원본 문서를 참조하세요. diff --git a/journey/research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md b/journey/research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md new file mode 100644 index 0000000..eb73919 --- /dev/null +++ b/journey/research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md @@ -0,0 +1,47 @@ +# 9시 네이버 이메일 분석 미전송 실패 은닉 리서치 + +tags: [naverworks, email, briefing, timeout, failure-observability] + +**날짜**: 2026-03-09 +**작성자**: Codex +**상위 원칙**: [문서 작성 원칙](../../book/300_architecture/312_writing-principles.md), [백엔드 구조 원칙](../../book/300_architecture/311_백엔드_구조_원칙.md) + +## 관련 문서 +- [9시 네이버 이메일 분석 미전송과 실패 은닉 해결](../troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_해결.md) +- [NAVER WORKS Refresh unauthorized_client 장애 및 재인증 복구 기록](../troubleshooting/260227_naverworks_refresh_unauthorized_client_reauth_recovery.md) + +--- + +## 조사 대상 +- 왜 `2026-03-09 09:00` 네이버 이메일 분석 브리핑이 전송되지 않았는가 +- 왜 실패가 운영 로그에서는 성공처럼 보였는가 +- 이번 문제를 우회 없이 닫으려면 무엇을 직접 수정해야 하는가 + +## 확인된 사실 +1. `rb8001` 스케줄 로그 기준 `2026-03-09 09:00:00` KST에 `naverworks_daily` 작업은 실행됐다. +2. 같은 요청 흐름에서 `2026-03-09 09:00:30` KST에 `httpx.ReadTimeout`이 발생했다. +3. `rb8001/app/services/skills/naverworks_briefing.py`는 메일 조회 실패를 빈 목록처럼 다뤄 `No emails`로 끝낼 수 있는 구조였다. +4. `rb8001/app/scheduler/jobs/naverworks_briefing.py`는 작업 예외를 다시 올리지 않아 APScheduler 성공 로그와 실제 실패가 어긋날 수 있는 구조였다. +5. `skill-email/services/naverworks_provider.py`는 당시 단계별 elapsed 로그가 부족해, `30초 블로킹`의 최종 지점이 `auth-server refresh`인지 NAVER WORKS 외부 API인지 즉시 분리하기 어려웠다. +6. `skill-email`, `auth-server` 컨테이너는 UTC 기준으로 동작하고, `rb8001`은 KST 기준 스케줄을 사용한다. +7. `main_db.naverworks_token.expires_at`는 `timestamp without time zone`이라 비교 기준이 코드에서 명시되지 않으면 해석 흔들림이 생긴다. + +## 해석 +- 직접 원인은 `rb8001 -> skill-email /messages` 경로의 read timeout이다. +- 구조 원인은 "조회 실패"와 "조회 결과 0건"을 같은 값으로 취급한 설계다. +- 관측 실패 원인은 스케줄러 래퍼가 예외를 다시 올리지 않은 점이다. +- 시간대 혼재와 naive timestamp는 재발 위험을 높이는 구조 요인이지만, 이번 09:00 미전송의 직접 원인 자체를 대체하지는 않는다. + +## 이번 문제를 닫는 수정 기준 +1. 메일 조회 실패는 빈 목록이 아니라 명시적 실패 타입으로 분리한다. +2. 스케줄러는 실패를 다시 올려 운영 로그와 실제 결과를 일치시킨다. +3. `skill-email`에는 단계별 추적 로그를 추가해 다음 장애에서 block point를 즉시 좁힐 수 있게 한다. +4. 토큰 만료 판단은 UTC 기준을 코드에서 명시해 DB 값과 비교 축을 고정한다. + +## 미확정 +- `2026-03-09 09:00`의 실제 30초 block point가 `auth-server refresh`였는지, 외부 NAVER WORKS API였는지는 당시 세부 로그 부재로 100% 확정하지 못했다. +- 이 항목은 이번 수정 후 재발 시 새 추적 로그로 닫는다. + +## 결론 +- 이 문제의 핵심은 "9시 작업이 안 돌았다"가 아니라 "9시 작업 실패가 성공처럼 은닉됐다"는 점이다. +- 따라서 우회 없는 해법은 재시도 강화가 아니라 실패 타입 분리, 예외 재전파, 단계별 추적 로그 추가다. diff --git a/journey/research/README.md b/journey/research/README.md index 7b4dbb6..3b465b9 100644 --- a/journey/research/README.md +++ b/journey/research/README.md @@ -9,6 +9,7 @@ - [아침브리핑 형식혼선·동남아영어노출 원인확정 리서치 (260305)](./260305_아침브리핑_형식혼선_동남아영어노출_원인확정_리서치.md) - [51124 먹통 48시간 코드 차분 원인 확정 리서치 (260304)](./260304_51124_먹통_48시간_코드차분_원인확정_리서치.md) +- [9시 네이버 이메일 분석 미전송 실패 은닉 리서치 (260309)](./260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md) ### [기억(Memory)](./memory/README.md) - 장단기 기억 메커니즘 diff --git a/journey/troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_해결.md b/journey/troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_해결.md new file mode 100644 index 0000000..eb6bda8 --- /dev/null +++ b/journey/troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_해결.md @@ -0,0 +1,43 @@ +# 9시 네이버 이메일 분석 미전송과 실패 은닉 해결 + +tags: [naverworks, email, briefing, scheduler, timeout, fallback] + +**날짜**: 2026-03-09 +**작성자**: Codex +**관련 파일**: `rb8001/app/services/skills/naverworks_briefing.py`, `rb8001/app/scheduler/jobs/naverworks_briefing.py`, `skill-email/services/naverworks_provider.py` +**상위 원칙**: [문서 작성 원칙](../../book/300_architecture/312_writing-principles.md), [백엔드 구조 원칙](../../book/300_architecture/311_백엔드_구조_원칙.md) + +## 관련 문서 +- [9시 네이버 이메일 분석 미전송 실패 은닉 리서치](../research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md) +- [NAVER WORKS Refresh unauthorized_client 장애 및 재인증 복구 기록](./260227_naverworks_refresh_unauthorized_client_reauth_recovery.md) + +--- + +## 문제 상황 +- `2026-03-09 09:00:00` KST에 `naverworks_daily` 작업은 실제 실행됐다. +- `2026-03-09 09:00:30` KST에 `rb8001 -> skill-email /messages` 호출이 `httpx.ReadTimeout`으로 실패했다. +- 그런데 `rb8001/app/services/skills/naverworks_briefing.py:57` 경로가 이 실패를 `No emails`처럼 처리해, 운영자가 "메일이 없었다"고 오인할 수 있는 상태가 됐다. +- `rb8001/app/scheduler/jobs/naverworks_briefing.py:87`도 예외를 다시 올리지 않아 스케줄러에는 성공처럼 남았다. + +## 해결 방안 +- `rb8001/app/services/skills/naverworks_briefing.py:16`: `BriefingFetchError`를 추가해 메일 조회 실패를 빈 결과와 분리했다. +- `rb8001/app/services/skills/naverworks_briefing.py:57`: 실제 빈 목록일 때만 `No emails in the configured briefing window`를 기록하도록 바꿨다. +- `rb8001/app/services/skills/naverworks_briefing.py:109`: `httpx.ReadTimeout`, `httpx.HTTPError`, 비정상 응답, 잘못된 payload를 `BriefingFetchError`로 올리도록 바꿨다. +- `rb8001/app/scheduler/jobs/naverworks_briefing.py:89`: 스케줄러 래퍼가 `ImportError`와 일반 예외를 다시 올려 APScheduler가 실패로 기록하게 바꿨다. +- `skill-email/services/naverworks_provider.py:38`: 토큰 만료 판단 기준을 `datetime.utcnow()`로 명시해 UTC naive DB 값과 비교 기준을 맞췄다. +- `skill-email/services/naverworks_provider.py:49`, `skill-email/services/naverworks_provider.py:197`, `skill-email/services/naverworks_provider.py:357`: 컨텍스트 조회, 외부 NAVER WORKS API 호출, refresh 호출에 단계별 추적 로그와 timeout 분기 로그를 추가했다. + +## 검증 +- `docker exec -e PYTHONPATH=/code rb8001 pytest -q tests/test_naverworks_briefing.py` 결과 `10 passed` +- `https://ro-being.com/rb8001/health` 응답 `200` +- `https://ro-being.com/skill-email/health` 응답 `200` +- `skill-email /messages` 재호출 시 `200` 응답과 단계별 로그(`context lookup`, `token state`, `API request finished`)를 확인했다. + +## 구현 완료 +- `rb8001` 커밋: `6b2280e` (`fix: surface naverworks briefing failures`) +- `skill-email` 커밋: `7a57aae` (`fix: add naverworks fetch tracing`) + +## 교훈 +- 외부 API 조회 실패와 "실제 0건"은 같은 값으로 처리하면 안 된다. +- 스케줄러 성공 로그는 실제 작업 성공과 같지 않으므로, 예외를 다시 올려 관측 가능한 실패로 남겨야 한다. +- 시간대가 섞인 토큰 만료 판단은 장애를 숨기기 쉬우므로, DB 타입과 비교 기준을 같은 시간 축으로 맞춰야 한다. diff --git a/journey/troubleshooting/README.md b/journey/troubleshooting/README.md index c85cd8f..88efbf8 100644 --- a/journey/troubleshooting/README.md +++ b/journey/troubleshooting/README.md @@ -1,13 +1,12 @@ # Troubleshooting Guide -51124 서버 운용 중 발생한 모든 이슈는 날짜 기반 마크다운 파일(`yymmdd_작성자_주제.md`)로 저장됩니다. 이 README는 340개가 넘는 문서 중 필요한 정보를 빠르게 찾을 수 있도록 돕습니다. +51124 서버 운용 중 발생한 이슈 기록의 진입점입니다. ## 구조와 네이밍 - 경로: `DOCS/troubleshooting/` - 파일명: `YYMMDD_author_topic.md` - 예: `250723_happybell80_Slack_3초룰.md` -- 각 문서는 **상황 → 원인 → 해결 → 교훈** 순서를 기본으로 하며, 교훈 섹션의 규칙은 `AGENTS.md`에 반영됩니다. ## 빠르게 찾는 방법 @@ -33,25 +32,7 @@ | Storage/용량 | `250716_SSD_용량_부족_및_Docker_정리.md` | 정리 스크립트 가이드 | | Intent 리뷰 큐 | `251116_admin_intent_review_queue_api_implementation.md` | 최신 TDD 사례 | -## 문서 작성 시 체크리스트 - -- [ ] 문제 원인/증상 구분 -- [ ] 명령어/로그는 fenced code block 사용 -- [ ] 교훈 섹션에 재발 방지 규칙 명시 -- [ ] 관련 문서/참고 링크를 하단에 추가 - -## 자주 쓰는 명령 - -```bash -rg "교훈" DOCS/troubleshooting -n # 교훈 섹션 검색 -rg "Docker" DOCS/troubleshooting -g "*.md" # 도커 관련 문서 검색 -``` - -## 다음 단계 - -- 월별 인덱스 문서 작성 (예: `2025-07.md`) -- Slack/Email/Calendar 등 서비스별 태그 파일 정리 -- 교훈 섹션 자동 추출 스크립트 추가 (plans/ 디렉토리 예정) - - [260226_51124_openclaw_gateway_상주프로세스_정리.md](./260226_51124_openclaw_gateway_상주프로세스_정리.md) - 51124에서 비관리 openclaw-gateway 상주 프로세스 확인 및 종료/검증 기록 +- [260309_9시_네이버이메일분석_미전송_실패은닉_해결.md](./260309_9시_네이버이메일분석_미전송_실패은닉_해결.md) + - 9시 브리핑 미전송의 직접 원인인 `skill-email` timeout과 실패 은닉 제거 기록