3. 日志标准化核心概念
日志标准化是构建可观测性体系的基石。在分布式系统中,来自不同服务、不同语言、不同框架的日志如果格式各异,将导致采集、解析、检索和分析的极大困难。本章围绕日志标准化的五个核心维度展开:日志格式规范、字段定义、严重级别、时间戳格式以及上下文传播。
核心原则:标准化不是限制灵活性,而是为异构系统建立统一的“语言契约”。只有日志在结构上达成一致,自动化工具链(如 OpenTelemetry Collector、Elasticsearch、Loki)才能高效运转。
3.1 日志格式规范:从文本到结构化
传统日志多为纯文本格式(如 Apache 的 Common Log Format),依赖正则表达式解析,脆弱且难以扩展。标准化要求日志采用结构化格式,常见方案包括 JSON、Protobuf 以及基于 Key=Value 的文本格式。其中 JSON 因其自描述性和广泛的语言支持,成为业界首选。
| 格式类型 | 示例 | 可读性 | 解析性能 | 推荐场景 |
|---|---|---|---|---|
| 纯文本 | 2025-03-15 10:00:00 ERROR timeout |
高 | 低(依赖正则) | 临时调试、遗留系统 |
| Key=Value | time=2025-03-15T10:00:00Z level=error msg="timeout" |
中 | 中 | 轻量级采集、边缘设备 |
| JSON | {"time":"2025-03-15T10:00:00Z","level":"error","msg":"timeout"} |
中 | 高(原生解析) | 推荐标准 |
| Protobuf | 二进制序列化 | 低 | 极高 | 高性能传输、Agent 内部 |
实践建议:在 OpenTelemetry 生态中,推荐使用 JSON 作为日志输出格式,并确保每条日志独占一行(即 JSON Lines 格式),便于流式采集和行级解析。
3.2 字段定义:最小公共字段集
标准化的核心在于定义一组必须包含的公共字段,以及一组推荐包含的扩展字段。OpenTelemetry 日志数据模型(LogRecord)定义了以下核心字段:
- Timestamp:日志产生的时间,精度建议到纳秒。
- ObservedTimestamp:采集系统观察到日志的时间。
- SeverityText:严重级别文本描述(如 ERROR)。
- SeverityNumber:严重级别数值编码(如 17)。
- Body:日志正文,可以是字符串或结构化对象。
- Attributes:键值对形式的上下文属性。
- TraceId / SpanId:关联分布式追踪的上下文标识。
- Resource:产生日志的资源信息(如服务名、主机名、容器 ID)。
| 字段名 | 类型 | 必须/可选 | 说明 |
|---|---|---|---|
timestamp |
RFC3339Nano | 必须 | 日志产生时间,格式如 2025-03-15T10:00:00.123456789Z |
severity |
string | 必须 | 严重级别文本,如 ERROR、WARN |
body |
string/object | 必须 | 日志消息主体 |
trace_id |
string | 推荐 | 关联追踪的 Trace ID,32 位十六进制 |
span_id |
string | 推荐 | 关联追踪的 Span ID,16 位十六进制 |
service.name |
string | 推荐 | 服务名称,用于日志归属 |
3.3 严重级别:统一数值与文本映射
不同日志库对严重级别的定义存在差异(如 Python 的 logging.WARNING 与 Java 的 Level.WARN)。OpenTelemetry 定义了标准化的 SeverityNumber 枚举,实现跨语言的对齐。
| SeverityNumber | 文本表示 | 说明 |
|---|---|---|
| 1 | TRACE | 最细粒度的调试信息 |
| 5 | DEBUG | 调试信息,开发阶段使用 |
| 9 | INFO | 常规信息,系统正常运行状态 |
| 13 | WARN | 警告,潜在问题但未影响功能 |
| 17 | ERROR | 错误,功能受损 |
| 21 | FATAL | 致命错误,系统可能崩溃 |
避坑指南:不要自定义严重级别文本(如
CRITICAL、NOTICE),除非你同时提供对应的 SeverityNumber 映射。否则在统一告警和过滤时,不同服务的日志将无法按严重程度排序。
3.4 时间戳格式:精度与时区统一
时间戳是日志标准化中最容易被忽视但影响最大的字段。常见问题包括:时区未标注、精度不一致(秒 vs 毫秒 vs 纳秒)、格式不统一。标准方案采用 RFC3339Nano 格式:
# 标准格式示例
2025-03-15T10:00:00.123456789Z
# 格式说明
# 2025-03-15 : 日期
# T : 日期与时间的分隔符
# 10:00:00 : 时间(时:分:秒)
# .123456789 : 纳秒精度(可选,至少毫秒)
# Z : 时区标识(Z 表示 UTC,或 +08:00 表示东八区)
- 强制使用 UTC:所有日志时间戳统一使用 UTC 时间,避免因时区转换导致的混乱。
- 精度至少到毫秒:对于高并发系统,毫秒精度是基本要求;推荐使用微秒或纳秒。
- 避免使用 Unix 时间戳:虽然机器解析方便,但人工排查问题时可读性差。
3.5 上下文传播:关联追踪与日志
日志标准化的最终目标是实现可关联性。在分布式系统中,一个用户请求会穿越多个服务,每个服务产生的日志必须能够通过统一的上下文标识串联起来。OpenTelemetry 通过 TraceId 和 SpanId 实现日志与追踪的关联。
// 示例:在日志中注入追踪上下文
{
"timestamp": "2025-03-15T10:00:00.123456789Z",
"severity": "ERROR",
"body": "数据库连接超时",
"trace_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"span_id": "a1b2c3d4e5f6a7b8",
"attributes": {
"db.system": "postgresql",
"db.operation": "select",
"db.statement": "SELECT * FROM users WHERE id = ?"
},
"resource": {
"service.name": "user-service",
"host.name": "pod-xyz-123"
}
}
关键价值:当日志包含
trace_id 和 span_id 后,你可以从一条错误日志直接跳转到对应的分布式追踪视图,查看该请求在上下游服务中的完整调用链和性能数据,实现真正的“可观测性”。
3.6 标准化落地:从规范到代码
理论规范需要落地到实际的日志输出代码中。以下是一个符合 OpenTelemetry 标准的日志输出示例(Python + structlog):
import structlog
import datetime
# 配置结构化日志
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger()
# 在业务代码中输出标准化日志
logger.error(
"数据库连接超时",
db_system="postgresql",
db_operation="select",
trace_id="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
span_id="a1b2c3d4e5f6a7b8",
service_name="user-service"
)
输出结果:
{"event": "数据库连接超时", "level": "error", "timestamp": "2025-03-15T10:00:00.123456Z", "db_system": "postgresql", "db_operation": "select", "trace_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "span_id": "a1b2c3d4e5f6a7b8", "service_name": "user-service"}
落地检查清单:
- 所有日志输出是否为 JSON 格式?
- 是否包含
timestamp(RFC3339Nano)和severity? - 是否在入口处注入
trace_id和span_id? - 是否统一使用 UTC 时间?
- 是否定义了服务级别的
service.name资源属性?