3、结构化日志基础:什么是结构化日志?JSON格式、日志级别、字段设计原则
3.1 从文本日志到结构化日志的演进
传统日志是纯文本字符串,人类可读但机器难以解析。在分布式系统中,日志需要被采集、过滤、聚合、告警和关联分析,纯文本日志的局限性日益突出。
| 特性 | 传统文本日志 | 结构化日志 |
|---|---|---|
| 格式 | 自由文本,无固定模式 | 键值对,如 JSON |
| 可解析性 | 依赖正则表达式,脆弱 | 机器可直接解析,稳定 |
| 查询效率 | 全文检索,性能低 | 字段索引,快速过滤 |
| 扩展性 | 追加文本,易破坏格式 | 新增字段,向后兼容 |
| 关联能力 | 需手动拼接 TraceID | 内嵌 TraceID/SpanID |
核心转变:日志不再是“写给人类看的文字”,而是“机器可消费的事件数据”。结构化日志是 OpenTelemetry 可观测性三大支柱(日志、指标、链路)协同的基础。
3.2 JSON 格式:结构化日志的事实标准
JSON(JavaScript Object Notation)因其轻量、自描述、跨语言支持,成为结构化日志最广泛采用的序列化格式。一个典型的 JSON 日志条目如下:
{
"timestamp": "2025-03-21T14:32:10.123Z",
"level": "ERROR",
"logger": "com.example.order.Service",
"message": "订单支付超时",
"service.name": "order-service",
"trace_id": "abc123def456",
"span_id": "span789ghi012",
"order_id": "ORD-20250321-0042",
"amount": 299.99,
"duration_ms": 5230,
"exception": {
"type": "java.util.concurrent.TimeoutException",
"message": "等待支付确认超过5秒",
"stacktrace": "java.util.concurrent.TimeoutException: ..."
}
}
JSON 格式的优势在于:
- 自描述性:字段名即语义,无需额外文档
- 嵌套结构:支持对象和数组,表达复杂上下文
- 类型保留:字符串、数字、布尔值、null 原生区分
- 生态成熟:所有编程语言、日志框架、采集工具均支持
注意:避免单行 JSON 过长(超过 10KB),否则会增大网络传输和存储压力。建议对嵌套过深的对象进行扁平化处理,或使用
span_id 引用外部链路数据。
3.3 日志级别:从 DEBUG 到 FATAL
日志级别是结构化日志的必选字段,用于标识事件的严重程度。OpenTelemetry 日志数据模型(LogRecord)沿用了业界通用的级别体系:
| 级别 | 数值 | 典型用途 | 生产环境建议 |
|---|---|---|---|
| TRACE | 1 | 细粒度调试,如函数入口/出口 | 关闭 |
| DEBUG | 5 | 开发调试信息,变量值、分支判断 | 关闭 |
| INFO | 9 | 业务里程碑,如订单创建、用户注册 | 开启 |
| WARN | 13 | 潜在问题,如重试、降级、限流触发 | 开启 |
| ERROR | 17 | 功能异常,需人工介入 | 开启 |
| FATAL | 21 | 系统不可用,进程即将退出 | 开启 |
最佳实践:生产环境默认开启 INFO 及以上级别。DEBUG 日志可通过动态配置开关按需开启,避免磁盘 I/O 爆炸。ERROR 日志必须包含
exception 字段,记录完整堆栈。
3.4 字段设计原则:让日志成为可查询的数据
结构化日志的字段设计直接决定后续的可观测性能力。以下原则来自 OpenTelemetry 语义约定(Semantic Conventions)及一线生产经验:
3.4.1 必选字段(Minimum Required)
timestamp— 事件发生时间,RFC3339 格式,精度到毫秒或微秒level— 日志级别,字符串或数值message— 人类可读的描述,支持模板参数service.name— 服务标识,用于跨服务过滤
3.4.2 可观测性关联字段
trace_id/span_id— 与分布式链路追踪关联resource— 基础设施信息(主机、容器、集群)attributes— 业务自定义键值对,用于过滤和聚合
3.4.3 字段命名规范
| 原则 | 说明 | 示例 |
|---|---|---|
| 小写蛇形 | 统一使用 snake_case |
order_id 而非 orderId |
| 命名空间 | 用点号分隔归属域 | db.query.duration_ms |
| 类型后缀 | 数值字段标注单位 | duration_ms, size_bytes |
| 避免冗余 | 不重复记录上下文 | 已有 trace_id 则不再写 request_id |
3.4.4 字段值类型约束
- 字符串:用于标识符、消息、名称,长度建议不超过 1024 字符
- 数值:用于时长、计数、金额,避免字符串化
- 布尔值:用于开关状态、成功/失败标志
- 对象:用于异常堆栈、请求参数,深度不超过 3 层
- 数组:用于标签列表、批量 ID,元素数量不超过 100
黄金法则:每个字段在被写入时,就要想清楚“这个字段将来会被怎样查询和聚合”。如果无法回答,就不要写。
3.5 结构化日志与 OpenTelemetry 的协同关系
下图展示了结构化日志在 OpenTelemetry 体系中的位置,以及它与 Trace、Metric 的数据关联方式:
结构化日志是连接 Trace 和 Metric 的“粘合剂”。当一条日志携带了 trace_id,运维人员可以从日志直接跳转到对应的分布式调用链;当日志包含 duration_ms 等数值字段,可以聚合为业务指标。
3.6 实战:从零构建一条符合 OpenTelemetry 规范的结构化日志
以下示例使用 Python 的 structlog 库,展示如何输出符合 OpenTelemetry 语义的结构化日志:
import structlog
import json
from datetime import datetime, timezone
# 配置结构化日志处理器
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger()
# 在业务代码中记录日志
def process_order(order_id: str, amount: float):
logger.info("order.received",
order_id=order_id,
amount=amount,
service_name="order-service")
try:
# 模拟支付处理
result = pay(order_id, amount)
logger.info("order.paid",
order_id=order_id,
payment_id=result["payment_id"],
duration_ms=result["duration_ms"])
except Exception as e:
logger.error("order.payment_failed",
order_id=order_id,
amount=amount,
exception={
"type": type(e).__name__,
"message": str(e)
})
输出示例(单行 JSON):
{"event": "order.received", "level": "info", "timestamp": "2025-03-21T14:32:10.123Z", "order_id": "ORD-20250321-0042", "amount": 299.99, "service_name": "order-service"}
生产建议:使用
structlog 或 loguru(Python)、logback + logstash-encoder(Java)、slog(Go)等结构化日志库,避免手动拼接 JSON 字符串。这些库会自动处理时间戳格式化、异常序列化、字段排序等细节。
3.7 常见陷阱与避坑指南
| 陷阱 | 表现 | 解决方案 |
|---|---|---|
| 字段名不统一 | 同一字段在不同服务中叫 userId、user_id、uid |
制定团队级字段命名规范,使用 OpenTelemetry 语义约定 |
| 敏感信息泄露 | 日志中包含密码、身份证、信用卡号 | 在日志处理器中配置脱敏规则,或使用 redact 处理器 |
| 日志行过长 | 单行 JSON 超过 100KB,导致采集器 OOM | 限制单条日志大小(建议 10KB),大对象用引用替代 |
| 缺少 TraceID | 日志无法关联到调用链,排查问题需手动搜索 | 在中间件中自动注入 trace_id 和 span_id |
| 时间戳精度不足 | 多行日志时间相同,无法排序 | 使用毫秒或微秒精度,统一 UTC 时区 |
关键提醒:结构化日志不是“把文本变成 JSON 就完事了”。字段设计决定了日志的可查询性、可关联性和可聚合性。在 OpenTelemetry 体系中,日志是事务边界追踪的最后一公里——没有结构化的日志,Trace 和 Metric 将失去上下文验证的锚点。