4. 核心数据模型:Log(日志)的定义、与 Trace/Metric 的关联、LogRecord 结构
在可观测性三大支柱中,Log(日志) 是最古老、最广泛使用的数据形式。OpenTelemetry 并未重新发明日志,而是定义了 Log 数据模型 的标准结构,并提供了将日志与 Trace、Metric 关联的机制,从而打破传统日志孤岛,实现真正的可观测性融合。
4.1 什么是 OTel 中的 Log?
在 OpenTelemetry 语境下,Log 是一条带有时间戳的、记录系统或应用程序中发生事件的文本记录。与传统日志不同,OTel 的 Log 数据模型是结构化的,它强制要求日志记录包含一组标准属性,使其可以被机器解析、关联和分析。
核心转变: 从“非结构化文本”到“结构化事件”。OTel 不要求你停止写日志,而是要求你按照
LogRecord 的规范来记录日志,从而让日志成为可观测性数据的一部分,而非孤立文本。
4.2 Log 与 Trace / Metric 的关联
这是 OTel 日志模型最具价值的部分。通过标准属性,日志可以无缝关联到 Trace 和 Metric,形成完整的可观测性拼图。
| 关联维度 | 关键属性 | 关联方式与价值 |
|---|---|---|
| Log ↔ Trace | trace_id, span_id |
日志记录中包含当前请求的 Trace ID 和 Span ID。当排查问题时,可以从一条错误日志直接跳转到对应的分布式调用链,查看完整的请求上下文。 |
| Log ↔ Metric | metric.name, metric.value (通过 LogRecord 的 body 或 attributes) |
日志可以记录指标的瞬时值或事件。例如,记录“订单创建成功”日志时,可以同时携带订单金额作为属性,后续通过日志分析聚合出“总订单金额”指标。 |
| Log ↔ Resource | resource.* (如 service.name, host.name) |
所有 LogRecord 都关联一个 Resource 对象,标识日志来源的服务、主机、容器等。这是跨服务日志聚合和过滤的基础。 |
实战案例: 当用户下单失败时,系统输出一条错误日志。该日志包含
trace_id: "abc123" 和 span_id: "def456"。通过 OTel 后端,你可以:
- 从这条日志出发,直接查询 Trace ID 为
abc123的完整调用链。 - 在调用链中看到该 Span 的详细属性,如请求参数、数据库查询耗时。
- 同时,该日志的
service.name属性让你知道是哪个微服务出了问题。
4.3 LogRecord 结构详解
LogRecord 是 OTel 日志数据模型的核心对象。它定义了日志记录必须包含的字段,确保不同语言和框架产生的日志具有一致的结构。
下图展示了 LogRecord 的核心字段及其在可观测性体系中的位置:
4.4 LogRecord 字段速查表
| 字段名 | 类型 | 必需性 | 说明 |
|---|---|---|---|
Timestamp |
UnixNano | 是 | 日志事件发生的精确时间戳,纳秒精度。 |
ObservedTimestamp |
UnixNano | 否 | 日志被观测到的时间,通常与 Timestamp 相同,用于处理异步日志场景。 |
SeverityNumber |
int | 是 | 严重程度的数字表示(1=TRACE, 5=DEBUG, 9=INFO, 13=WARN, 17=ERROR, 21=FATAL)。 |
SeverityText |
string | 否 | 严重程度的文本表示(如 "ERROR"),用于兼容传统日志框架。 |
Body |
AnyValue | 是 | 日志正文内容,可以是字符串、结构化对象或原始字节。 |
TraceId |
byte[16] | 否 | 关联的 Trace ID,用于将日志绑定到分布式调用链。 |
SpanId |
byte[8] | 否 | 关联的 Span ID,标识日志在调用链中的具体位置。 |
TraceFlags |
byte | 否 | Trace 的采样标记,用于指示该 Trace 是否被采样。 |
Resource |
Resource | 是 | 标识日志来源的资源信息(服务名、主机名、容器ID等)。 |
InstrumentationScope |
InstrumentationScope | 是 | 标识产生日志的库或框架(如 "my-logger", "otel-spring-boot")。 |
Attributes |
map[string]AnyValue | 否 | 可扩展的键值对集合,用于携带任意上下文信息(如用户ID、请求路径、错误码)。 |
4.5 日志与 Trace 关联的代码示例
以下示例展示了如何在 Java 中使用 OTel API 创建一条关联到当前 Trace 的日志记录:
// 获取当前 Span
Span currentSpan = Span.current();
String traceId = currentSpan.getSpanContext().getTraceId();
String spanId = currentSpan.getSpanContext().getSpanId();
// 创建 LogRecord
LogRecordBuilder logBuilder = LoggerProvider.get("my-service")
.loggerBuilder("my-logger")
.build()
.logRecordBuilder();
logBuilder
.setTimestamp(Instant.now())
.setSeverity(Severity.ERROR)
.setBody("用户下单失败:库存不足")
.setAttribute("user.id", "user_12345")
.setAttribute("order.id", "order_67890")
.setAttribute("error.code", "INSUFFICIENT_STOCK")
// 关联 Trace
.setTraceId(traceId)
.setSpanId(spanId)
.emit();
注意: 并非所有日志都需要关联 Trace。对于系统级日志(如服务器启动、配置加载),它们不属于任何请求上下文,此时
TraceId 和 SpanId 应留空。仅在业务请求处理过程中产生的日志才需要关联 Trace。
4.6 日志数据模型的核心价值
- 结构化: 强制使用标准字段,消除日志格式碎片化,便于自动化处理和分析。
- 可关联: 通过 TraceId/SpanId 与调用链关联,通过 Resource 与基础设施关联,打破数据孤岛。
- 可扩展: Attributes 机制允许携带任意业务上下文,满足不同场景的定制需求。
- 标准化: 无论使用哪种编程语言或日志框架,产生的 LogRecord 结构一致,后端可以统一处理。