website: 백엔드 ERROR 로그 알림 스크립트 추가
마침 비슷한 문제를 이미 한 번 풀어놓은 게 있었다. 모집 이메일 발송 실패를 감지하는 `push-email-failure-metric.py`다. 5분 윈도우로 최근 실패 건수를 세서 OCI Monitoring에 custom metric으로 올리고, cron `*/5`로 주기 실행하고, instance principal로 인증하는 구조. 새로 만들 스...
#313에서 운영 원인추적용으로 backend ERROR 로그를 남기기 시작했는데, 그 뒤로 계속 걸리던 문제가 하나 있었다. 로그는 쌓이는데, 그걸 보려면 사람이 SSH로 들어가서 파일을 직접 열어봐야 한다는 것. 사고가 나도 누군가 능동적으로 확인하지 않으면 아무도 모르는 상태였다. "쌓인다"에서 "쌓이면 알림이 온다"로 넘어가는 작업이 필요했다.
다만 하나 다른 지점이 있었다. 이메일 실패는 email_log 테이블에 기록이 남아서 SQL로 세면 됐는데, ERROR 로그는 DB가 아니라 파일에만 있다. 그래서 파일을 직접 읽어야 했고, 여기서 신경 쓴 부분이 두 가지였다.
하나는 어떤 로그 파일을 읽을지 정하는 문제. 로그 파일은 배포 태그별로 나뉘어 쌓이는 구조라(infra/docs/logging.md 참고), 재배포가 일어나면 새 파일이 비어있는 채로 시작한다. 그래서 파일명을 고정해서 참조하는 대신, infra/logs/{prod,stage}/ 디렉터리 안에서 가장 최근에 수정된 파일을 "지금 컨테이너가 실제로 쓰고 있는 로그"로 판단하도록 했다. 다른 파일들은 이전 배포가 남긴 것이라 지금은 아무도 안 쓰니 mtime이 갱신될 일이 없다는 전제다.
def current_log_file(env):
log_dir = _LOG_DIR_TEMPLATE.format(env=env)
candidates = glob.glob(f"{log_dir}/*.log")
if not candidates:
return None
return max(candidates, key=os.path.getmtime)
다른 하나는 "최근 5분"을 어떻게 걸러낼지였다. Spring Boot 기본 로그 포맷의 타임스탬프(yyyy-MM-dd HH:mm:ss.SSS)를 줄 앞에서 그대로 파싱해서, cutoff 시각 이후인 것만 카운트하는 방식을 썼다. 정규식을 짤 때 한 가지 확인이 필요했는데, application.yml의 logging.pattern.level이 로그 레벨 표기를 %5p [reqId=...] 형태로 커스텀하고 있어서 혹시 ERROR 앞에 패딩 공백이 붙는지 봐야 했다. ERROR는 정확히 5글자라 패딩이 안 붙고 타임스탬프 바로 뒤에 그대로 나오는 걸 확인하고 정규식을 그에 맞춰 작성했다.
_LINE_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d{3}\s+ERROR\b")
임계치를 정하는 부분은 기존 이메일실패 알람과 다르게 갔다. 이메일실패는 >2로 잡았는데, 이건 오탈자나 일시적 반송처럼 어느 정도 간헐적으로 발생하는 게 정상 범주에 속하기 때문이다. 반면 backend ERROR 레벨은 #313에서 설계할 때부터 "예상 못한 버그"에만 쓰도록 좁혀놓은 로그다. 흔한 4xx 같은 정상적인 클라이언트 흐름은 GlobalExceptionHandler에서 애초에 ERROR로 안 남기게 되어 있다. 그러니 이 지표는 한 건이라도 뜨면 그 자체로 확인이 필요한 신호라고 보고, 임계치를 0으로 잡았다(ErrorLogCountProd[5m].max() > 0). severity는 디스크·메모리·백업부재처럼 사이트 생존에 직결되는 수준은 아니라서(에러 난 요청 하나만 실패하고 나머지는 정상 동작) 이메일실패와 같은 WARNING으로 맞췄다.
코드 쪽 변경은 스크립트 하나(push-error-log-metric.py)와 observability.md 문서 갱신이 전부였다. 실제로 서버 crontab에 두 줄(prod/stage) 등록하는 것과 OCI Alarm Definition을 콘솔에서 만드는 건 이 PR에 포함시키지 않았다. 둘 다 운영 서버 상태를 직접 건드리는 작업이라, 코드만 담는 이 PR과는 분리해서 머지 후에 진행하기로 했다. 로컬에는 OCI 자격증명도 실제 로그 파일도 없어서, 검증은 python3 -m py_compile로 문법만 확인했고 실제 동작 확인은 서버에 crontab을 등록한 뒤 수동으로 한 번 돌려보는 식으로 미뤘다. 기존 push-*-metric.py 스크립트들을 검증할 때도 같은 방식을 썼던 터라 이번에도 그대로 따랐다.
PR은 커밋 하나로 깔끔하게 올라갔고, 생성한 지 몇 분 만에 머지됐다. 기존에 잘 정리된 패턴이 있으면 새 지표를 추가할 때 고민할 지점이 확 줄어든다는 걸 다시 확인한 작업이었다.