2005년부터 2023년까지 수백 가지의 로그 포맷을 파싱하면서 많은 문제 사례를 접했는데, 어디에도 어떻게 로깅해야 하는가에 대한 좋은 가이드가 없었습니다. 이에 대표적으로 피해야 할 로깅 안티패턴 10선을 정리하였습니다.
안티 패턴
1. 이스케이프 처리 누락
이스케이프 누락은 가장 흔하게 발견되는 사례입니다. 예를 들어 KEY=VALUE KEY2=VALUE2 와 같은 형식으로 로그 포맷을 정의했다면, VALUE에 공백이나 =
문자가 포함될 경우를 고려해야 합니다. KEY="VALUE" 와 같은 형식으로 정의했다면, VALUE에 큰 따옴표가 포함될 경우를 대비한 이스케이프 규칙이 필요합니다. CSV 형식으로 정의했으나 값에 쉼표가 포함된 경우에 대한 이스케이프가 없었던 사례도 있습니다. 이스케이프 규칙이 반영되지 않으면 수신 시스템은 휴리스틱한 처리를 할 수 밖에 없습니다.
2. 버전 헤더 누락
여러분의 솔루션이나 장비는 단 하나의 버전으로 통일되어 고객사에 배포되지 않습니다. 심하게는 한 고객사에서 동일 웹방화벽 제품에 대해 10종의 서로 다른 로그 포맷을 목격한 적도 있습니다. 로그 시작 부분에 버전이 명시되어 있지 않으면, 고객이 로그 파싱이 무언가 잘못된 것을 발견한 이후에야 대응하게 됩니다.
그 때 파서를 수정하려고 해도, 버전 헤더가 없으면 뭔가 로그의 특징적인 부분을 식별해서 파싱을 시도할 수 밖에 없습니다. 휴리스틱한 방법을 사용하여 어떻게든 버전 차이를 식별하더라도 여전히 로그 파싱이 100% 완벽하게 되리라 기대하기 어렵습니다.
3. 로그 타입 누락
하나의 솔루션이나 장비에서는 단 한 가지의 로그 유형만 나오지 않습니다. 당연히 여러 종류의 로그 유형들이 혼합되어 발생합니다. 하지만 로그 시작 위치에 로그 타입 정보가 없으면 이것도 버전 헤더 누락과 마찬가지로 무언가 특징적인 문자열을 식별하여 휴리스틱하게 파싱할 수 밖에 없습니다.
4. 호환성 고려 미비
로그 포맷을 업데이트 할 때는 자연히 필드 순서를 개발자가 보기 좋은 순서대로 다시 정렬하려는 유혹을 느끼게 됩니다. 하지만 외부 시스템을 고려한다면 새로운 필드는 반드시 끝부분에 추가해야 합니다. 만약 로그 중간에 새로운 필드를 삽입하게 되면, 외부 시스템은 버전별로 다른 포맷 파싱 규칙을 적용할 수 밖에 없습니다. 하위 호환성이 없는 로그에 버전 헤더까지 없다면 이것은 파싱에 매우 큰 어려움을 야기합니다.
5. 자연어 문장 사용
사람이 읽기 좋은 자연어 문장으로 로그를 기록하면, 프로그램으로 파싱하기가 극도로 어려워집니다. 대표적 예는 Cisco ASA 방화벽이나 네트워크 스위치의 로그들인데, 메시지 패턴별 정규식 수천 개를 추가해야 모든 경우의 수를 처리할 수 있게 됩니다. 대부분은 파싱을 포기하고 일부 필요한 로그만 파싱하여 활용하는 사태가 벌어집니다.
6. 고정 길이 형식 사용
정반대로 필드별 최대 길이가 정의된 고정 길이 로그 형식은 프로그램이 처리하기에 아주 좋은 것 아니냐고 생각할 수도 있겠습니다. 하지만 대부분의 풀텍스트 인덱스 엔진은 특수 문자를 기준으로 문자열을 분할하여 토큰을 추출하기 때문에, 의도하지 않은 값들이 하나의 토큰으로 인덱스되고 이 때문에 별도의 파싱이 없이는 검색 자체가 어려워집니다.
고정 길이로 인해 공백으로 비는 부분이 발생하므로 비효율적이기도 하며 이후 로그 포맷 업데이트에도 매우 불리합니다.
7. 코드 값 사용
애플리케이션에서는 로케일 처리를 위하여 ID 값을 사용하는 경우가 많습니다. 그렇지만 SIEM과 같은 외부 시스템은 ID에 대응하는 문자열 리터럴을 알 수가 없습니다. 물론 로그 포맷 정의서에 수백 개의 코드에 대한 명세를 작성하여 전달하기도 하지만, 장비나 솔루션이 업데이트 되면 새로 추가된 ID에 대한 정보는 역시 외부 시스템에서 즉각 알 수가 없으므로 기대한대로 파싱되지 않습니다. 코드 값 대신에 처음부터 영문 문자열을 사용하는 것이 변화에 대응하는 좋은 방법입니다.
8. 단위 변환
특히 바이트 값을 KB나 MB 단위로 기록하는 경우가 종종 있습니다. 아무래도 직관적으로 사람이 보기 편하기 때문에 이런 변환을 수행할 것입니다. 그러나 일부 값이 손실되어 정확하게 트래픽 총량을 계산할 수가 없을 뿐 아니라, 파싱 시점에 바이트 단위로 수치를 재변환하기 위해 별도의 로직을 작성해야 합니다.
9. 국제화 고려 미비
그동안 경험한 대부분의 로그의 타임스탬프 값에서 시간대를 보기 어려웠습니다. 여러분의 솔루션이나 장비가 같은 시간대에만 있다면 상관없다고 생각할 수도 있지만, 가령 AWS 인스턴스는 기본 시간대가 UTC에 맞춰져 있기 때문에 별도의 설정으로 타임존 보정을 하지 않으면 정확한 타임스탬프 값을 맞출 수가 없게 됩니다.
한편, 2023년에도 여전히 확장완성형으로 로그를 전송하는 사례가 있습니다. 요즘 세상에 UTF-8을 사용하지 않을 이유가 없습니다.
10. 길이 초과 잘림
JSON 형식의 로그를 SYSLOG over UDP로 전송하게 했는데 로그가 잘리면서 JSON 파서로는 파싱할 수 없게 되는 로그들도 있습니다. SYSLOG를 사용한다면 1000바이트 이내로 로그를 정리하는 것이 좋습니다. 이론적으로는 64K도 가능하지만 MTU를 넘는 로그를 UDP로 전송하면 패킷이 수십 개로 분할되고 이는 커널 스택에 도달하는 과정에서 쉽게 유실될 수 있습니다.
SQL 등 감사 로그를 SYSLOG over UDP로 보내도록 설계한 시스템들도 종종 보게 됩니다. 백업, 복원 수행 시에 SQL 감사 로그는 간단히 메가바이트 단위로 넘어갑니다. 이렇게 되면 감사 로그 본연의 목적을 달성할 수 없게 됩니다.
방화벽처럼 대량의 로그가 발생하는 네트워크 장비가 아니라면 HTTPS로 REST API를 지원하는 방안을 권장합니다. 물론 최근 글로벌 벤더 방화벽은 대부분 HTTPS를 통한 로그 조회도 지원합니다. SYSLOG over UDP는 패킷 스니핑에 전송 내용이 그대로 노출되기 때문입니다.
결론
로그 포맷을 설계할 때 이미 잘 정의된 형식을 사용하는 것을 권장합니다.
- JSON: 로그 길이가 그리 길지 않다면 가장 범용적인 JSON 포맷이 좋습니다. 대부분의 로그 통합 시스템은 JSON을 그대로 인식할 수 있습니다.
- CSV: 반복적으로 키 문자열을 쓰는게 낭비로 느껴진다면, CSV가 대안입니다. 단, 버전, 타입 필드를 시작 위치에 포함하고 이스케이프에 유의하세요.
- WELF: 키=값 형식을 선호한다면 WELF 포맷 규칙을 추천합니다. 단, WELF는 쓸만한 외부 라이브러리가 없으므로 직접 로깅 모듈을 작성하는 과정에서 예외 처리들을 세밀하게 검토해야 합니다.