로그로 원인 찾기 · 섞인 형식을 한 레코드로 · 이론
한 줄이 한 사건은 아니다
한 줄 요약
로그 분석의 첫 단추는 세는 것이 아니라 무엇이 한 레코드인지 정하는 것이다. 형식이 섞인 묶음에서 줄 수를 레코드 수로 착각하면, 예외 한 건이 오류 열 건이 되어 그대로 보고서에 실린다.
왜 이게 필요했나
고객사에서 받는 첫 묶음은 대개 정돈되어 있지 않다. 애플리케이션은 자기 취향의 형식으로 찍고, 앞단 웹서버는 Combined 접근 로그를 쓰고, 방화벽과 프록시는 syslog 로 보낸다. 세 파일을 한 디렉터리에 놓고 grep -c ERROR 를 돌리면 숫자가 하나 나오는데, 그 숫자는 아무 뜻이 없다.
이유는 두 가지다. 첫째, 자바나 파이썬의 예외는 여러 줄이다. 헤더 한 줄 뒤에 at … 프레임이 대여섯 줄, 그 뒤에 Caused by: 가 또 붙는다. 줄로 세면 예외 한 건이 열 건이 된다. 둘째, 줄마다 시각 표기가 다르다. 하나는 +09:00 을 달고, 하나는 05/Mar/2026:14:22:31 +0900 이고, 하나는 UTC 에 Z 다. 시각을 문자열로 정렬하면 이 셋은 뒤죽박죽 선다.
그래서 분석 전에 한 단계가 필요하다. 세 형식을 한 줄 한 레코드의 구조화된 형태로 바꾸고, 시각을 한 가지 기준으로 맞추고, 필드 이름을 통일하는 일이다. 이 일을 정규화라고 부른다.
어떻게 동작하나
정규화는 네 가지 결정으로 이루어진다.
첫째, 레코드의 시작을 무엇으로 볼 것인가. 가장 튼튼한 방법은 "줄이 시각으로 시작하면 새 레코드, 아니면 앞 레코드의 계속" 이다. 예외 프레임은 탭이나 공백으로 시작하고 Caused by: 는 시각이 없으므로 자동으로 앞에 붙는다. 이 규칙 하나로 여러 줄 예외가 해결된다.
둘째, 시각을 어떤 문자열로 굳힐 것인가. [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html)가 인터넷에서 쓰는 표기를 정한다. 이 문서의 5.1절은 왜 이 표기를 쓰는지도 함께 적어 두었다 — 오프셋 표기가 같고 소수 자릿수가 같으면 문자열 정렬이 곧 시간순 정렬이 된다. 그래서 모든 시각을 2026-03-05T05:22:31.118Z 처럼 UTC · 밀리초 세 자리 · Z 로 굳혀 두면, 그 뒤의 모든 작업에서 sort 한 번이면 끝난다.
셋째, 구조화한 결과를 어디에 담을 것인가. 실무의 기본값은 [JSON Lines(NDJSON)](https://jsonlines.org/) 이다. 한 줄에 JSON 객체 하나를 담는 형식이라 head·tail·split 같은 줄 단위 도구가 그대로 통하고, 100GB 짜리도 스트리밍으로 읽을 수 있다. 통짜 JSON 배열은 파일을 끝까지 읽어야 첫 레코드를 쓸 수 있어 큰 로그에는 맞지 않는다.
넷째, 필드 이름을 무엇으로 통일할 것인가. 여기서 바퀴를 새로 만들 이유는 없다. [OpenTelemetry 로그 데이터 모델](https://opentelemetry.io/docs/specs/otel/logs/data-model/)은 로그 레코드를 Timestamp·ObservedTimestamp·SeverityText·SeverityNumber·Body·Attributes 같은 필드로 규정하고, "기존 로그 형식을 이 모델로 모호하지 않게 옮길 수 있어야 한다" 를 설계 요구사항으로 적어 두었다. [Elastic Common Schema](https://www.elastic.co/docs/reference/ecs) 도 같은 자리를 노리는 또 하나의 사전이다. 어느 쪽을 쓰든, 팀 안에서 한 가지를 정해 두는 것이 중요하다.
syslog 를 다룬다면 [RFC 5424](https://www.rfc-editor.org/rfc/rfc5424.html)를 한 번 읽어 둘 값어치가 있다. 머리의 <134> 는 PRI 라고 부르고, 그 안의 숫자 하나가 facility 와 severity 둘을 담는다 — facility 는 몫, severity 는 나머지다(8로 나눈다). 6.2.3절은 TIMESTAMP 가 RFC 3339 의 부분집합이며 T 와 Z 는 반드시 대문자여야 한다고 더 조인다.
현장에서 만나는 모습
버리는 줄이 조용히 생긴다. 파서가 못 읽은 줄을 continue 로 건너뛰면, 그 손실은 어디에도 남지 않는다. RFC 5424 는 6.1절에서 전송 수신자가 지원 크기를 넘는 메시지를 잘라도 되고 버려도 된다고 적는다(최소 480 옥텟, 2048 옥텟까지는 지원해야 한다). 즉 잘린 줄은 정상적으로 발생한다. 그래서 파싱 실패를 예외 상황이 아니라 한 종류의 결과로 다루고, 원문·파일명·줄 번호를 붙여 따로 남긴다. 나중에 "이 시각에 왜 기록이 비었나" 를 물을 때 그 파일이 답이 된다.
같은 이유로 RFC 5424 8.3절은 중요한 정보를 메시지 앞쪽에 두라고 권한다. 뒤가 잘려 나가도 앞은 남기 때문이다.
정규화 규칙은 코드가 아니라 계약이다. 어떤 줄을 레코드의 시작으로 보는지, 시각을 어떻게 굳히는지, 못 읽은 줄을 어디에 두는지를 문서로 적어 두지 않으면 다음 사람이 같은 파일에서 다른 숫자를 낸다. 고객과의 다음 대화에서 "다음부터는 JSON 한 줄로 주세요" 를 요구할 근거도 이 문서다.
다음 실습에서 할 것
고객사 묶음을 그대로 재현한 세 파일을 만들고, 줄 수와 레코드 수를 갈라 센다. 그다음 여러 줄 예외를 한 레코드로 묶고, 접근 로그와 syslog 를 각각 구조화하고, 파싱 못 한 줄을 격리한다. 마지막에는 셋을 공통 스키마로 합쳐 시간순으로 세우고, 정규화 규칙과 버린 줄을 보고서로 남긴다. 채점기는 원본을 스스로 다시 파싱해 여러분의 결과와 대조한다.