收到的日志里混着三种格式
目标
把格式各不相同的三个日志文件规范化为一行一条记录的 NDJSON,把多行异常合并成一条记录,隔离无法解析的行,然后把三者合并为通用 schema。
为什么重要
在现场第一次拿到的日志往往没有整理过。格式混杂时运行 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——把三者合并为通用 schema,并按时间顺序排列。/root/norm/norm_report.md——把规范化规则和被丢弃的行写成报告。
参考
- 所有
ts都转换成 UTC,写成2026-03-05T05:22:31.118Z的样子——日期与时间之间是大写的T,毫秒三位数,末尾是大写的Z。 - Python 的
datetime.strptime用%z既能读+09:00,也能读+0900。Combined 日志的时间用%d/%b/%Y:%H:%M:%S %z来读取,这时只有 locale 是 C,Mar才能被解析。 - syslog 的
<134>是 PRI。facility 是除以 8 的商,severity 是余数。 - 常见错误:把 stack trace 的行当作独立记录来统计,把响应时间保留为以秒为单位的浮点数,悄悄跳过无法解析的行,不转换成 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 locale 下解析 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 必须是未经改动的原文,以后才能回溯。
把三个原始来源合并成通用 schema
在 /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 中取用。