ログが語ることを数字にする
한국어 원문으로 표시합니다.
목표
표준 라이브러리만으로 접근 로그와 배포 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)다.
단계
/root/pyops/data/logstat.py를 만든다.logstat.py count <로그>는 전체 줄 수와 정규식으로 파싱에 성공한 줄 수를lines=<n> parsed=<m>한 줄로 낸다. 파싱은 시각·경로·상태 코드·처리 시간을 뽑는다.logstat.py status <로그>는 상태 코드 클래스별 건수를2xx=<n> 3xx=<n> 4xx=<n> 5xx=<n>한 줄로 낸다(Counter).logstat.py top <로그> --n 5는 요청 수가 많은 경로 상위 n 개를<건수> <경로>형식으로 한 줄에 하나씩, 많은 순으로 낸다(most_common).logstat.py latency <로그>는 처리 시간의 p50·p95·p99 를p50=<x> p95=<x> p99=<x>로 낸다. statistics.quantiles(times, n=100)(기본 method) 의 [49]·[94]·[98] 을 소수 셋째 자리로 반올림한다.logstat.py worst <로그>는 5xx 가 가장 많았던 1분 구간을worst_minute=<YYYY-MM-DDTHH:MM> count=<n>으로 낸다. 시각은 strptime 으로 aware datetime 을 만들고 초를 0 으로 맞춰 묶는다. 로그의 시간대(+0900) 그대로 적는다./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 는 소수 셋째 자리).logstat.py status <로그> --since <ISO> --until <ISO>로 시각 필터를 붙인다. 두 값은2026-09-10T03:00:00+09:00처럼 시간대가 붙은 ISO 8601 이고, since ≤ ts < until 인 줄만 센다.logstat.py report <로그> --json은 위 지표를 전부 담은 JSON 객체 하나를 표준 출력에 낸다. 키는lines·parsed·status(클래스별 객체)·top([[경로, 건수], ...]5개)·latency(p50·p95·p99)·worst_minute({"minute": ..., "count": ...})다. 이것을/root/pyops/data/report.json에 저장한다.
참고
- 정규식 예:
\[(?P<ts>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^"]*" (?P<status>\d{3}) \d+ "[^"]*" "[^"]*" (?P<rt>[\d.]+)$ - 시각:
datetime.strptime(ts, "%d/%b/%Y:%H:%M:%S %z"), 필터 입력:datetime.fromisoformat(s) - 흔한 실수: 평균으로 지연을 보고하는 것, naive 와 aware 시각을 비교하는 것, 파싱 실패 줄을 세지 않는 것.
한 줄씩 읽고 파싱 성공을 센다
/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 으로 저장합니다.