查询UI与API
Jaeger 官方 architecture 页给 query 角色的定义一句话:“query: 为查询和可视化 trace 提供 API 与用户界面”。这一章围绕这个端口(默认 HTTP 16686、gRPC 16685)展开三件事:UI 的各个页面各自"靠什么数据活着"(决定了生产环境哪些页面可能空、空了怪谁);UI 的全部配置项速查(jaeger_query.ui.config_file);以及查询侧 API 的分层与稳定性承诺(哪些能写进脚本、哪些是 UI 私产别碰)。最后把 Monitor 页背后的 SPM 单独立一节讲透——它是 UI 里唯一一个"配置没配好就整页空白"的功能,也是与采样口径(06-采样.md)耦合最深的地方。
v2 语境先钉一下(细节见 02-v2架构与组件.md):单一 jaeger 二进制,query 角色由 YAML 里的 jaeger_query 扩展声明而来;all-in-one 和独立 query 用的是同一个扩展,只是存储与管线编排不同。本章大部分内容对两种部署形态通用。
查询组件的形状:UI 只是壳
16686 端口上同时跑着三类东西:
| 内容 | 路径前缀 | 稳定性 |
|---|---|---|
| React 静态 UI | /、/search、/trace/{id} 等 |
面向人 |
| 内部 JSON API(UI 的后端私产) | /api/* |
internal:官方原文 “intentionally undocumented and subject to change” |
| api_v3 的 JSON/HTTP 映射 | /api/v3/* |
stable(OpenAPI 文件为契约) |
外加 jaeger_query 扩展同时监听 gRPC 16685(jaeger.api_v3.QueryService,程序化取数的推荐入口)。UI 本身是个纯前端应用(features 页:React 实现,v1.0 起做了大量渲染性能优化),它对上的就是 /api/*——理解这一点,就理解了"UI 能做的查询 = 存储后端能做的查询",也理解了为什么搜索表单的每个条件都能在存储文档模型上找到对应字段(下一节)。
搜索页:每个条件都会翻译成存储侧字段
搜索表单的六类控件——Service、Operation、Tags、Min/Max Duration、时间范围(Lookback 或自定义 Start/End)、Results to display——不是界面装饰,它们一一对应存储里 span 文档的字段。以 Elasticsearch 后端为参照实物(jaeger-span* 索引,一条 span 一个文档),字段模型是:traceID、serviceName、operationName、startTime(微秒 epoch)、duration(微秒)、tags([{key, type, value}] 数组)。对照关系:
| UI 控件 | 落到的字段 | 语义 |
|---|---|---|
| Service | serviceName |
精确匹配(下拉来自 GetServices) |
| Operation | operationName |
精确匹配(下拉来自 GetOperations,按所选服务过滤) |
Tags key=value |
tags 数组内嵌套条件 |
同时给多个条件时按 AND 合取;值按 type 区分字符串/布尔/数字等 |
| Start / End(或 Lookback 换算) | startTime 区间 |
微秒 epoch;Lookback 只是"当前时刻往回推 N" |
| Min / Max Duration | duration 区间 |
微秒;UI 输入会自动换算 |
| Results to display | 返回条数上限 | 受 search.maxLimit 封顶 |
| 直接粘 traceID | traceID |
走按 ID 取整条 trace 的路径,与搜索不同 |
由此能得到几个实用推论:标签搜索的粒度取决于 tags 数组是否存了这个键(SDK 没上报的 attribute,UI 永远搜不到,埋点问题见 05-Go客户端埋点.md);Duration 条件筛的是"单个 span 的时长",不是整条 trace 的端到端时长(trace 级过滤要靠操作名与时间窗间接完成);高基数标签值(比如把 order_id 当 tag 查)在 ES 上很便宜、在 Cassandra/Badger 这类 KV 后端上则是客户端侧大范围扫描(FAQ 推荐存储时说 ES 系"可直接查询、可做聚合",而 Cassandra 这类 KV 后端是在客户端侧实现搜索逻辑、受限且不一致,说的就是这个;细节见 04-存储后端.md)。
搜索 URL 就是一等公民接口
搜索页的状态全部编码在 URL query 参数里。frontend-ui 页给的嵌入式示例逐字如下(start/end 是微秒 epoch,lookback 是 1h 这类简写,其余还有 minDuration/maxDuration/tags 等参数):
http://localhost:16686/search?service=my-service&start=1543917759557000&end=1543921359557000&limit=20&lookback=1h&maxDuration&minDuration&uiEmbed=v0排障群里丢一条"带好全部搜索条件的链接"比截图有用;做告警到追踪的跳转链接,拼的也是这套参数(时间单位记得是微秒)。日志平台反跳 Jaeger 的按钮,用 linkPatterns(详见配置一节)同样能生成这类 URL。
刚生成的 trace 查不到,先怪可见性延迟再怪丢数据
ES 路线有个精确的官方解释可背(storage/elasticsearch 页"Durability versus searchability"):_bulk 返回成功只代表持久化,搜索可见性由索引 refresh 间隔决定(默认 1 秒),且 Jaeger 写入时不强制 refresh(强刷会拖垮索引吞吐)。所以刚发生的 span 在 ack 之后最长约一个 refresh 周期才会出现在搜索结果——“发了但查不到"先等一秒重试,再走 06-采样.md 的排障流程。Cassandra/Badger 后端没有 refresh 窗口这个概念——在这类存储上"查不到”,更该先怀疑采样或链路断了(走 06-采样.md 的排障流程),而不是等可见性。
Trace 详情页:Gantt、关键路径与可点击的标签
拿到一条 trace 后的主视图是 Gantt(span 树按时间轴铺开)+ minimap(整条 trace 的缩略导航)+ header 摘要(服务数、span 数、时长)。规模上限官方有实测背书:几万 span 的 trace 渲染得动(features 页:试过 80,000 个 span 的 trace)。页面上值得知道的配置:
- Critical Path:
criticalPathEnabled,默认开。开启后每个 span 会高亮"处在整条 trace 关键路径上"的部分——找耗时大头时直接按它走,不用人肉对时间轴。 - Top Tag Prefixes:
topTagPrefixes: ["http.", "db."]让匹配前缀的标签在 span 详情里置顶,默认空。语义约定标签(http.*、db.*)多的团队建议配上。 - Link Patterns:
linkPatterns能把 process/tags/logs/traces 四类位置的字段值变成可点击外链(比如点db.statement跳内部 SQL 平台、按 trace 起止时间拼出日志查询链接),模板语法#{field}加管道格式化函数,见配置一节。 - traceIdDisplayLength:列表/详情头部显示的 traceID 字符数,默认 7。日志/工单系统对 ID 有全量要求时调大它。
- JSON 视图与文件操作:trace 页有 JSON 视图(可
disableJsonView关掉);disableFileUploadControl控制"从本地文件加载 trace JSON"的入口——内网合规部署常关这两个,配合forbidNewPage: true阻止开新标签,是嵌入场景三件套。 - 时钟偏差:多机器时间不同步时 Gantt 会出现"子 span 比父 span 还早开始"的毛刺。v2 的调整开关是
jaeger_query.max_clock_skew_adjust,默认 0s——config.yaml 注释原文即说明这等于关闭 skew 调整。看到毛刺先修 NTP,再考虑调大该值开启这种有损修正。 - traceID 的表示(UI 输入什么格式):16 或 32 位 hex 字符串(128-bit traceID 32 位;legacy 64-bit 模式 16 位)——FAQ 页对"为什么 Kafka/Protobuf JSON 里长得不一样"(base64)有完整解释,贴 ID 进搜索框时以 hex 为准。
Archive:手工归档第二条存储
archiveEnabled: true(UI 配置)+ jaeger_query.storage.traces_archive 指向第二个 backend(config.yaml 出厂例:some_store 主存 + another_store 归档),trace 页上就会出现归档按钮。三个语义必须说准(frontend-ui 页原文):
- 归档是手工动作——从主存储把这条 trace 保存到归档存储,不是自动生命周期;
- 归档里的 trace 只能按 ID 直接打开,不进搜索索引(“Archived traces are only accessible directly by ID, they are not searchable”);
- 典型用法是"主存储短 TTL 滚动删、重要证据永久留"——归档保留多久由归档 backend 自己的生命周期策略决定,Jaeger 侧只负责存进去。
主存储 TTL/索引轮转的运维做法见 04-存储后端.md;api_v3 的 OpenAPI 契约里没有单独的归档查询端点,UI 里归档 trace 就是按 ID 直接打开。
Monitor 页与两张服务图
Monitor 就是 SPM 的 UI 入口(RED 指标时间序列页),默认关(monitor.menuEnabled: false),且开关背后还要求 metrics 后端真实存在——它是"配了才亮、亮了还可能空白"的双门槛功能,机制与排查全部放本文 SPM 专节。页面定位一句话(SPM docs 原文改写):帮你在不知道服务/操作名的情况下找出 Interesting 的 trace(高 QPS、慢、错误率高的),服务级 + 操作级聚合,延迟给 P50/P75/P95,另有一个 Impact = 延迟 × 请求率 的排序信号(用于压掉"天然就慢的批处理"、揪出"单次不快但打得多"的操作);从图表可以一键预填 Trace 搜索条件跳转取证——这条"从指标到 trace"的动线就是 Monitor 存在的意义。
服务拓扑两张图(features 页):
- System Architecture:全系统一跳依赖图(A-B-C 只说明 A→B、B→C 的调用都发生过,不代表 A→C)。节点粒度只有服务。
- Deep Dependency Graph(DDG):传递依赖图,必须选一个 focal service,只显示穿过它的调用链;节点粒度可在"服务 / 服务×端点"间切换(后者把
A::op1、A::op2画成不同节点)。官方原文注明:当前 DDG 只能由搜索结果里的 trace 现场构建(未来才有 Flink 全量聚合)。
生产部署最容易踩的是依赖图数据来源。FAQ"为什么 Dependencies 页是空的"给了标准答案:内存存储会基于内存里的全部 trace 实时计算依赖图;换成 Cassandra/ES/OpenSearch 等真实分布式存储后,全表扫描构建图太贵,必须由外部作业把依赖数据写进存储——官方给的两个作业:jaegertracing/spark-dependencies(周期性 Spark 作业,老牌)与 jaegertracing/jaeger-analytics(持续运行的 Flink 作业,实验性)。所以"本地跑得好好的、上了 K8s 依赖图空了"九成不是 bug,是没部署依赖作业。UI 侧参数:dependencies.menuEnabled(默认 true)、dependencies.dagMaxNumServices(默认 200,超过则 DAG 视图禁用)。
依赖图也有 HTTP 接口,两种形态:internal 的老端点 GET /api/dependencies?endTs=<毫秒epoch>&lookback=<毫秒>,返回 (caller, callee, count) 三元组列表;稳定端点是 v3 的 GET /api/v3/dependencies?startTime=<RFC3339>&endTime=<RFC3339>。官方对程序化取依赖图的推荐是 gRPC/Protobuf。
UI 配置速查
配置入口一个:
extensions:
jaeger_query:
ui:
config_file: /path/to/config-ui.json # 或 .jsJSON 与 JS 两种格式;JS 格式必须定义顶层 UIConfig() 函数返回配置对象(缺了这个函数 query 启动直接失败),换来的是注释与自定义函数能力(如自定义埋点插件)。参数总表(默认值全部出自 frontend-ui 页原文):
| 参数 | 默认 | 说明 |
|---|---|---|
themes.enabled |
false |
顶栏明暗切换按钮;关闭则恒为 light。选择存浏览器 localStorage,未设置时跟随系统 prefers-color-scheme |
dependencies.menuEnabled |
true |
依赖图菜单入口 |
dependencies.dagMaxNumServices |
200 |
超过此服务数则 DAG 视图禁用 |
monitor.menuEnabled |
false |
Monitor(SPM)入口 |
monitor.docsLink |
无 | 设置后 Monitor 页显示帮助链接 |
monitor.emptyState |
内置文案 | 自定义无数据空态(mainTitle/subTitle/description/info/alert) |
archiveEnabled |
false |
归档按钮;需配 traces_archive |
search.maxLookback |
内置 | 可选对象 {label: "2 Days", value: "2d"};Lookback 下拉中超长项直接隐藏 |
search.maxLimit |
内置 | Results to display 输入框上限 |
linkPatterns |
无 | 字段值→外链,见下 |
traceIdDisplayLength |
7 |
界面显示 traceID 的字符数 |
criticalPathEnabled |
true |
关键路径高亮 |
topTagPrefixes |
无 | 置顶展示的标签前缀数组 |
traceGraph.layoutManagerMemory |
16MB | GraphViz 渲染内存(Power of two;报 “Cannot enlarge memory arrays” 才需要动) |
disableFileUploadControl / disableJsonView / forbidNewPage |
false |
嵌入/合规三件套 |
tracking.gaID / tracking.trackErrors |
null / true |
Google Analytics 埋点;JS 格式可写 customWebAnalytics 工厂函数 |
menu |
无 | 顶部导航追加自定义链接/下拉组(label/url/anchorTarget,组用 items) |
linkPatterns 模板语法
四段式配置:type(process/tags/logs/traces 四类元数据位置)、key(字段名,traces 类型可省)、url、text(tooltip)。模板变量 #{...} 的可用字段:traces 级 duration、startTime、endTime、traceName、traceID;process/tags/logs 用同名字段,跨级引用 trace 字段加 trace. 前缀(如 #{trace.traceID})。格式化函数管道 #{field | function args}:epoch_micros_to_date_iso(微秒→ISO 时间)、add <offset>、pad_start <len> <char>,可链式。官方示例(跳日志平台,按 trace 起止时间前后各扩 10 分钟):
{
"linkPatterns": [
{
"type": "process",
"key": "jaeger.version",
"url": "https://github.com/jaegertracing/jaeger-client-java/releases/tag/#{jaeger.version}",
"text": "Information about Jaeger release #{jaeger.version}"
},
{
"type": "traces",
"url": "https://my-logs.server?from=#{startTime | add -60000000 | epoch_micros_to_date_iso}&to=#{endTime | add 60000000 | epoch_micros_to_date_iso}",
"text": "View logs around this trace"
}
]
}嵌入模式与 URL 参数
uiEmbed=v0 进入精简布局(目前支持 search 与 trace 两个页面),配合参数:uiSearchHideGraph=1(去散点图)、uiTimelineCollapseTitle=1(折叠头部摘要+minimap)、uiTimelineHideMinimap=1、uiTimelineHideSummary=1。示例:
http://localhost:16686/trace/{trace-id}?uiEmbed=v0&uiTimelineHideMinimap=1把 Jaeger 嵌进内部工单/运维平台时,这条 + 上面的搜索 URL 参数就是全部胶水层,不需要改 UI 代码。
base_path 与反向代理前缀
前缀挂载靠 extensions.jaeger_query.base_path(如 --set extensions.jaeger_query.base_path=/jaeger/prefix),后端把 UI 页面与 API 路由都注册在该前缀下。v2.18.0 起 UI 自动探测浏览器地址里的外部前缀(ADR-009,release notes 原文 “auto-detect ui base path from browser url”;仓库 examples/reverse-proxy/ 三种用例:前缀透传 / 单实例双前缀(代理 strip)/ 内外前缀不一致(代理 rewrite)——后两种不需要/少配 base_path 即可工作)。细节坑:访问不带尾斜杠的前缀根路径会 301 到带尾斜杠的规范 URL,代理规则要放行这个重定向。
两个轻量转发:otlp_proxy 与 ai.mcp
jaeger_query.otlp_proxy.target:让 UI 同源端口接 OTLP/HTTP——/api/otlp/v1/*收到什么就转发给配置的 OTLP HTTP receiver(出厂示例指向http://127.0.0.1:4318)。用途:嵌入/代理场景下让 demo 流量从 UI 端口进来,免开 4318。jaeger_query.ai.mcp:空对象{}即启用,在 query 端口挂出/api/ai/mcp/,进程内提供 Jaeger 遥测 MCP 工具(config.yaml 注释原文:replaces the retired standalonejaeger_mcpextension;可选base_url、skills_dir)。配套的ai.agent_url出厂示例为ws://localhost:16688。接入方式见08-可观测性联动.md。
UI 没有账号体系:认证交给反向代理
FAQ 原文口径:Jaeger UI 不支持任何 accounts/roles 概念,没有可配的用户认证;需要限制访问就上标准反向代理(HAProxy、NGINX、Keycloak 等),官方明说这样能白嫖代理生态的各种 SSO 集成,“这是 Jaeger UI 永远做不到的”。所以部署时的安全边界画在代理层:认证/审计/限流在代理,TLS 终结与证书在代理或 jaeger_query 的 TLS 配置(生产加固清单见 09-性能与运维.md)。同理,UI 也没有"每用户数据过滤"——所有能打开页面的用户看的是全量 trace,这个前提决定了它适不适合暴露给业务方自助。
查询 API:分层、稳定性与实例命令
三档标签与读 API 总表
APIs 页对兼容性给了三档定义:stable(保证向后兼容,破坏性变更必须升版本号/新命名空间)、internal(组件内部通信专用,不建议外部使用)、deprecated(仅因历史包袱维护,将来移除)。读侧的端口与端点(官方 Default Ports 表 + 正文):
| 端点 | 协议 | 内容与状态 |
|---|---|---|
16685 |
gRPC | jaeger.api_v3.QueryService(推荐,OTLP 系 Protobuf);同端口 legacy jaeger.api_v2.QueryService(stable)与 jaeger.api_v2.metrics.MetricsQueryService(SPM,见后文) |
16686 |
HTTP | /api/v3/*:api_v3 proto 的 JSON 映射(stable,OpenAPI 契约);/api/*:UI 内部 JSON(internal,undocumented、可能变化) |
程序化取数的正确姿势按优先级:gRPC api_v3 > HTTP /api/v3/*(proto 注解生成的 JSON,有 OpenAPI 文件可生成客户端) > /api/* 仅限"自己看看、不写进代码"的场景。v1 时代的 /api/services 等 UI JSON 路径在 v2 文档中没有逐条背书——别按记忆写脚本;v3 契约里有 services/operations/traces/dependencies 等对等端点,程序化指标查询见下文 SPM 一节。
v3 HTTP 面:已核实的端点与参数语法
端点全集(jaeger-idl swagger/api_v3/query_service.openapi.yaml 核对):
GET /api/v3/capabilities 能力探测
GET /api/v3/services 服务名列表
GET /api/v3/operations?service=&spanKind=
GET /api/v3/dependencies?startTime=&endTime= (RFC3339 时间)
GET|POST /api/v3/spans FindSpans
GET|POST /api/v3/trace-summaries FindTraceSummaries
GET|POST /api/v3/traces FindTraces(GET 用 query.* 展平参数,POST 收 JSON body)
GET /api/v3/traces/{traceId} GetTrace(hex 16/32 位;可选 startTime/endTime 缩小查找窗)参数语法要点(都来自 OpenAPI 文件):GET 搜索参数带 query. 前缀(query.serviceName、query.operationName、query.startTimeMin/Max(date-time)、query.durationMin/Max(Go duration 字符串,如 10s));分页 query.pagination.pageSize + query.pagination.pageToken(token 不透明、只对产生它的查询有效);结构化过滤 query.filter 是 URL 编码的 JSON Call 表达式(RFC 0005 §6);GetTrace 的 JSON 响应包在 {"result": ...} 信封里,裸 JSONPb 反序列化会翻车,按信封解析或用 OpenAPI 生成的客户端。
一组可跑的示例(默认 all-in-one,端口 16686):
# 服务列表
curl -s http://localhost:16686/api/v3/services
# 某服务的操作名
curl -s "http://localhost:16686/api/v3/operations?service=checkout"
# 搜 trace:服务 + 时间窗 + 慢于 2s + 分页
curl -s -G http://localhost:16686/api/v3/traces \
--data-urlencode 'query.serviceName=checkout' \
--data-urlencode 'query.startTimeMin=2026-10-20T10:00:00Z' \
--data-urlencode 'query.startTimeMax=2026-10-20T10:15:00Z' \
--data-urlencode 'query.durationMin=2s' \
--data-urlencode 'query.pagination.pageSize=20'
# 结构化过滤(http.status_code == 500)
curl -s -G http://localhost:16686/api/v3/traces \
--data-urlencode 'query.serviceName=checkout' \
--data-urlencode 'query.startTimeMin=2026-10-20T10:00:00Z' \
--data-urlencode 'query.startTimeMax=2026-10-20T10:15:00Z' \
--data-urlencode 'query.filter={"op":"eq","args":[{"attr":{"key":"http.status_code"}},{"scalar":{"value":"500"}}]}'
# 按 ID 取整条 trace(注意响应外层有 result 信封)
curl -s http://localhost:16686/api/v3/traces/00000000000000016a00000000000001internal /api/* 的三个官方示例(出现在 APIs/troubleshooting 页的仅有这些,当"看一眼"用):
# 按 traceID 取(UI 内部接口,无兼容承诺)
curl -s http://localhost:16686/api/traces/00000000000000016a00000000000001
# 依赖图:endTs 毫秒 epoch,lookback 毫秒
curl -s "http://localhost:16686/api/dependencies?endTs=1790812800000&lookback=3600000"
# 采样策略服务(也即 06 章的验证入口,14268 是 legacy 端口上的同一接口)
curl -s "http://localhost:14268/api/sampling?service=checkout"
curl -s "http://localhost:5778/sampling?service=checkout"gRPC 16685:reflection 只有 list 能用
官方明说:gRPC 端口都开了 reflection,但内部用的 gogo/protobuf 与官方 golang/protobuf 存在兼容问题,目前只有 list 命令工作正常(apis 页原文)。排障第一步照抄官方:
grpc_cli ls localhost:16685
# grpc.health.v1.Health
# grpc.reflection.v1.ServerReflection
# grpc.reflection.v1alpha.ServerReflection
# jaeger.api_v2.QueryService
# jaeger.api_v2.metrics.MetricsQueryService
# jaeger.api_v3.QueryServicegrpcurl -plaintext localhost:16685 list 同样可用(它依赖的也是 list);但 reflection 的 describe 不可用,真要调方法得自带 proto:grpcurl 配 -import-path/-protos(从 jaeger-idl 仓库取 proto/api_v3/),或者直接用生成的语言客户端。写自动化取数选 gRPC v3 的理由:OTLP 系 Protobuf schema、类型化客户端、以及依赖图这类结构化返回。
Remote Storage:query 背后的第二种 gRPC
jaeger_storage.backends.<n>.grpc 形态下,query 连的是远端存储服务而非本地引擎(apis 页"Remote Storage API",stable):对端实现 TraceReader/DependencyReader/TraceService 三个 gRPC 服务即可,仓库示例出厂端点 localhost:17271。这意味着"只跑 query + 自定义存储"是被官方承认的形态——但注意 FAQ 的反面案例:直接拿别的 exporter 写进 ES/ClickHouse 的索引给 Jaeger query 读,不支持,schema 是 Jaeger 自己存储实现的私事(见 04-存储后端.md)。
SPM 专节:两条路线、命名与空白页排查
路线 A:Prometheus 预聚合(connector 管线)
v1 的 metrics-generator 组件在 v2 已消失,取而代之是 OTel spanmetrics connector——它在 traces pipeline 里消费 span、产 RED 指标,经 metrics pipeline 写入 PromQL 兼容后端(官方 SPM 页 architecture 节称之 “Pre-computing metrics”)。骨架逐字取自仓库 cmd/jaeger/config-spm.yaml:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter, spanmetrics] # connector 挂为 exporter
metrics/spanmetrics: # 第二条管线:接 connector 产出的指标
receivers: [spanmetrics]
exporters: [prometheus]
connectors:
spanmetrics:
metrics_flush_interval: ${env:SPANMETRICS_FLUSH_INTERVAL:-60s}
extensions:
jaeger_query:
storage:
traces: some_storage
metrics: some_metrics_storage # query 引用 metrics 后端
jaeger_storage:
metric_backends:
some_metrics_storage:
prometheus:
endpoint: http://prometheus:9090
normalize_calls: true
normalize_duration: true
exporters:
prometheus:
endpoint: "0.0.0.0:8889" # 供 Prometheus 抓取;也可换推送式 exporter两个端口别混淆:8889 是 spanmetrics 产出的指标的抓取端点;8888 是 Jaeger 自身 internal telemetry 的 /metrics(排障看它)。不想自建 Prometheus 也可以推送到外部 metrics 存储——SPM 页口径是"任何 PromQL 兼容后端都行"。
路线 B:ES/OpenSearch(及 ClickHouse)直查
官方页面称之为 “Direct to storage”:query 在查询时刻直接从 trace 存储计算 RED,不需要 connector、不需要第二条 metrics 管线。配置只剩"同名引用"三步(config-spm-elasticsearch.yaml 骨架):
extensions:
jaeger_query:
storage:
traces: elasticsearch_trace_storage
metrics: elasticsearch_trace_storage # 指向同一个后端
jaeger_storage:
backends:
elasticsearch_trace_storage: &es_config
elasticsearch: { server_urls: [http://elasticsearch:9200] }
metric_backends:
elasticsearch_trace_storage: *es_config # metric 后端同名同配置SPM 页正文点名的直查支持是 Elasticsearch 和 OpenSearch;仓库另有同模式的 config-spm-clickhouse.yaml(metric_backends 指向同一 ClickHouse 后端)。两条路线的硬取舍:A 多养一套指标链路、但查询轻(指标已在管线里预聚合,且聚合口径发生在哪一级——head 采样之前还是 tail 之后——由你在 collector 配置里选);B 零新增组件、指标是查询时现算——存储查询压力与查询时延都直接落在 ES/OS 集群上(容量账见 09-性能与运维.md)。
指标命名与时间线基数
SPM 页 “Derived Time Series” 给了基名约定(仅适用于 PromQL 路线):
- counter:
calls_total——总 span 数含错误 span;错误判定看status_code标签,STATUS_CODE_ERROR即错误。 - histogram:
[namespace_]duration_[units]——如未配 namespace、单位毫秒,则产生duration_milliseconds_count/_sum/_bucket(bucket 带le标签,默认 17 个延迟桶)。
抓到 Prometheus 里的全名取决于 connector 的 namespace 配置:官方 troubleshooting 节的示例查询用的是 traces_span_metrics_calls_total 与 traces_span_metrics_duration_milliseconds_bucket(即 connector 默认 namespace traces.span.metrics 经 Prometheus 名称清洗后的形态,改 namespace 全名就跟着变,以抓取端点实际输出为准)。Monitor 页对 Prometheus 实际发的 PromQL 也在 debug 日志里可见(同页示例),形如:
sum(rate(traces_span_metrics_calls_total{service_name =~ "redis", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name, span_name)核对命名以 exporter 端点的实际输出为准(curl -s http://<collector>:8889/metrics | grep calls),别照文档猜。normalize_calls/normalize_duration 这对开关的存在原因官方讲得很细:历史上 Jaeger 按 calls/duration 找指标,OTel prometheus exporter 又爱给 counter 补 _total 后缀,两边命名对不上就全靠 normalize 兼容——config-spm.yaml 出厂即 true。
基数(时间线条数)估算公式(官方原文):
num_status_codes(≤3, 典型 2: ok/error) × num_span_kinds(≤6, 典型 2: client/server) × (1 + num_latency_buckets, 默认 17) × num_operations
→ 最大 324 × 操作数;典型 72 × 操作数按操作数乘一乘就知道 SPM 值不值:300 个操作 ≈ 2.2 万条时间线,指标后端的内存与抓取账都在这(成本视角见 09-性能与运维.md)。自定义 duration 桶或自定义维度会改变公式;且 SPM 查询不支持自定义维度——多余的维度标签只会被聚合掉,别指望往 span 上塞业务维度就能在 Monitor 里按它下钻。
打开与"空白页"排查
打开 Monitor 的条件:UI 配置 monitor.menuEnabled: true + jaeger_query.storage.metrics 指向可用的 metrics 后端(任一路线)。空白/缺数据的四类原因,按官方 troubleshooting 节的排查顺序:
- query ↔ 指标后端连通性:看 Jaeger 自身 telemetry:
curl -s http://<jaeger>:8888/metrics | grep jaeger_metricstore——jaeger_metricstore_requests_total{operation=...,result="ok"}增长为正常,result="err"增长说明读不到(连不上/403)。403 常见于 Prometheus 要 bearer token:metrics 后端配token_file_path(可配token_override_from_context透传请求携带的 token)。 - 指标存在但名字对不上:直接查指标后端确认
calls_total/duration_*系列在涨;命名差异用normalize_calls/normalize_duration或对齐 connector namespace 解决。 - 服务/操作不出现在 Monitor,但搜索下拉里却有:官方点名的高频原因——Monitor 默认只查
serverspan kind(避免 client/server 双计数),你的数据可能是 client/unspecified kind,属埋点质量问题,SDK 侧补SpanKind(见05-Go客户端埋点.md)。 - 有错误 span 却没有错误曲线:指标缺
status.code维度——检查 collector 配置有没有exclude_dimensions: ['status.code'],把它排除掉等于亲手拆了 Jaeger 的错误判定。
程序化读 RED 指标:官方推荐 jaeger.api_v2.metrics.MetricsQueryService gRPC 端点(metricsquery.proto,就在 16685 的 reflection 列表里);Monitor 页用的 HTTP JSON 是 internal 实现,规格文档在仓库 docker-compose/monitor 的 README(#http-api)。采样与口径:RED 由"到达的 span"聚合,head/tail 采样直接改变 QPS 与错误率的绝对值——做告警前先读 06-采样.md 的组合套路一节。
扩展键位速查:jaeger_query
query 角色的全部行为由 extensions.jaeger_query 一个块决定(键名逐字对照 v2.22.0 仓库 config.yaml/all-in-one.yaml/config-spm*.yaml):
extensions:
jaeger_query:
storage:
traces: some_store # 主 trace 存储(后端名,见 04)
# traces_archive: another_store # 归档第二存储(archiveEnabled 的前提)
# metrics: some_metrics_storage # Monitor/SPM 的指标后端
ui:
config_file: ./config-ui.json # UI 配置(JSON 或 JS)
# base_path: /jaeger # 反向代理前缀(v2.18+ 外部前缀可自动探测)
# otlp_proxy:
# target: http://127.0.0.1:4318 # UI 同源 /api/otlp/v1/* 转发
# ai:
# agent_url: ws://localhost:16688
# mcp: {} # /api/ai/mcp/ 端点(presence 即启用)
max_clock_skew_adjust: 0s # 时钟歪斜调整上限,0=关闭| 键 | 不配会怎样 |
|---|---|
storage.traces |
无法查询(query 扩展的核心依赖,后端名必须存在于 jaeger_storage.backends) |
storage.traces_archive |
UI 即便 archiveEnabled: true 也没有可写入的归档存储 |
storage.metrics |
Monitor 页无数据源;配了 menuEnabled 也只剩空壳 |
ui.config_file |
全默认 UI(无主题切换、无 Monitor、无归档按钮、依赖菜单默认开) |
otlp_proxy |
UI 端口不接 OTLP 上报(直连 4318 即可,通常不需要) |
查询侧症状速查
| 症状 | 最可能的原因 | 第一动作 |
|---|---|---|
| 搜索页 Service 下拉为空 | 存储里就没有该服务的数据 | curl /api/v3/services 确认,空则回 06-采样.md 排障 |
| 刚产生的 trace 查不到 | ES refresh 可见性窗口(约 1s);其次采样/断链 | 等一秒重试,仍无走采样流程 |
| 依赖图页空白(生产存储) | 没部署依赖计算作业 | spark-dependencies / Flink job(见 03-部署与Kubernetes.md) |
| Monitor 整页空白 | monitor.menuEnabled 未开,或 metrics 后端不可达 |
查 jaeger_metricstore_requests_total{result="err"} |
| Monitor 有调用量、无错误曲线 | 指标缺 status.code 维度(exclude_dimensions 排除过) |
修 collector 的 spanmetrics 配置 |
| 搜索下拉有、Monitor 没有该服务 | span 未设 kind,而 Monitor 默认只查 server | 埋点补 SpanKind(05-Go客户端埋点.md) |
| 套了 SSO 代理后页面/资源 404 | 前缀与路由没对上 | base_path + v2.18+ 自动探测;放行尾斜杠 301 |
| 归档按钮不出现 | 缺 archiveEnabled 或缺 traces_archive 后端 |
两个都配上再刷新 UI 配置 |
实战建议
- 把 UI 当排障入口,不当报表工具。搜索页每次查询都打到存储后端:深时间窗 + 高基数 tag + 大 limit 组合是最容易把 ES 打穿的操作。长期趋势与容量分析直接查指标后端(PromQL),别用 UI 搜索回放历史;UI 的搜索护栏
search.maxLookback/search.maxLimit在生产副本上建议显式收紧(默认值偏开发友好)。 - 脚本取数只用 16685 gRPC v3 或
/api/v3/*。/api/*是 UI 私产、官方明说会变,v1 时代的端点记忆一律不要带进 v2 脚本;要跨版本稳,gRPC proto 优先。 - 上了生产存储,依赖图 = spark-dependencies/Flink 作业,部署时当作一个正式组件排期(
03-部署与Kubernetes.md可挂为 CronJob),别等有人问"图怎么是空的"才补。 - Monitor 上线按 checklist 走:菜单开关 → metrics 后端连通(
jaeger_metricstore_*)→ 指标命名(normalize/namespace)→ 埋点质量(span kind、status.code 维度)。四项各对应上面一类空白症状。 - 嵌入与分享优先用 URL 层能力:搜索条件全编码在 URL、trace 直链
/trace/{id}、uiEmbed=v0精简布局、linkPatterns双向跳转——内部平台集成靠这些就够,不需要 fork UI。 - UI 前置反向代理统一收口:认证、TLS、访问日志、前缀(v2.18+ 自动探测)都在代理层解决(见
09-性能与运维.md);记住所有能打开页面的人看到的是全量数据,据此决定暴露范围。