Skip to content
可观测性联动

可观测性联动

排障很少是"打开一个系统看到答案"。真实过程更像接力:监控告警说 checkout 服务错误率涨了,你点开曲线上的一个点,跳到一条具体的 trace,看到 340ms 里 300ms 卡在某个 SQL span 上,再从这条 span 跳到同一个服务同一时刻的日志,读到那条慢查询的具体参数——修完之后回到曲线确认回落。指标、链路、日志三个信号(可观测性"三支柱")各自都不完整,把它们缝起来的那根线,是一个 32 位十六进制的 trace_id。

本章就是讲这些"通道"怎么修。口径:Jaeger v2.22.0(2026-10-06)、OTel Collector spanmetrics connector(v0.163.0)、Grafana v13.2 数据源文档(2026-08-11 复核)。Prometheus/Loki/Grafana 自身的深入用法不在本章范围,只讲与 Jaeger 衔接的配置面和容易断掉的环节。

排障动线:三种信号怎么互相找到对方

先明确各支柱单独回答什么问题:

  • 指标(Metrics):出事了吗、哪个服务哪个接口、恶化到什么程度。便宜、可聚合、适合告警和趋势,但它把千万次请求压成一个数字,单请求细节全丢了。
  • 链路(Traces):这一次请求的时间花在调用链的哪一跳、哪一跳报错。按请求记录、细节丰富,但太贵,通常要采样,而且不适合直接做告警。
  • 日志(Logs):这个 span 对应的那行代码里,到底发生了什么(异常栈、参数、上游返回值)。细节最全,但跨服务对时间戳拼线索效率最低。

三者的关系是"逐级下钻 + 原路返回":

告警响起(Prometheus:错误率/延迟越过阈值,已知 service+operation+时间窗)
   |  从数据点取 exemplar: trace_id=<32位hex>
   v
Jaeger: 按 trace_id 展开调用树,定位最慢/出错的 span
   |  Trace to logs:按 span 的 tag 与时间窗过滤日志
   v
Loki: 日志行里带着同一条 trace_id,顺藤摸到上下文相邻日志
   |  修复、发布
   v
回到指标曲线确认回落;留存 trace_id 便于复盘

反方向(从日志/指标跳进 Jaeger)靠的机制各有名字,先建立地图,后面逐一落地:

跳转方向 机制 落地位置
指标 → 链路 exemplar(数据点上附原始 trace_id) spanmetrics connector 开启 exemplars + Prometheus 开 exemplar-storage
日志 → 链路 derived fields(正则从日志行提取生成跳转链接) Grafana Loki 数据源 derivedFields
链路 → 日志 Trace to logs(按 tag 匹配日志标签 + 时间平移) Grafana Jaeger 数据源配置
链路 → 指标 Trace to metrics(拼接 PromQL 跳转) Grafana Jaeger 数据源配置
链路 → 指标(聚合) SPM:从 span 派生 RED 指标 spanmetrics connector 或 ES/OS 直查,见下文

贯穿这一切的 ID 只有一种:W3C/OTel 格式的 trace_id,128-bit、32 位小写十六进制(格式见 01-核心概念.md,埋点侧怎么拿到它见 05-Go客户端埋点.md)。exemplar 里带的是它,日志行里打的是它,Grafana 跳 Jaeger 的 URL 参数还是它。任何一环格式或字段名对不上,通道就断。

Jaeger 在联动里的边界

OTLP 协议(4317/4318)本身同时承载 traces/metrics/logs 三种信号,但 Jaeger 的定位始终是 tracing 后端:落到 Jaeger 存储里的只有 spans,它不存储、也不查询指标和日志。所以别指望"把 metrics 也发到 Jaeger 就全有了"——Jaeger 与另外两支柱的接口只有两个:

  1. 从 trace 派生指标(SPM,Monitor 页签 + 供告警用的 RED 时间序列),本质是"traces 是另一种形态的指标数据源";
  2. 成为跳转的终点/起点:Grafana 里作为 trace 数据源被日志/指标面板链过去,以及 Trace to logs/metrics 链出去。

因此"联动工程"的配置分摊在两边:Jaeger/Collector 侧负责把 trace_id 和由它派生的指标送进 Prometheus/Grafana 能看到的地方;Prometheus/Loki/Grafana 侧负责认这串 ID(exemplar 存储、derived fields、数据源关联)。Jaeger 自身的自监控指标(/metrics,8888 端口)只在本章排查环节用到一句,完整口径见 09-性能与运维.md。

SPM:从 trace 派生 RED 指标

Jaeger v2 的 SPM(Service Performance Monitoring)替代了 v1 时代独立部署的组件思路,由官方文档给出两条路线,产出的都是按 service+operation 聚合的 RED 指标(Rate/Errors/Duration,P50/P75/P95),驱动 UI 的 Monitor 页签,也天然适合接 Prometheus 告警。

指标的形状

按 v2.22 文档口径,SPM 关注两个指标名(可带命名空间前缀 [namespace_]):

  • calls_total:counter,span 总数。错误与否由标签区分:status_code="STATUS_CODE_ERROR" 的序列即错误数;
  • duration_[units]:histogram,span 时长直方图。落到 Prometheus 里会展开成 duration_milliseconds_count / _sum / _bucket 三族时间序列,_bucket 按 le 分桶,默认 17 个桶。

注意标签名的两次变换:OTel 侧维度叫 status.code、service.name(点号),经 Prometheus 导出惯例清洗后变成 status_code、service_name(下划线)。v2.22 文档故障排查一节给出的真实 Prometheus 序列名带 connector 默认命名空间,形如 traces_span_metrics_calls_total 和 traces_span_metrics_duration_milliseconds_bucket,Monitor 页签对 Prometheus 发的查询也长这样:

sum(rate(traces_span_metrics_calls_total{service_name =~ "redis",
    span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name, span_name)

写自己的告警表达式前,以自己采集端的 /metrics 实际输出为准,不要照抄任何年代教程猜名字;名字对不上时用后文的 normalize_calls/normalize_duration 或显式 namespace 对齐。

路线一:spanmetrics connector + Prometheus 后端(可告警的路线)

链路:span 流经 Collector 时,一部分进 Jaeger 存储,另一部分被 spanmetrics connector 聚合成指标,经 prometheus exporter 开一个采集端点,外部 Prometheus 来 scrape;Jaeger Query 再连回这个 Prometheus 供 UI 查询。v2 的 jaeger 二进制本身就是 OTel Collector 定制发行版(见 02-v2架构与组件.md),所以 connector 可以直接配在 jaeger 进程里,也可以放在独立的 Collector 上——官方 config-spm 示例就是把 connector 写进 jaeger 的配置,演示用,生产拆分更常见。

Collector/Jaeger 配置骨架(按 v2.22 文档整理;connector 组件名见下文"改名"一节,旧名 spanmetrics 仍可用):

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"     # 给 Prometheus 采集用的端点,别与 8888 自监控混淆

connectors:
  span_metrics:
    exemplars:
      enabled: true              # 默认 false,想要指标到链路的一跳就打开它

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter, span_metrics]
    metrics/span_metrics:
      receivers: [span_metrics]
      exporters: [prometheus]

Jaeger 侧声明指标后端并接进查询扩展:

extensions:
  jaeger_storage:
    backends:
      some_trace_storage:        # trace 存储,配置见 04-存储后端.md
        ...
    metric_backends:
      some_metrics_storage:
        prometheus:
          endpoint: http://prometheus:9090
          normalize_calls: true      # 让 Jaeger 按 calls_total 形态找指标
          normalize_duration: true   # 按 duration_milliseconds_bucket 形态找
  jaeger_query:
    storage:
      traces: some_trace_storage
      metrics: some_metrics_storage

normalize_* 这对开关专治命名错位:v2.22 文档记录过真实事故——OTel 侧导出的计数器带 _total 后缀、直方图带单位后缀(duration_milliseconds_bucket),而 Jaeger 默认找 calls/duration_bucket,名字对不上时 Monitor 有数据源却没图,打开 normalize 即可。

Prometheus 侧常规 scrape:

scrape_configs:
  - job_name: spanmetrics
    scrape_interval: 15s
    static_configs:
      - targets: ["jaeger:8889"]

UI 侧打开 Monitor 页签:Jaeger UI 配置里设置 monitor.menuEnabled=true(v2 的挂载方式见 03-部署与Kubernetes.md)。

{
  "monitor": {
    "menuEnabled": true
  }
}

用 SPM 指标写告警

Monitor 页签是给人在面板里点着玩的;进告警系统的是同一批 Prometheus 时间序列。两个最常用的表达式:

# checkout 服务 server span 的 5 分钟错误率
sum(rate(traces_span_metrics_calls_total{service_name="checkout",
      span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"}[5m]))
/
sum(rate(traces_span_metrics_calls_total{service_name="checkout",
      span_kind="SPAN_KIND_SERVER"}[5m]))

# 按接口聚合的 P95 延迟,单位毫秒(默认单位,阈值按 ms 写)
histogram_quantile(0.95,
  sum by (le, service_name, span_name) (
    rate(traces_span_metrics_duration_milliseconds_bucket[5m])))

告警路由、静默、SLO 化是告警系统的话题,本章不展开;要提前知道的只有下一节的基数账。

注意 时间序列基数按 v2.22 文档公式估算:status_code 数 × span_kind 数 × (1 + 直方图桶数) × 操作数。默认配置下典型值为 2 × 2 × 18 = 72 条序列每操作(上限按 3 种 status、6 种 kind 算,324×操作数)。一个 30 服务、平均 20 个接口的系统就是约 4.3 万条序列——直方图桶数改大、dimensions 加宽都会线性放大这个数。另外 SPM 查询层不支持自定义维度(多余的维度会被聚合掉),自定义桶也需计入公式。

路线二:Elasticsearch/OpenSearch 直查(省组件的路线)

如果 trace 存储本身就是 ES/OS,Jaeger Query 可以在查询时直接对它聚合出 RED 指标,不需要 connector、不需要独立 metrics pipeline、不需要额外指标库:

extensions:
  jaeger_storage:
    backends:
      elasticsearch_trace_storage: &es_config
        elasticsearch:
          endpoint: http://elasticsearch:9200
          ...
    metric_backends:
      elasticsearch_trace_storage: *es_config   # 指标后端指回同一个 ES 存储
  jaeger_query:
    storage:
      traces: elasticsearch_trace_storage
      metrics: elasticsearch_trace_storage      # 同一个后端

OpenSearch 同构(官方另有 config-spm-opensearch 示例,ClickHouse 直查为实验性支持)。同样只需 monitor.menuEnabled=true。

取舍很直接:直查路线组件最少、没有独立存储和采集链的运维负担,代价是聚合查询压力全部落在 trace 存储上,且指标不落到 Prometheus,等于没有这条告警通道——想从 SPM 数据报警,要么用路线一,要么在 ES 侧自建 watcher,后者已脱离 Jaeger 联动范畴。中小规模 ES 存储用直查,有成熟 Prometheus 告警体系用路线一,是多数团队的选择。

Monitor 页签的数据也能程序化取:v2.22 提供 jaeger.api_v2.metrics.MetricsQueryService gRPC 端点与 UI 内部使用的 HTTP JSON 接口,查询 API 全貌见 07-查询UI与API.md。

SPM 不通时的自查

Monitor 空白、报错,按 v2.22 文档的排查顺序:

  1. Jaeger 读指标是否成功:开内部 telemetry(service.telemetry.metrics,0.0.0.0:8888),curl http://jaeger:8888/metrics | grep jaeger_metricstore。关注 jaeger_metricstore_requests_total,operation 标签有 get_call_rates/get_error_rates/get_latencies/get_min_step_duration,正常时 result="ok" 递增、result="err" 静止。注意端口账:8888 是 Jaeger 自监控,8889 是 connector 导出给 Prometheus 采集的端点,两者必须错开。
  2. 指标真的进了 Prometheus 没有:直接在 Prometheus 查 traces_span_metrics_calls_total、traces_span_metrics_duration_milliseconds_bucket 这类序列是否随流量增长。有数据但 UI 空白,多半是命名错位,把 telemetry.logs.level: debug 打开,看 Jaeger 实际发出的 PromQL 里用的指标名,再决定 normalize 开关。
  3. 有错误 span 却没有错误指标:检查指标序列上有没有 status.code 标签——Jaeger 靠它判定错误。若没有,检查 connector 配置里是不是写了 exclude_dimensions: ['status.code'] 把它排除了。
  4. 某 service/operation 在 Monitor 消失但搜索下拉里有:SPM 查询默认只取 server span kind(避免 client/server 双向重复计数)。该服务的埋点没设 span kind 或全是 client/unspecified,就查不到——这是埋点质量问题,回 05-Go客户端埋点.md 修。
  5. Prometheus 返回 403:指标后端配置支持 token_file_path(bearer token 文件)与 token_override_from_context。

spanmetrics connector 深读

路线一的引擎是这个 connector,几个直接影响联动的配置事实(均按 v0.163.0 模块 README 核实):

组件改名。connector 的组件类型已从 spanmetrics 更名为 span_metrics(snake_case,与 Collector 命名规范对齐);旧名 spanmetrics 仍可用但已标记 deprecated,将在未来版本移除。新配置一律写 span_metrics;Jaeger 官方 SPM 文档的示例目前仍写作 spanmetrics,两者解析到同一组件。

默认指标与维度。生成的指标名默认带命名空间 traces.span.metrics,即 traces.span.metrics.calls(counter)、traces.span.metrics.duration(histogram)、开启后 traces.span.metrics.events。所有序列至少带这些公共维度:service.name、span.name、span.kind、status.code、collector.instance.id。可通过 dimensions 追加维度,每个条目是 name(可带 default,属性缺失时用默认值,不给 default 则属性缺失时整条维度省略)或 glob 模式(. 为分隔符,k8s.*.name 匹配单级、db.**.name 匹配任意多级)。calls_dimensions/histogram.dimensions 可分别只给 calls 或 duration 加维度。

桶与单位。histogram 支持 explicit(默认 16 个边界:2ms 4ms 6ms 8ms 10ms 50ms 100ms 200ms 400ms 800ms 1s 1400ms 2s 5s 10s 15s)与 exponential(max_size 默认 160)两种。单位 histogram.unit 默认 ms。

注意 feature gate connector.spanmetrics.useSecondAsDefaultMetricsUnit 打开后 duration 指标默认单位改为秒,官方已预告将以秒为默认值(对齐 OTel 语义约定)。你的告警阈值(P95 = 800 这种毫秒数)在单位切换后会整体错一千倍——升级 Collector 组件时把这件事列进检查项。

exemplars。exemplars.enabled 默认 false;打开后每个数据点最多附 max_per_data_point(默认 5)条原始 span 作为 exemplar,且 exemplar 只保留一个 flush 周期(metrics_flush_interval 默认 60s)。这两个参数直接决定"从指标点跳到 trace"的体验,详见下文 exemplar 通路一节。

temporality 与基数。aggregation_temporality 默认 AGGREGATION_TEMPORALITY_CUMULATIVE,可改 ..._DELTA,两种都支持进 Prometheus。aggregation_cardinality_limit 默认 0(不限),超限后新维度组合会被并入 otel.metric.overflow="true" 序列。dimensions_cache_size 已废弃,同样用 cardinality limit 管理。系列过期用 series_expiration/metrics_expiration 控制,低峰期可回收长期不再出现的维度组合。

collector.instance.id 与单写者原则。默认维度里的 collector.instance.id 是每个 Collector 实例的 UUID,目的是多副本部署时不违反 Single Writer 原则(各自累计各自的序列)。副作用是同样的 service+operation 会按实例分裂成多条序列,PromQL 聚合时用 sum by (...) 抹掉它即可。若确认单实例或接受重启归零,可用 exclude_dimensions: ['collector.instance.id'] 或 feature gate --feature-gates=-connector.spanmetrics.includeCollectorInstanceID 去掉。

一份把常用键排齐的配置(可直接改成生产用,键名均以 v0.163.0 README 为准):

connectors:
  span_metrics:
    namespace: traces.span.metrics          # 默认值;改短它会改变 Prometheus 侧序列名前缀
    metrics_flush_interval: 15s             # 默认 60s;越短 exemplar 存活窗口越短但更实时
    aggregation_temporality: AGGREGATION_TEMPORALITY_CUMULATIVE
    histogram:
      unit: ms                              # 注意上文"默认将改为 s"的预警
      explicit:
        buckets: [2ms, 4ms, 6ms, 8ms, 10ms, 50ms, 100ms, 200ms, 400ms, 800ms, 1s, 1400ms, 2s, 5s, 10s, 15s]
    dimensions:
      - name: http.method
        default: GET
      - name: http.status_code
      - glob: "k8s.*.name"
    exclude_dimensions: ['collector.instance.id']
    aggregation_cardinality_limit: 100000
    series_expiration: 30m
    exemplars:
      enabled: true
      max_per_data_point: 5
    events:
      enabled: true
      dimensions:
        - name: exception.type

另有一条历史线索可解释大量旧教程:span_metrics 的前身是 span-to-metrics processor,connector 化时做了破坏性改名——维度 operation 改为 span.name、histogram 的 latency 改为 duration、生成名不再带 _total、不再做 Prometheus 式标签清洗(_total/单位后缀是之后 Prometheus exporter 导出时按惯例补的)。看到 latency_bucket、traces_spanmetrics_calls_total 这类名字,基本可断定素材来自 connector 化之前,先对照 2.22 文档重校再抄。

日志关联:让日志带上 trace_id

三个方向里最常用也最容易断的是日志通道:日志里没有 trace_id(或有但注入不上),后面一切跳转都是空谈。

在 Go 里注入:slog 的 handler 包装

做法是包装一层 slog.Handler,在 Handle 里从 ctx 取 span context,有 trace 就把 trace_id/span_id 追加进日志属性。这样业务代码不用关心追踪系统,没接 tracing 时日志照常输出:

package main

import (
	"context"
	"log/slog"
	"os"

	"go.opentelemetry.io/otel/trace"
)

// TraceHandler 包装任意 slog.Handler:凡 ctx 中带有效 span 的记录,
// 自动追加 trace_id / span_id 两个属性。
type TraceHandler struct {
	slog.Handler
}

func NewTraceHandler(h slog.Handler) *TraceHandler {
	return &TraceHandler{Handler: h}
}

func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {
	if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() {
		r.AddAttrs(
			slog.String("trace_id", sc.TraceID().String()),
			slog.String("span_id", sc.SpanID().String()),
		)
	}
	return h.Handler.Handle(ctx, r)
}

func (h *TraceHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return &TraceHandler{Handler: h.Handler.WithAttrs(attrs)}
}

func (h *TraceHandler) WithGroup(name string) slog.Handler {
	return &TraceHandler{Handler: h.Handler.WithGroup(name)}
}

func main() {
	log := slog.New(NewTraceHandler(slog.NewJSONHandler(os.Stdout, nil)))

	// 真实服务里,ctx 应来自埋点中间件之后携带 span 的上下文(见 05-Go客户端埋点.md)。
	// 用 context.Background() 演示"无 span"路径:不会注入 trace_id,日志正常输出。
	log.InfoContext(context.Background(), "order created", slog.String("order_id", "A-1024"))
}

ctx 里有 span 时,同一行输出:

{"time":"2026-10-20T14:32:07.112+08:00","level":"INFO","msg":"order created","order_id":"A-1024","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"}

注意 两个高频失血点。其一,必须用 logger.InfoContext/WarnContext/... 这一族带 ctx 的方法——裸的 logger.Info() 传给 handler 的是空 ctx,永远注入不上;其二,后台 goroutine 里若直接 context.Background() 起 ctx 也注入不上,想把请求链路的 span 带过去,用 context.WithoutCancel(ctx)(Go 1.21+)保留值、剥离取消信号,或干脆为异步任务新建 span 再手动记录父 trace_id。这两条坑在 10-常见陷阱与最佳实践.md 亦有汇总。

Loki 侧的字段设计:trace_id 进日志行,不进标签

Loki 只索引标签(labels),标签设计原则是"少而可枚举"。trace_id 每行都不同,把它做成标签等于给 Loki 制造无限基数,是明确的反模式。正确姿势:采集时标签只放 service(或 job)、namespace、pod 这类低基数维度,trace_id 留在日志正文里,由 Grafana 查询时用 derived fields 正则提取。这与 OTel 语义约定、Jaeger span 的 service.name 属性形成对应,也为下文 Trace to logs 的标签匹配打下基础。

Grafana derived fields:日志一键跳 trace

Loki 数据源的 Derived fields 支持从日志行(Regex in log line)或标签(Label)提取字段生成链接:标签模式可用 trace[_]?id 这类正则匹配 traceid/trace_id 变体;正文模式写捕获组,Grafana 取第一个捕获组作为字段值。链接可以走内部(指向本 Grafana 的 trace 数据源)或外部(直接拼 Jaeger UI 的 /trace/<id> URL,格式见 07-查询UI与API.md)。

provisioning YAML 形态(官方文档示例的同构写法,注意 YAML 里 $ 要写成 $$ 转义,且 Jaeger 数据源的 uid 必须与 datasourceUid 一致):

apiVersion: 1
datasources:
  - name: Loki
    type: loki
    uid: loki-prod
    access: proxy
    url: http://loki:3100
    jsonData:
      derivedFields:
        - name: TraceID
          datasourceUid: jaeger-prod        # 对上文的 Jaeger 数据源 uid
          matcherRegex: "trace_id=([0-9a-f]{32})"   # 正文形态,等价于 trace_id: 前缀也行
          url: '$${__value.raw}'            # 对 trace 数据源而言,这就是 trace 查询参数
          urlDisplayLabel: 'View Trace'
  - name: Jaeger
    type: jaeger
    uid: jaeger-prod
    access: proxy
    url: http://jaeger-query:16686

要点与限制:每条日志行只能出一个 derived field 值(多个捕获组不能拼成一个字段);复杂正则在海量日志上有浏览器性能开销,够用即可;内部链接只是"把提取值交给目标数据源",目标里没有这条 trace 时,链接照常渲染、点开却是空结果。

最后一句值得单独说,因为它和采样直接相关:head 采样下未命中的 trace 根本没进 Jaeger(见 06-采样.md),日志里的 trace_id 是真的,跳转目标却不存在,“跳空"不是通道坏了,而是采样预算的体现。两种管理预期的做法:对错误率敏感的关键服务用 tail-based 采样保错误全采;或在告警/日志文案上接受"部分链接会空"的现实,别为了链接必中把采样率拉满。

Grafana 的 Jaeger 数据源:trace 向外跳

Jaeger 数据源在 Grafana OSS/Enterprise 中预装(Grafana v13.2 起作为独立插件随 catalog 自动更新)。能力矩阵很克制:只支持 Traces,Metrics/Logs/Alerting/Annotations 全为 No——它的全部价值在"关联”:把 trace 视图里的信息变成通往日志和指标面板的链接。

Trace to logs:从 span 跳到 Loki

配置两种模式。简单模式填目标日志数据源 + 标签映射:Tags 里的键必须存在于 span 的属性或资源中,匹配到的值会拼成日志查询的标签过滤;默认尝试的标签是 cluster、hostname、namespace、pod、service.name、service.namespace。目标数据源不允许点号标签时要做重映射,例如 span 属性 http.status 映到 Loki 的 http_status——这正是"span tag 名与日志标签名对齐"这件事的具象。Span start/end time shift 用 5s/1m/3h(可为负)平移或放宽查询时间窗,应对日志时间与 span 时间不完全一致;可勾选 Filter by trace ID / Filter by span ID 把 ID 也拼进查询。

自定义查询模式用模板变量拼任意 LogQL:${__tags} 插入已映射标签,${__span.traceId}、${__span.tags["http.status"]}(带点号的标签用下标写法)、${__trace.duration} 等变量按需组合。规则:所有变量都解析出非空值,链接才会出现——引用了 span 上并不存在的 tag,链接就整个消失,这是"我的跳转按钮怎么没了"的第一排查点。

provisioning 形态(jsonData 节选,$$ 转义同理):

jsonData:
  tracesToLogsV2:
    datasourceUid: loki-prod
    spanStartTimeShift: '-5m'
    spanEndTimeShift: '5m'
    tags:
      - key: 'service.name'
        value: 'service'
      - key: 'k8s.pod.name'
        value: 'pod'
    filterByTraceID: true
    filterBySpanID: false
    customQuery: false

Trace to metrics:从 span 跳到指标面板

选目标指标数据源,Tags 把 span 属性名映射为指标标签名(如 k8s.pod 映到 pod),每条关联查询 = Link Label + Query,查询里用 $__tags 展开成标签选择器。官方文档的例子:requests_total{$__tags} 配上映射后实际发出 requests_total{pod="nginx-554b9", cluster="us-east-1"}。给 SPM 用户最实用的两条是"看这个 service+operation 的 P95 曲线"和"看错误率曲线"——查询里的指标名/标签名就按本章前文的 Prometheus 口径写。

jsonData:
  tracesToMetrics:
    datasourceUid: prom
    spanStartTimeShift: '-2m'
    spanEndTimeShift: '2m'
    tags:
      - key: 'service.name'
        value: 'service'
    queries:
      - name: 'Call rate'
        query: 'sum(rate(traces_span_metrics_calls_total{$$__tags}[5m]))'

指标 → trace:exemplar 通路

反向通道是 Grafana 文档明说的组合:“从日志用 derived fields、从指标用 exemplars 链回 Jaeger trace”。三处配置缺一不可:

  1. connector 侧:exemplars.enabled: true(默认关,这是最常见的"曲线上没有可点的小圆点"原因);
  2. Prometheus 侧:启动参数 --enable-feature=exemplar-storage。不开,scrape 到的 exemplar 直接丢弃;开了,exemplar 存在内存固定大小环形缓冲里(一条带 trace_id 的 exemplar 约 100 字节),缓冲容量由配置文件 storage.exemplars 块控制,并随 WAL 做本地持久化;
  3. Grafana 侧:面板/查询编辑里启用 exemplar 并把 trace 数据源指到 Jaeger。

时序上要有预期:connector 的 exemplar 只活一个 flush 周期,Prometheus 侧是环形缓冲、新数据挤掉旧数据——exemplar 是"正在响的告警"这种现场的工作方式,隔天复盘十有八九已经取不到,那时靠的是写进日志行的 trace_id。

端到端演练:从告警到修复的一条完整动线

用一个 30 分钟能走完的剧本把本章串起来。前提:三条通道都已配好(SPM 路线一的指标进 Prometheus、日志带 trace_id、Grafana 数据源双向关联)。

  1. 告警响起:Alertmanager 通知错误率表达式越过阈值,标注里带着 service_name="checkout"、span_name="POST /order"、时间窗。这些标签来自 SPM 指标,天然可读。
  2. 取 trace_id:在 Grafana 对应曲线面板上点开告警时间窗内的 exemplar 点,拿到 trace_id=4bf92f3577b34da6a3ce929d0e0e4736。若 exemplar 已被冲掉(超出一个 flush 周期/缓冲),退化方案:Jaeger 搜索页按 service+operation+时间窗过滤(见 07-查询UI与API.md)。
  3. 看链路:trace 展开,发现 POST /order 的 340ms 里一个 SQL SELECT 子 span 占 300ms,再往下是某依赖服务的 client span 超时重试。记下出错 span 的 service 与时间戳。
  4. 跳日志:在该 span 上触发 Trace to logs 链接(tag 匹配 + 时间窗已含前后 5 分钟),落到 Loki 上下文;顺日志行的 trace_id= 用 derived field 反查同请求在其他服务的全部日志,确认是缓存失效引发的雪崩。
  5. 修复并验证:发布修复;回到指标面板看错误率与 P95 回落;把这条 trace 的 ID 记进事故单——trace 存储的保留期内随时能原样调出(存储与过期见 04-存储后端.md)。

三支柱 ID 一致性检查清单——任何一条不满足,上面剧本就会在对应环节断掉:

  • 日志里的 trace_id 是 32 位小写 hex,与 Jaeger URL/查询参数完全同值(注入用带 span 的 ctx,调用用 InfoContext 族);
  • span 资源属性 service.name 与日志标签(如 Loki 的 service)、指标标签(service_name)三者语义一致,点号/下划线差异有明确的映射表;
  • 指标序列名按你部署的实际 connector 命名与 Prometheus 抓取形态核对,告警表达式和 Jaeger SPM 的 normalize_* 用同一套结论;
  • exemplars 三处开关(connector / --enable-feature=exemplar-storage / Grafana 面板)全开,且接受"只活一个 flush 周期"的时效;
  • 采样策略与跳转预期匹配:低 head 采样率意味着日志→trace 的链接会按比例跳空,关键服务考虑 tail 采样或全采短保留;
  • duration 指标单位(ms 或即将改为 s)与告警阈值的量纲对过账。

实战建议

  • 联动的最小可用集是两件事:日志行里稳定注入 trace_id,SPM 走路线一(指标进 Prometheus)。前者解决 90% 的"从日志找链路",后者解决"从告警进现场"。ES/OS 直查路线留作 UI 便利功能,不承担告警职责。
  • 命名口径每半年查一次岗:span_metrics 改名、_total/单位后缀、traces_spanmetrics_* 旧名 processor 素材,都说明这块是变动密集区。写查询前看一眼自己 /metrics 端点的真实输出,升级 Collector 组件时重读 connector README 的 deprecation 与 feature gate 段落。
  • 单位从 ms 改 s 是这颗延迟引信:它同时打穿直方图桶义、告警阈值、SLO 表达式。改桶、改单位都要按 status × kind × (1+桶数) × 操作数 重新过基数账,基数失控在 Prometheus 侧的症状通常是内存曲线而非 UI 报错。
  • 标签对齐是跨团队约定:埋点规范里写明"哪些 span 属性会被当作日志匹配键"(通常就是 service.name/pod/cluster),日志采集规范里写明"Loki 标签白名单、trace_id 只进正文"。两边各自随机演化,Trace to logs 就会悄悄失效——它失效的形式是链接不出现,而不是报错。
  • exemplar 用于现场,不用于取证:它的保留只有一个 flush 周期外加 Prometheus 环形缓冲,复盘永远以日志行和已入库 trace 为准。
  • 在告警文案里直接嵌 Jaeger 深链(/search?service=... 或 /trace/<id>)能把"看到告警"到"进入现场"压到一次点击;URL 参数形态见 07-查询UI与API.md,深链依赖保留期,过期 trace 跳过去同样是空。
  • 演练常态化:每个季度走一遍本章剧本(触发一次测试错误→从告警走到日志),三个跳转方向各验一次。通道坏掉最常见的原因是配置漂移(数据源 uid 改名、标签重映射丢失),这类故障只有在演练时才会现形,而不是在真正的凌晨告警里。