引言
日志是最「便宜」的观测手段,也是最容易被糊弄的数据:一行文本里埋着时间、级别、请求 ID、错误码……没有结构化字段之前,每次排障都要靠人工肉眼正则。本文把日志从「写出来」到「用起来」讲透:先讲日志格式设计(该不该结构化、字段怎么约定、级别语义),再讲解析器的构建(正则、流式、多行拼接、字段提取),接着讲日志管道的完整落地(采集 → 清洗 → 聚合 → 路由 → 存储),然后处理采样与脱敏(PII 与合规)、日志轮转与保留策略,最后给常见陷阱与性能优化,让你把日志变成真正可检索、可告警、可审计的工程资产。
前置:/regex-deep-dive/(正则引擎与回溯)、/text-processing-toolkit/(命令行处理)、/others-json-yaml-processing/(结构化格式)。分布式追踪见 可观测性专题。
目录
- 1. 日志为什么难搞:非结构化到结构化的演进
- 2. 日志格式设计:字段、级别与约定
- 3. 正则解析:模式、分组与性能
- 4. 流式解析与多行拼接
- 5. 日志管道:采集清洗聚合路由
- 6. 采样与脱敏:PII 与合规
- 7. 日志轮转与保留策略
- 8. 日志查询与告警
- 9. 常见陷阱
- 10. 速查表与一句话记忆
- 延伸阅读
1. 日志为什么难搞:非结构化到结构化的演进
非结构化日志:人类可读,机器难用。
2026-09-28 10:00:01.234 INFO api-server | request_id=req-7f3a2 user=u-99 order=1234 status=200 cost=35ms
2026-09-28 10:00:01.567 ERROR api-server | timeout calling payment-gateway, retry=1
问题:
- 解析依赖「肉眼 + 正则」,字段错位就错全错
- 时间格式/字段顺序脆弱,一行改版整条管道崩
- 无法直接聚合、无法按字段过滤、无法做告警阈值
结构化日志:机器友好,人类靠格式化看。
{"ts":"2026-09-28T10:00:01.234Z","level":"info","logger":"api-server",
"request_id":"req-7f3a2","user_id":"u-99","order_id":1234,
"http":{"status":200,"latency_ms":35}}
关键权衡:
结构化日志:
+ 字段稳定、可查询/聚合/告警、可自动脱敏
- 可读性差(裸 JSON 难扫)、体积更大、写入略慢
非结构化:
+ 人友好、体积小
- 机器难用、解析脆弱、难维护
→ 现代结论:应用内部日志默认结构化,人类消费靠 UI 格式化。
心智:日志的消费者不止「人」还有「系统」——为系统设计结构化,为人类提供格式化视图。
2. 日志格式设计:字段、级别与约定
最小字段集(所有日志都应该有):
ts 时间戳(ISO 8601 + UTC,别用本地时间)
level 级别(debug/info/warn/error)
logger 来源(模块/服务名)
message 人类可读描述
trace_id / span_id (链路追踪关联)
级别语义要「统一口径」:
| 级别 | 语义 | 使用时机 |
|---|---|---|
| DEBUG | 开发排障细节 | 调试阶段,生产默认关闭 |
| INFO | 正常运行事件 | 请求进出、任务完成 |
| WARN | 异常但不阻断 | 重试、降级、慢查询 |
| ERROR | 功能受损 | 请求失败、依赖错误 |
| FATAL | 进程级故障 | 启动失败、无法恢复 |
字段命名约定(少踩坑):
- 统一 snake_case 或 camelCase,别混用
- 显式表示嵌套(http.status / user.id),别用点号当字段名
- 错误单独成字段(error.kind / error.message / error.stack)
- 业务字段前缀命名空间(order.id / payment.status)
日志的「铁律」:
1. 不打印敏感信息(密码/token/PII)——打标记不打印值
2. 不把日志当唯一事实来源(有损、有采样)
3. 每条日志自带上下文(request_id 贯穿始终)
4. 禁止循环日志(每秒打一条 = 容量炸弹)
心智:日志格式是「接口」——字段、级别、命名先立规矩,否则排障就是一场考古。
3. 正则解析:模式、分组与性能
解析非结构化日志的正则模式:
import re
LOG_PATTERN = re.compile(
r'^(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})'
r'\s+(?P<level>[A-Z]+)'
r'\s+(?P<logger>\S+)'
r'\s*\|(?P<msg>.*)$'
)
def parse(line):
m = LOG_PATTERN.match(line)
if not m:
return None # 不匹配 → 丢到「原始桶」
return m.groupdict()
解析性能的三条法则:
1. 锚定边界:用 ^ 开头、$ 结尾,减少回溯空间
2. 先分级再全量:先抽 level/ts(最稳定),不匹配再降级
3. 缓存编译:re.compile 一次,别每次 match 都编译
字段提取的正确姿势:
- 稳定字段用「位置 + 命名分组」直接提
- 可选字段用 (?P<name>...)? 容忍缺失
- 提取不到的字段落进 raw_line,别让解析器吞数据
- 解析失败要「计数 + 告警」,不是静默丢弃
回溯风险(借鉴 /regex-deep-dive/ 的 ReDoS 结论):
/^(.*)+$/ ← 灾难性回溯(嵌套量词)
/^(a+)+$/ ← 同样危险
→ 生产解析用「线性引擎」(RE2/ripgrep)或加超时
心智:正则解析要「锚定 + 分组 + 兜底」——快、容错、不吞数据,失败要看得见。
4. 流式解析与多行拼接
日志是「流」,不是「文件」——采集端要能边读边解析:
单行流:tail -f 一行一行来 → 每行独立解析
多行流:异常堆栈/SQL 跨多行 → 需要「拼接缓冲」
多行日志的拼接策略:
策略一:下一行若不以「已知起始模式」开头 → 并入上一行
例:Java 异常堆栈第一行含 "Exception",后续缩进行都并入
策略二:时间戳识别
新行若匹配「时间戳前缀」 → 新日志;否则并入上一条
→ 最通用,因为每条日志都以时间戳开始
策略三:明确的结束标记
如 GELF 的 \x00 分隔,或 protobuf 长度前缀
# 多行拼接(时间戳识别法)
def multiline(lines):
buf = []
for line in lines:
if is_ts_start(line): # 新日志开始
if buf: yield '\n'.join(buf)
buf = [line]
else:
buf.append(line) # 续行并入
if buf: yield '\n'.join(buf)
流式处理的工程要点:
- 采集器(Filebeat/Fluent Bit)原生支持 multiline 配置
- 大文件回放(读历史日志)与实时流共用同一解析逻辑
- 缓冲要有上限(拼接等待超过 N 行/N 时间 → 强制 flush)
心智:日志是流——单行直接过、多行靠「时间戳/起始模式」拼接,缓冲必须有上限防悬挂。
5. 日志管道:采集清洗聚合路由
典型日志管道的五个阶段:
① 采集(Collect) → 从应用/容器/系统抓日志
② 清洗(Parse) → 格式归一、字段提取
③ 聚合(Aggregate) → 同一 trace/接口的日志合并、指标提取
④ 路由(Route) → 按级别/服务/标签分流
⑤ 存储(Store) → 热存储(ES/Loki)+ 冷归档(对象存储)
采集层的选择:
Filebeat / Fluent Bit:轻量、agent 形态、多源
Fluentd / Vector:较重、可编程、富转换
→ 组合:边缘用轻量采集,中心用重处理
清洗层的转换:
# 清洗示例:字段归一 + 时间标准化
def clean(record):
if 'ts' in record:
record['@timestamp'] = normalize_ts(record['ts']) # → ISO UTC
if 'latency_ms' in record:
record['latency_s'] = record['latency_ms'] / 1000
record['level'] = record.get('level', 'INFO').upper()
return record
路由规则:
level=ERROR → 告警队列(快、保留久)
服务=pay → 独立索引(方便查询隔离)
栈堆日志 → 单独压缩存储(体积大、查询少)
心智:日志管道 = 采集 → 清洗 → 聚合 → 路由 → 存储,每一级只做一件事,级间用队列解耦。
6. 采样与脱敏:PII 与合规
日志里的敏感数据是合规红线(GDPR/PIPL/PCI):
PII:邮箱、手机号、身份证、IP、精确位置
凭证:密码、token、API key、session id
支付:卡号(可留后四位)、完整账单号
脱敏的两种时机:
源头脱敏(推荐):应用写日志前就屏蔽敏感值
→ 打标记不打值:log("user_login", masked_email="a***@gmail.com")
管道脱敏(兜底):采集/清洗层正则替换
→ 对历史非结构化日志也有效
# 管道层正则脱敏(兜底方案)
PII_PATTERNS = [
(r'\b[\w.+-]+@[\w-]+\.[\w.]+\b', '<EMAIL>'),
(r'\b\d{11}\b', '<PHONE>'),
(r'(?i)\b(api[_-]?key|token)\b\s*[:=]\s*\S+', r'\1=<REDACTED>'),
]
def mask(text):
for pattern, repl in PII_PATTERNS:
text = re.sub(pattern, repl, text)
return text
采样策略(大数据量下保质量):
- 全量保留 ERROR、超阈值慢日志(这些最重要)
- INFO 按需采样(如高流量接口 10% 采样)
- 调试日志直接丢弃(生产不收集)
- 采样要带采样率字段,查询时别把采样当全量
心智:脱敏优先在源头做、管道兜底;采样只牺牲低价值日志,关键日志全量保留。
7. 日志轮转与保留策略
日志不轮转 = 磁盘写满 = 服务崩溃。轮转与保留是运维基本功:
按大小轮转:single.log → single.log.1 → ...(日志收集器/max log size)
按时间轮转:每天一个文件(daily),配合定时清理
保留策略:保留 N 天 / N GB,超期压缩归档
常见工具:
# Linux logrotate 经典配置
/path/to/app/*.log {
daily
rotate 7 # 保留 7 个轮转文件
compress # 压缩旧日志(.gz)
delaycompress # 延迟一天压缩(配合应用仍打开的 fd)
copytruncate # 复制后截断原文件(应用不感知)
missingok
notifempty
}
容器与云原生场景:
容器:日志写 stdout/stderr → 由平台采集,轮转交给引擎
(应用别自己写文件,交给 12-factor 日志约定)
云存储:热数据本地/托管检索,冷数据归档对象存储(S3/OSS)
保留周期按合规要求(如审计日志 6 个月)
保留策略的权衡:
- 磁盘成本 vs 排查能力:日志删了就不能再查
- 审计/合规日志有硬性保留期(不能提前删)
- 线上事故复盘需要「事发前后的日志」→ 事故隔离期日志重点保
心智:轮转防磁盘满、保留要分级——热数据可查、冷数据归档、审计数据按合规期锁死。
8. 日志查询与告警
日志的价值在「查询」与「告警」——结构化之后才能放大:
查询模式(ELK/Loki/Splunk 类):
按字段过滤:level=ERROR AND service=api
时间范围:last 1h
全文模糊:message: "timeout"
聚合统计:count by error.kind
# Loki LogQL 示例
{service="api-server"} |= "timeout"
{service="api-server"} | json | level="error"
count_over_time({service="api-server"} | json | level="error" [5m])
从日志到告警的链路:
1. 定义规则:某级别/错误码在窗口内出现次数超阈值
2. 上下文聚合:同 request_id 的错误归为一件事(去重)
3. 降噪:先告警聚合(1 次故障 = 1 条告警,不是 1000 条)
4. 分级:ERROR 瀑布 → 只对「根因级」告警
日志告警的常见误区:
误区:对每条 ERROR 都告警 → 告警疲劳,真正问题被淹没
正解:告警「错误率/错误模式」,不告警「单个错误」
心智:日志告警告的是「率与模式」不是「单条错误」——先聚合降噪,再设定阈值,避免告警疲劳。
9. 常见陷阱
| 陷阱 | 现象 | 规避 |
|---|---|---|
| 日志打敏感值 | 合规事故 | 源头脱敏 + 管道兜底 |
| 每条都打 INFO | 容量炸弹 | 分级 + 采样 |
| 无 trace_id | 关联不上 | 贯穿上下文 |
| 解析失败静默 | 数据丢失无感知 | 计数 + 告警 + 原始桶 |
| 正则回溯 | 解析被卡死 | 锚定 + 线性引擎 |
| 无轮转 | 磁盘写满 | logrotate / 平台接管 |
| 采样当全量 | 误判错误率 | 带采样率字段 |
| 单条告警 | 告警疲劳 | 聚合 + 阈值 |
10. 速查表与一句话记忆
全篇速查:
| 主题 | 结论 |
|---|---|
| 定位 | 日志默认结构化,人类看格式化视图 |
| 字段 | ts/level/logger/message/trace_id |
| 级别 | 统一口径,ERROR 才告警 |
| 解析 | 锚定正则 + 分组 + 原始桶兜底 |
| 多行 | 时间戳/起始模式拼接,缓冲设上限 |
| 管道 | 采集→清洗→聚合→路由→存储 |
| 脱敏 | 源头优先 + 管道兜底 |
| 采样 | 关键日志全量,低价值采样带率 |
| 轮转 | logrotate/平台接管,按合规保留 |
| 告警 | 告「率与模式」不告「单条」 |
一句话记忆:日志默认结构化、字段立约定;解析用锚定正则加兜底桶、多行靠时间戳拼接;管道五段各司其职,脱敏源头优先管道兜底,采样只牺牲低价值日志;轮转防磁盘满、保留按合规分级,告警告「率与模式」而非单条错误,避免疲劳——日志是排障的地基,别让它变成灾后考古。
延伸阅读
- /regex-deep-dive/ — 正则引擎、回溯与灾难性回溯(解析层基础)
- /text-processing-toolkit/ — grep/awk/jq 命令行日志处理
- /others-json-yaml-processing/ — 结构化日志的格式与 Schema
- /serialization-formats-compare/ — 日志序列化格式对比
- 可观测性专题 — 指标、追踪与日志的完整观测体系
- DevOps 专题 — 日志平台的部署与运维
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。