Exemplar 与指标-链路-日志关联:从指标异常跳转到具体 trace

深入讲解 Exemplar 与指标、链路、日志三者的关联工程:Exemplar 的 OpenMetrics 规范与存储、Prometheus 与客户端埋点、从直方图指标一键跳转到具体 trace、TraceID 与日志的双向关联、Grafana 关联配置,以及常见避坑与最佳实践清单。

告警响了,指标告诉你「P99 延迟从 200ms 涨到 2s」,但到底是哪几个请求慢、慢在哪个环节,指标本身永远回答不了。Exemplar 就是为了打通这道墙而生的:它把个别 trace 的 ID 作为「样本」挂在指标的时间序列上,让你能从一条聚合曲线直接跳到一次真实的请求链路。本文从 Exemplar 的规范与存储,讲到客户端埋点、Prometheus 采集、Grafana 关联配置与 TraceID/日志的双向打通。

关键概念:Exemplar=附着在指标样本上的「示例引用」,通常是 trace_id。它让聚合指标保留「指向个体」的能力——指标看趋势,Exemplar 带你找到具体的那个慢请求。



1. 三支柱割裂与关联的必要性

1.1 割裂的典型症状

现象一:看指标发现延迟飙升 → 切到链路系统 → 不知道查哪个时间点
现象二:看到一条慢 trace → 想找"同一时刻还有多少条慢" → 指标里查不到
现象三:日志里有错误 → 想关联到指标趋势 → 只能靠人肉对时间

根因:三套系统各自为政,缺少"可跳转的关联键"

1.2 关联的三条路径

指标 → 链路:Exemplar(指标样本携带 trace_id)
链路 → 日志:TraceID 注入日志(log-trace correlation)
日志 → 链路:日志中提取 TraceID 反查链路
闭环目标:任一入口都能在 3 次点击内到达根因

1.3 关联键的选择

关联方向关联键实现方式
指标→链路trace_idExemplar
链路→日志trace_id + span_id日志注入上下文
日志→指标service + 时间统一标签与时间对齐
指标→日志service + 时间数据链接
核心原则:trace_id 是唯一贯穿三者的"主键",务必全程透传

2. Exemplar 原理与 OpenMetrics 规范

2.1 Exemplar 是什么

定义:附着在某个指标样本上的、带时间戳的额外信息
典型形态:一个直方图桶的样本 + 一条 trace_id
价值:聚合指标不再"只见森林",能指向具体的"一棵树"

适用指标类型:
  Histogram:最常用,每个桶样本可带 exemplar
  Counter:也可带,但语义较弱
  不适用:Gauge(无累积语义,关联意义小)

2.2 OpenMetrics 文本格式

# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.5"} 24054 # {trace_id="a1b2c3"} 0.42 1696233600.123
http_request_duration_seconds_bucket{le="1.0"} 24110 # {trace_id="d4e5f6"} 0.91 1696233601.456
http_request_duration_seconds_sum 53423.0
http_request_duration_seconds_count 24110
语法要点:
  # {key="value"} value timestamp   → exemplar 附着在样本行尾
  trace_id 是约定俗成的 key,也可以是 span_id / request_id
  只有 OpenMetrics 格式支持,旧版 Prometheus 文本格式不支持

2.3 存储与生命周期

Prometheus:exemplar 单独存储(exemplar storage),默认保留 5 分钟
  内存环形缓冲区,容量由 --storage.exemplars 控制
  不会长期保留——exemplar 是"近期样本",不是历史档案

远端写入:需开启 send_exemplars,且远端(Thanos/Mimir)需支持
Grafana:只展示 exemplar 存在的窗口,过期后自动消失

⚠️ 注意:exemplar 默认只保留几分钟,看到告警后再去翻几小时前的 exemplar 通常已经没了。要么调大保留,要么依赖长期存储。


3. Prometheus 与客户端埋点实践

3.1 服务端开启 exemplar

启动参数:
  --enable-feature=exemplar-storage

远端写入:
  remote_write:
    - url: http://mimir:9009/api/v1/push
      send_exemplars: true

3.2 Go 客户端埋点

import (
    "github.com/prometheus/client_golang/prometheus"
    "go.opentelemetry.io/otel/trace"
)

hist := prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request duration",
        Buckets: prometheus.DefBuckets,
    },
    []string{"route", "method", "status"},
)

// 在处理函数里把当前 span 的 trace_id 作为 exemplar 附上
func observe(ctx context.Context, h prometheus.Observer, dur float64) {
    sc := trace.SpanContextFromContext(ctx)
    if sc.IsValid() {
        h.(prometheus.ExemplarObserver).ObserveWithExemplar(
            dur,
            prometheus.Labels{"trace_id": sc.TraceID().String()},
        )
        return
    }
    h.Observe(dur)
}

3.3 OpenTelemetry 的自动关联

OTel Prometheus Exporter 支持自动把当前 span 的 trace_id 作为 exemplar
条件:指标记录时 context 中存在有效 span
配置:WithExemplarSetting / ExemplarFilter
好处:不用手写 ObserveWithExemplar,框架自动完成
注意:exemplar 数量按采样比控制,不要每个请求都记

3.4 采样策略

exemplar 不是越多越好:
  - 高流量服务每个请求都带 → 存储与传输压力大
  - 建议按固定比例(如 1%)或仅对慢请求/错误请求记录
  - 关注价值:慢请求和错误的 trace 最有诊断意义

4. 从指标跳转到 trace 的工程实现

4.1 跳转链路

Grafana 指标面板(直方图)
  → 鼠标悬停出现 exemplar 点(菱形)
  → 点击携带 trace_id
  → 通过数据链接跳转到 Tempo/Jaeger
  → 打开该 trace 详情

4.2 Grafana 数据链接配置

面板 Data links:
  Title: 查看 Trace
  URL:   http://tempo.observability:3200/trace/${__value.raw}
  # 或使用 TraceID 变量
  URL:   /explore?left={"datasource":"tempo","queries":[{"query":"${__value.raw}"}]}

要点:
  - 目标数据源需在 Grafana 中注册(Tempo / Jaeger)
  - 变量名与 exemplar 的 label key 一致(trace_id)

4.3 全链路闭环示例

1. SLO 面板显示 P99 超标(指标)
2. 悬停直方图 P99 桶 → 看到 exemplar 菱形点
3. 点击 → 跳到 Tempo,看到这条慢 trace
4. trace 中某 span 异常 → 点 span → 跳转该服务的日志
5. 日志中看到具体报错 → 定位根因

4.4 没有 Exemplar 时的替代方案

替代一:exemplar 不可用 → 用 trace 系统的"慢查询"入口
  按 service + 时间 + 耗时阈值直接搜 trace
替代二:指标与 trace 时间对齐,人工缩小窗口
替代三:在指标 label 中带 trace_id(高基数,不推荐)
结论:替代方案都更笨重,Exemplar 是成本最低的关联方式

5. Trace 与日志的双向关联

5.1 链路 → 日志:注入 TraceID

做法:日志框架在每条日志里自动带上 trace_id / span_id
Go 示例(slog + OTel):
  handler := slog.NewJSONHandler(os.Stdout, nil)
  logger := slog.New(handler)
  sc := trace.SpanContextFromContext(ctx)
  logger.Info("payment failed",
      "trace_id", sc.TraceID().String(),
      "span_id", sc.SpanID().String())

5.2 日志 → 链路:反查

做法:日志系统中提取 trace_id 字段,配置跳转链接
Grafana Loki:
  派生字段(derived field)正则提取 trace_id
  → 生成跳转到 Tempo 的链接
效果:在日志里点 trace_id 直接打开对应链路

5.3 结构化日志与语义约定

字段命名统一(OTel 语义约定):
  trace_id, span_id 固定小写下划线
  不要用 traceId / TraceID / trace 混用
好处:Loki、ES 等日志系统可用同一套提取规则

ℹ️ 核心:日志与链路的关联靠 TraceID 注入,链路与指标的关联靠 Exemplar。三者一旦串起来,排障路径就变成「看指标 → 点 exemplar → 看 trace → 点 trace_id → 看日志」。


6. Grafana 关联跳转与钻取

6.1 三种关联机制

1. Data links        面板内值 → 外部 URL
2. Exemplars         直方图点 → trace 系统
3. Correlations      数据源之间的关联配置(Grafana 9+)
   Correlations 可在 Loki/Metrics 间定义"用 trace_id 关联"

6.2 Correlations 配置

在 Grafana 数据源设置里添加 Correlation:
  Source: Loki(日志)
  Target: Tempo(链路)
  Label:  trace_id
  URL:    ${__value.raw}
效果:日志行中的 trace_id 自动变成可点击链接

6.3 统一 Explore 体验

Grafana Explore 支持 Split 视图:
  左:指标(Prometheus)
  右:链路(Tempo)
  同一时间窗口,点击 exemplar 双向联动
建议:把常用关联固化成 Dashboard 变量与链接,降低排障门槛

6.4 权限与网络

跳转依赖浏览器可达后端数据源:
  - Tempo/Jaeger 需对 Grafana 前端可达(或走代理)
  - 跨集群时注意网络策略与鉴权
  - 建议统一入口,避免多处配置漂移

7. 常见避坑

坑现象对策
未开 exemplar 存储直方图上没有菱形点加 –enable-feature=exemplar-storage
exemplar 过期告警后翻不到旧样本调大保留或依赖远端长期存储
全量记录 exemplar存储与带宽暴涨按比例或只对慢/错请求记录
trace_id 命名不一致跳转链接取不到值统一 trace_id 小写命名
远端未开 send_exemplars中心端看不到 exemplarremote_write 显式开启
日志未注入 TraceID无法从日志跳链路日志中间件统一注入上下文
跳转后端不可达点击后 404/超时检查网络策略与数据源注册
只做单向关联排障仍需人工切换三向打通,形成闭环

8. 最佳实践清单

□ 开启 exemplar 存储并配置远端写入 send_exemplars
□ 用 OpenTelemetry 自动注入 trace_id,减少手工埋点
□ exemplar 按比例采样,优先记录慢请求与错误请求
□ 统一关联键命名:trace_id / span_id 全小写下划线
□ 日志框架统一注入 trace_id,不依赖人工打印
□ 在 Grafana 配置 Data links 与 Correlations 打通三向跳转
□ 保证 Tempo/Jaeger 对 Grafana 前端可达
□ 排障 SOP 明确「指标→exemplar→trace→日志」路径
□ 定期验证关联链路(造一条慢请求走通全流程)
□ 关注 exemplar 保留窗口,关键故障窗口及时留存

一句话原则

指标看趋势、Exemplar 指个体、TraceID 串日志——
三向关联打通,排障从"猜"变成"点"。

小结

指标、链路、日志的关联不是锦上添花,而是把三套系统从并列变成一体的关键工程。核心只有两条纽带:Exemplar 把 trace_id 挂到指标样本上,实现「指标 → 链路」;TraceID 注入把链路上下文写进日志,实现「链路 ↔ 日志」。落地时注意:开启 exemplar 存储与远端转发、用 OpenTelemetry 自动埋点、统一关联键命名、在 Grafana 配好 Data links 与 Correlations。当三者真正打通,排障就变成「看趋势 → 点样本 → 看链路 → 点 ID → 看日志」的四步闭环,平均定位时间(MTTR)会显著下降。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 多集群可观测性联邦与聚合:联邦查询、数据分片与全局视图
  2. 可观测性即代码:仪表盘、告警规则与采集配置的 GitOps
  3. 消息队列可观测性:Kafka 与 RabbitMQ 的滞后、积压与端到端延迟