TT Lab
开始
学习 学习路径 课程

从日志里找原因

收到的日志里混着三种格式

在 TT Lab 中继续学习

目标

把格式各不相同的三个日志文件规范化为一行一条记录的 NDJSON,把多行异常合并成一条记录,隔离无法解析的行,然后把三者合并为通用 schema。

为什么重要

在现场第一次拿到的日志往往没有整理过。格式混杂时运行 grep -c,一个异常会被算成十个错误,这个数字还会原样写进报告。时间的写法也随文件而异,按字符串排序得不到时间顺序。所以在分析之前,必须先确定“什么算一条记录”和“把时间固定成什么样的字符串”。这个决定不是代码,而是契约,如果不写成文档,下一个人用同一个文件会得出不同的数字。

步骤

  1. 创建并运行 /root/norm/gen_norm.py,在 /root/norm/raw/ 下生成 app.log、gateway.log、sys.log。
  2. 在 /root/norm/counts.json 中把物理行数和逻辑记录数分开写下。
  3. /root/norm/app.ndjson——把应用日志合并成一行一条记录。
  4. /root/norm/gw.ndjson——对 Combined 访问日志做结构化。
  5. /root/norm/sys.ndjson——对 RFC 5424 syslog 做结构化。
  6. /root/norm/rejects.ndjson——把无法解析的行连同原文和行号隔离起来。
  7. /root/norm/all.ndjson——把三者合并为通用 schema,并按时间顺序排列。
  8. /root/norm/norm_report.md——把规范化规则和被丢弃的行写成报告。

参考

复现客户的日志

创建并运行 /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 中取用。