LabHub
배우기 러닝패스 코스

失敗したのに終了コードは 0 だった

ログが語ることを数字にする

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

표준 라이브러리만으로 접근 로그와 배포 CSV 에서 운영 지표(건수·상태 분포·상위 경로·분위수·최악의 1분·서비스별 실패율)를 뽑는 도구를 만든다.

왜 중요한가

운영 서버에는 pandas 도 인터넷도 없지만 csv·Counter·statistics·datetime 은 있다. 한 줄씩 읽는 파서, 분위수, 시간대가 붙은 시각 비교 — 이 셋을 손에 익히면 로그가 5천 줄이든 50만 줄이든 같은 도구로 답을 낸다. 재료는 /opt/fixtures/pyops/access.log(nginx combined + 마지막 열 처리 시간(초), 2026-09-10 02:00 부터 05:00 KST 까지, 03:12 부터 03:17 까지가 장애 구간)와 /opt/fixtures/pyops/deploys.csv(service,version,deployed_at,duration_s,status)다.

단계

  1. /root/pyops/data/logstat.py 를 만든다. logstat.py count <로그> 는 전체 줄 수와 정규식으로 파싱에 성공한 줄 수를 lines=<n> parsed=<m> 한 줄로 낸다. 파싱은 시각·경로·상태 코드·처리 시간을 뽑는다.
  2. logstat.py status <로그> 는 상태 코드 클래스별 건수를 2xx=<n> 3xx=<n> 4xx=<n> 5xx=<n> 한 줄로 낸다(Counter).
  3. logstat.py top <로그> --n 5 는 요청 수가 많은 경로 상위 n 개를 <건수> <경로> 형식으로 한 줄에 하나씩, 많은 순으로 낸다(most_common).
  4. logstat.py latency <로그> 는 처리 시간의 p50·p95·p99 를 p50=<x> p95=<x> p99=<x> 로 낸다. statistics.quantiles(times, n=100)(기본 method) 의 [49]·[94]·[98] 을 소수 셋째 자리로 반올림한다.
  5. logstat.py worst <로그> 는 5xx 가 가장 많았던 1분 구간을 worst_minute=<YYYY-MM-DDTHH:MM> count=<n> 으로 낸다. 시각은 strptime 으로 aware datetime 을 만들고 초를 0 으로 맞춰 묶는다. 로그의 시간대(+0900) 그대로 적는다.
  6. /root/pyops/data/deploys.py 를 만든다. deploys.py summary <csv> 는 서비스별로 service=<이름> total=<n> failed=<m> fail_rate=<0.000> median_s=<x> 를 서비스 이름 순으로 한 줄씩 낸다(DictReader·statistics.median, fail_rate 는 소수 셋째 자리).
  7. logstat.py status <로그> --since <ISO> --until <ISO> 로 시각 필터를 붙인다. 두 값은 2026-09-10T03:00:00+09:00 처럼 시간대가 붙은 ISO 8601 이고, since ≤ ts < until 인 줄만 센다.
  8. logstat.py report <로그> --json 은 위 지표를 전부 담은 JSON 객체 하나를 표준 출력에 낸다. 키는 lines·parsed·status(클래스별 객체)·top([[경로, 건수], ...] 5개)·latency(p50·p95·p99)·worst_minute({"minute": ..., "count": ...})다. 이것을 /root/pyops/data/report.json 에 저장한다.

참고

한 줄씩 읽고 파싱 성공을 센다

/root/pyops/data/logstat.py 를 만든다. logstat.py count <로그>lines=<n> parsed=<m> 을 낸다. 시각·경로·상태·처리 시간을 뽑는 정규식으로 파싱한다.

파일은 for line in f 로 한 줄씩 읽고, re.compile 한 패턴의 search 가 None 이면 건너뛰되 lines 는 셉니다. 참고 절의 정규식을 그대로 써도 됩니다.

상태 코드 클래스별 건수

logstat.py status <로그>2xx=<n> 3xx=<n> 4xx=<n> 5xx=<n> 한 줄을 낸다.

Counter 에 f"{status // 100}xx" 를 키로 넣으세요. 없는 클래스는 0 으로 찍어야 합니다(dict.get(k, 0)).

요청이 많은 경로 상위 n

logstat.py top <로그> --n 5<건수> <경로> 를 많은 순으로 한 줄에 하나씩 낸다.

Counter.most_common(n) 이 (값, 건수) 목록을 많은 순으로 돌려줍니다. 출력 순서는 건수 경로 입니다.

평균이 아니라 분위수

logstat.py latency <로그>p50=<x> p95=<x> p99=<x> 를 낸다. statistics.quantiles(times, n=100) 의 [49]·[94]·[98] 을 소수 셋째 자리로 반올림한다.

n=100 이면 절단점이 99개라 인덱스 49·94·98 이 p50·p95·p99 입니다. method 는 기본값(exclusive)을 그대로 두세요.

5xx 가 가장 많았던 1분

logstat.py worst <로그>worst_minute=<YYYY-MM-DDTHH:MM> count=<n> 을 낸다. 시각은 로그의 시간대 그대로, 초를 0 으로 맞춰 1분 단위로 묶는다.

strptime 에 %z 를 쓰면 aware datetime 이 됩니다. ts.replace(second=0, microsecond=0) 을 Counter 의 키로 쓰고 most_common(1) 을 보세요. 출력은 strftime("%Y-%m-%dT%H:%M") 입니다.

배포 CSV 의 서비스별 실패율

/root/pyops/data/deploys.py 를 만든다. deploys.py summary <csv> 가 서비스 이름 순으로 service=<이름> total=<n> failed=<m> fail_rate=<0.000> median_s=<x> 를 한 줄씩 낸다.

csv.DictReader 는 행마다 딕셔너리를 주고 값은 전부 문자열입니다. defaultdict(list) 에 duration_s 를 모아 statistics.median 을 내세요. fail_rate 는 failed / total 을 소수 셋째 자리로.

시간대가 붙은 시각으로 거른다

logstat.py status <로그> --since <ISO> --until <ISO> 가 since ≤ ts < until 인 줄만 센다. 두 값은 시간대가 붙은 ISO 8601(예: 2026-09-10T03:00:00+09:00)이다.

argparse 의 type=datetime.fromisoformat 으로 받으면 aware datetime 이 됩니다. naive 값과 비교하면 TypeError 가 나니, 입력에 시간대가 있는지 확인하세요.

전부를 JSON 보고서로

logstat.py report <로그> --json 이 lines·parsed·status·top(5개, [경로, 건수])·latency·worst_minute({minute, count}) 을 담은 JSON 객체 하나를 내고, 그것을 /root/pyops/data/report.json 에 저장한다.

json.dumps 에 datetime 을 넣으면 실패합니다 — 문자열로 바꾼 뒤 넣으세요. logstat.py report ... --json > /root/pyops/data/report.json 으로 저장합니다.