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 的数据关联方式:

结构化日志与 OpenTelemetry 协同架构 结构化日志与 OpenTelemetry 协同架构 Traces(链路) Metrics(指标) Logs(日志) 结构化日志 timestamp level / message trace_id / span_id trace_id 关联 attributes 关联 OpenTelemetry Collector Trace 存储 Metric 存储 Log 存储 核心协同机制 日志通过 trace_id 与链路关联 通过 attributes 与指标关联

结构化日志是连接 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 将失去上下文验证的锚点。