4. OpenTelemetry 日志数据模型:LogRecord 结构、属性、资源与作用域

在 OpenTelemetry(OTel)体系中,日志并非简单的文本字符串,而是被抽象为一种结构化的数据模型。理解 LogRecord 的构成,是标准化日志采集的基石。本章将深入剖析 LogRecord 的核心组件:结构体字段、属性(Attributes)、资源(Resource) 和 作用域(Scope),并展示它们如何协同工作,形成可观测性数据的统一语义。

4.1 LogRecord 核心结构

LogRecord 是 OTel 日志数据模型中的最小独立单元,代表一条日志事件。其结构设计兼顾了 高性能 与 可扩展性,通过预定义字段与动态属性的组合,覆盖了从传统 Syslog 到现代云原生应用的所有日志场景。

关键设计原则: LogRecord 不强制要求所有字段必填,但推荐尽可能填充结构化字段,以提升下游分析引擎(如 Elasticsearch、Loki)的查询效率。

下表列出了 LogRecord 的核心字段及其语义说明:

字段名称 类型 说明 示例值
Timestamp UnixNano 日志事件发生的时间戳(纳秒精度) 1697200000000000000
ObservedTimestamp UnixNano 日志被采集系统观察到的时间戳 1697200001000000000
SeverityText string 可读的严重级别文本(如 ERROR) ERROR
SeverityNumber int 标准化的严重级别数值(如 17 代表 Error) 17
Body Any 日志正文,可以是字符串或结构化值 {"message": "Connection refused", "port": 8080}
Attributes Map 与日志事件直接相关的键值对(动态元数据) {"http.method": "GET", "http.status_code": 500}
TraceId bytes 关联的分布式追踪 ID(用于链路关联) 0af7651916cd43dd8448eb211c80319c
SpanId bytes 关联的 Span ID b7ad6b7169203331
Resource Resource 标识日志来源的实体信息(如主机、容器、服务) {"service.name": "payment-api", "host.name": "node-1"}
Scope InstrumentationScope 标识产生日志的 instrumentation 库信息 {"name": "my-logger", "version": "1.0.0"}
实践建议: 在采集阶段,优先填充 Timestamp、SeverityNumber 和 Body。如果应用已接入分布式追踪,务必注入 TraceId 和 SpanId,实现日志与追踪的无缝关联。

4.2 属性(Attributes):动态元数据的载体

Attributes 是 LogRecord 中最灵活的部分,用于携带与日志事件相关的 上下文信息。它们遵循 OTel 的 语义约定(Semantic Conventions),确保不同服务产生的同类日志具有一致的字段命名。

  • 通用属性:如 http.method、http.url、db.system、exception.type。
  • 自定义属性:允许用户添加业务相关的键值对,如 order.id、user.role。
  • 类型支持:属性值支持字符串、布尔、整数、浮点数、数组以及嵌套的 Map 结构。
避坑指南: 避免在 Attributes 中存放大量重复的静态数据(如固定的环境名称)。这类信息应放入 Resource 中,以降低每条日志的序列化开销。

4.3 资源(Resource):日志来源的实体标识

Resource 描述了产生日志的 实体,通常在整个服务生命周期内保持不变。它独立于 LogRecord 的 Attributes,用于在跨服务、跨主机场景下快速定位日志来源。

常见的 Resource 属性包括:

属性键 说明 示例值
service.name 服务名称(必填) user-service
service.namespace 服务所属命名空间 production
host.name 主机名 ip-10-0-1-23
k8s.pod.name Kubernetes Pod 名称 payment-api-7d8f9c6b4c-x2y3z
cloud.region 云服务区域 us-east-1
最佳实践: Resource 应在 SDK 初始化时一次性设置,并通过 Resource Detector 自动从环境(如环境变量、K8s Downward API、云厂商 Metadata)中获取,避免硬编码。

4.4 作用域(Scope):Instrumentation 库的标识

Scope 用于标识是 哪个 instrumentation 库 产生了这条日志。它解决了多库共存时的归属问题,使得在分析日志时可以按库进行过滤或隔离。

Scope 包含两个核心字段:

  • name:库的名称,如 io.opentelemetry.contrib.mongodb。
  • version:库的版本号,如 1.2.3。
典型场景: 当应用同时使用了 OTel 官方 SDK 和第三方自定义 Logger 时,通过 Scope 可以区分日志来源,便于排查是 SDK 问题还是业务代码问题。

4.5 数据模型关系总览

下图展示了 LogRecord、Attributes、Resource、Scope 之间的层级关系:

LogRecord 数据模型层级关系 LogRecord Attributes Resource Scope 说明: Attributes 随每条日志变化;Resource 在整个服务生命周期内固定;Scope 标识日志产生的库。

4.6 完整 LogRecord 示例(Protobuf 表示)

以下是一个完整的 LogRecord 在 OTel 协议中的 Protobuf 序列化示例,展示了各字段如何组合在一起:

{
  "resource": {
    "attributes": [
      {"key": "service.name", "value": {"stringValue": "order-service"}},
      {"key": "host.name", "value": {"stringValue": "ip-10-0-1-45"}}
    ]
  },
  "scope": {
    "name": "com.example.logger",
    "version": "2.1.0"
  },
  "logRecords": [
    {
      "timeUnixNano": "1697200000000000000",
      "severityNumber": 17,
      "severityText": "ERROR",
      "body": {"stringValue": "Failed to process payment for order 12345"},
      "attributes": [
        {"key": "order.id", "value": {"stringValue": "12345"}},
        {"key": "payment.amount", "value": {"doubleValue": 99.99}},
        {"key": "http.status_code", "value": {"intValue": 500}}
      ],
      "traceId": "0af7651916cd43dd8448eb211c80319c",
      "spanId": "b7ad6b7169203331"
    }
  ]
}
关键观察: Resource 和 Scope 在传输时被提取到顶层,与 LogRecord 数组并列。这种设计避免了每条日志重复携带相同的 Resource 和 Scope 信息,显著降低了网络带宽和存储开销。

4.7 标准化采集的落地要点

在实现日志采集标准化时,请遵循以下原则:

  1. 统一 Resource 注入:通过 OTel Collector 的 resource processor 或 SDK 的 Resource Detector,确保所有日志都携带 service.name、host.name 等关键标识。
  2. 语义化 Attributes:优先使用 OTel 语义约定中的属性键,避免自定义命名导致的查询混乱。
  3. Scope 隔离:为不同的日志库(如 Logback、Log4j、自定义 Logger)设置独立的 Scope,便于在 UI 中按库过滤。
  4. 链路关联:在微服务架构中,务必通过 TraceId 和 SpanId 将日志与追踪关联,实现端到端的可观测性。