The Logs Arrived in Three Different Formats
한국어 원문으로 표시합니다.
목표
형식이 서로 다른 로그 파일 세 개를 한 줄 한 레코드의 NDJSON 으로 정규화하고, 여러 줄 예외를 한 레코드로 묶고, 파싱하지 못한 줄을 격리한 뒤 셋을 공통 스키마로 합칩니다.
왜 중요한가
현장에서 처음 받는 로그 묶음은 정돈되어 있지 않습니다. 형식이 섞인 채로 grep -c 를 돌리면 예외 한 건이 오류 열 건으로 세어지고, 그 숫자가 그대로 보고서에 실립니다. 시각 표기도 파일마다 달라서 문자열로 정렬하면 시간순이 되지 않습니다. 그래서 분석 전에 "무엇이 한 레코드인가" 와 "시각을 어떤 문자열로 굳히는가" 를 먼저 정해야 합니다. 이 결정은 코드가 아니라 계약이고, 문서로 남지 않으면 다음 사람이 같은 파일에서 다른 숫자를 냅니다.
단계
/root/norm/gen_norm.py를 만들어 실행해/root/norm/raw/아래 app.log · gateway.log · sys.log 를 만드세요./root/norm/counts.json에 물리적 줄 수와 논리 레코드 수를 갈라 적으세요./root/norm/app.ndjson— 애플리케이션 로그를 한 줄 한 레코드로 묶으세요./root/norm/gw.ndjson— Combined 접근 로그를 구조화하세요./root/norm/sys.ndjson— RFC 5424 syslog 를 구조화하세요./root/norm/rejects.ndjson— 파싱하지 못한 줄을 원문과 줄 번호로 격리하세요./root/norm/all.ndjson— 셋을 공통 스키마로 합쳐 시간순으로 세우세요./root/norm/norm_report.md— 정규화 규칙과 버린 줄을 보고서로 남기세요.
참고
- 모든
ts는 UTC 로 바꾸어2026-03-05T05:22:31.118Z모양으로 적습니다 — 날짜와 시각 사이에 대문자T, 밀리초 세 자리, 끝에 대문자Z. - 파이썬의
datetime.strptime은%z로+09:00과+0900을 모두 읽습니다. Combined 로그의 시각은%d/%b/%Y:%H:%M:%S %z로 읽히는데, 이때 로캘이 C 여야Mar이 해석됩니다. - syslog 의
<134>는 PRI 입니다. facility 는 8로 나눈 몫, severity 는 나머지입니다. - 흔한 실수: 스택트레이스 줄을 독립 레코드로 세기, 응답시간을 초 단위 실수 그대로 두기, 파싱 못 한 줄을 조용히 건너뛰기, 시각을 UTC 로 바꾸지 않고 문자열로 정렬하기.
- 이 실습의 산출물은 전부
/root/norm/아래에 모읍니다. 세션이 끝나면 사라지므로 중요한 것은 화면에 남겨 두세요.
고객사 로그 묶음 재현하기
/root/norm/gen_norm.py 를 만들어 실행해 /root/norm/raw/ 아래 app.log(91줄) · gateway.log(60줄) · sys.log(30줄)을 만드세요.
세 파일은 서로 다른 프로그램이 쓴 것이라 형식이 다릅니다. app.log 는 예외가 여러 줄이라 줄 수가 레코드 수보다 많고, sys.log 는 몇 줄이 전송 중에 잘려 있습니다. 먼저 /root/norm/raw 를 만들고 그 안에 세 파일을 씁니다.
줄 수와 레코드 수를 갈라 세기
/root/norm/counts.json 에 app_lines · app_records · gateway_records · syslog_lines · syslog_records · rejected_lines · total_records 를 적으세요. total_records 는 세 파일에서 살아남은 레코드의 합입니다.
app.log 는 시각으로 시작하는 줄만 레코드의 머리입니다. sys.log 는 30줄이지만 RFC 5424 머리를 갖추지 못한 줄이 섞여 있어 레코드 수가 더 적습니다. 두 숫자가 다르다는 사실 자체가 이 단계의 답입니다.
여러 줄 예외를 한 레코드로 묶기
/root/norm/app.ndjson 에 레코드마다 ts(UTC·Z) · level · logger · msg · stack_lines 를 담아 한 줄에 하나씩 쓰세요. stack_lines 는 그 레코드에 딸린 추가 줄 수입니다.
줄이 시각으로 시작하면 새 레코드, 아니면 직전 레코드의 계속입니다. 이 규칙 하나면 at … 프레임과 Caused by: 가 자동으로 앞에 붙습니다. +09:00 은 %z 로 읽히고, UTC 변환은 astimezone(timezone.utc) 입니다.
접근 로그를 구조화하기
/root/norm/gw.ndjson 에 ts(UTC·Z) · method · path · status(정수) · bytes(정수) · rt_ms(밀리초 정수)를 담아 한 줄에 하나씩 쓰세요.
Combined 로그의 시각은 대괄호 안에 05/Mar/2026:14:22:31 +0900 모양으로 있습니다. %d/%b/%Y:%H:%M:%S %z 로 읽히고, %b 는 C 로캘에서 Mar 을 해석합니다. 마지막 칸의 응답시간은 초 단위 실수라 1000을 곱해 반올림합니다.
PRI 를 facility 와 severity 로 가르기
/root/norm/sys.ndjson 에 ts(UTC·Z) · host · app · facility(정수) · severity(정수) · msgid · msg 를 담아 한 줄에 하나씩 쓰세요. 파싱하지 못한 줄은 여기에 넣지 않습니다.
RFC 5424 한 줄은 <PRI>1 TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSG 입니다. PRI 안의 숫자 하나가 둘을 담고 있습니다 — 8로 나눈 몫과 나머지입니다. 머리를 갖추지 못한 줄은 지금 건너뛰고, 다음 단계에서 따로 모읍니다.
파싱 못 한 줄을 버리지 않고 격리하기
/root/norm/rejects.ndjson 에 파싱하지 못한 줄을 src_file · line_no(1부터) · raw(원문 그대로) · reason 으로 남기세요.
파서가 못 읽은 줄을 continue 로 넘기면 손실이 어디에도 남지 않습니다. 파싱 실패는 예외 상황이 아니라 한 종류의 결과입니다. 줄 번호는 원본 파일에서 1부터 세고, raw 는 손대지 않은 원문 그대로여야 나중에 되짚을 수 있습니다.
세 원본을 공통 스키마로 합치기
/root/norm/all.ndjson 에 ts · source(app|gateway|syslog) · severity(ERROR|WARN|INFO) · message 를 담아 ts 오름차순으로 쓰세요. 접근 로그는 5xx 가 ERROR, 4xx 가 WARN, 나머지가 INFO 이고, syslog 는 severity 숫자 3 이하가 ERROR, 4 가 WARN, 나머지가 INFO 입니다.
앞 단계에서 만든 세 NDJSON 을 읽어 쓰면 다시 파싱할 필요가 없습니다. severity 를 세 값으로 접는 것이 이 단계의 핵심입니다 — 원본마다 등급 체계가 달라서 그대로 두면 비교할 수 없습니다. 시각을 이미 같은 표기로 굳혀 두었으므로 정렬은 문자열 정렬이면 됩니다.
정규화 규칙을 문서로 남기기
/root/norm/norm_report.md 에 ## 무엇이 섞여 있었나 ## 어떻게 한 줄 한 레코드로 만들었나 ## 버린 줄과 그 이유 ## 다음에 받을 때의 요구사항 네 절로 적으세요. 전체 레코드 수 · app.log 물리적 줄 수 · 격리한 줄 수를 숫자로 포함해야 합니다.
정규화 규칙은 코드가 아니라 계약입니다. 어떤 줄을 레코드의 시작으로 보았는지, 시각을 어떻게 굳혔는지, 못 읽은 줄을 어디에 두었는지를 적어 두지 않으면 다음 사람이 같은 파일에서 다른 숫자를 냅니다. 숫자는 counts.json 에서 꺼내 쓰세요.