LabHub
배우기 러닝패스 코스

It failed, but the exit code was 0

Turn what the log says into numbers

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 으로 저장합니다.