上一篇(第 08 篇)补全了可视化类型的全貌:Lens 覆盖通用场景,TSVB 专注时序,Vega 作为全自定义逃生舱。本篇回答的问题是:这些可视化如何被组合到一个 Dashboard 里;面板之间如何共享过滤状态;Dashboard Saved Object 的结构如何用 references 管理对可视化对象的依赖;URL 里的 rison 编码存了什么。

Embeddable 框架:任意内容的统一容器契约

Dashboard 不是一个专门为可视化设计的容器,它是 Embeddable 框架的消费者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Embeddable 框架的契约:

接口 IEmbeddable<TInput, TOutput>
├── input:Dashboard 传入的上下文(时间范围、过滤、查询)
│ + 面板自身的配置(savedObjectId、size、title...)
├── reload():重新执行查询
├── render(node):把自身渲染到指定 DOM 节点
└── getOutput():向父容器暴露当前状态(loading / error / rendered)

任何插件都可以注册一个 EmbeddableFactory:
EmbeddableFactoryRegistry.registerFactory(type, factory)

Dashboard 通过 type 找到对应的 factory,调用 factory.create(input) 得到
一个 Embeddable 实例,再调用 render() 把它挂到 Dashboard 的网格格子里。

这意味着 Dashboard 里的"面板"不只是 Lens 可视化,也可以是地图(Maps 插件)、Saved Search(来自 Discover)、Controls(过滤器控件)、任何通过 EmbeddableFactory 注册的类型。Embeddable 框架是 Kibana New Platform 插件体系的一个具体应用:Dashboard 对面板的内容一无所知,只负责把输入传进去、提供容器、接收输出。

面板间的 filter 联动

Dashboard 的过滤状态存在一个全局的 FilterManager 和 QueryService 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dashboard 状态流:

用户动作(时间选择器 / 搜索框 / 控件 / 面板内点击)

FilterManager.addFilters() 或 QueryService.setQuery()

Dashboard 订阅状态变化 → 更新所有 Embeddable 的 input

每个 Embeddable.reload() → 重新发 ES 请求 → 重新渲染

面板内点击的特殊路径(drill-through / filter apply):
用户点击 bar chart 某一段 → Lens 的 click handler

通过 FilterManager 往全局状态里追加一条 term filter

所有其他面板感知到 filter 变化,触发 reload()

这个机制是 Dashboard 面板联动的核心:所有面板共享同一份 filter / query / time range 状态,任何一个面板改变这个状态,其他所有面板都会重新查询。这不是面板之间点对点通信,而是发布-订阅模型——面板不知道也不需要知道"谁改了过滤"。

Drilldown(钻取)是另一个维度的联动:点击面板里的某个数据点,跳转到另一个 Dashboard 或 URL,并把当前的过滤上下文带过去。这是在 filter 联动之上的导航层。

Dashboard Saved Object 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Saved Object type: dashboard

attributes
├── title
├── description
├── panelsJSON → 序列化的面板数组(含位置、尺寸、面板配置)
├── optionsJSON → 面板选项(是否显示 title 等)
├── timeFrom / timeTo → Dashboard 默认时间范围(可选)
├── refreshInterval → 自动刷新间隔(可选)
└── kibanaSavedObjectMeta
└── searchSourceJSON → Dashboard 级别的过滤和查询

references(关键段)
├── { type: "visualization", id: "abc", name: "panel_1" }
├── { type: "lens", id: "def", name: "panel_2" }
├── { type: "search", id: "ghi", name: "panel_3" } ← Saved Search(Discover)
└── { type: "index-pattern", id: "jkl", name: "..." }

panelsJSON 里的每个面板用 panelRefName 字段指向 references 数组里的一条记录。这是 Kibana Saved Object 引用图的标准做法:不在 attributes 里硬写 ID,而是在 references 数组里集中声明,让 Kibana 的引用追踪机制能正确处理导出/导入/迁移时的 ID 替换。

导出 Dashboard 时,Kibana 沿着 references 递归把所有依赖的 visualization、lens、search、index-pattern 一起打包。这就是为什么"导出 Dashboard"的文件体积通常比"Dashboard 自身"大得多——它实际上是整个依赖子图的序列化。

URL 状态与 rison 编码

打开一个 Dashboard 时,URL 里通常有两个关键参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
URL 结构示例:

/app/dashboards#/view/DASHBOARD_ID
?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))
&_a=(description:'',filters:!(),query:(language:kuery,query:''),viewMode:view)

_g(global state):全局状态,跨 app 持久化
- time:当前时间范围
- refreshInterval:自动刷新配置
- filters:全局过滤(跨 app 传递时保留)

_a(app state):当前 app 的本地状态
- query:搜索框内容
- filters:Dashboard 级别的过滤
- viewMode:view 或 edit
- panels:(编辑状态下面板位置变化)

rison 是一种比标准 JSON 更紧凑的 URL 可读格式(RISON),把 JSON 的花括号/方括号/引号做了简化,使得复杂的过滤条件可以放在 URL 里而不至于过于冗长。Kibana 使用 rison 库在 JavaScript 侧序列化和反序列化这两个参数。

URL 状态机制的实际影响:可以通过拼接带特定 _g 参数的 URL 来预设过滤条件分享给其他人;_g 状态在 Kibana 不同 app(Discover、Dashboard)之间跳转时保留,这就是为什么从 Dashboard 跳到 Discover 后,时间范围还是原来的。

实验:观察 Dashboard 的依赖图和 URL 变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
实验准备:
建 3 个 Lens 可视化,各使用不同的 Data View:
- viz-A:来自 logs-*,柱状图(@timestamp + log.level)
- viz-B:来自 metrics-*,折线图(@timestamp + system.cpu.total.pct)
- viz-C:来自 traces-*,饼图(service.name + count)

Part 1 — 查看 Dashboard Saved Object:
1. 把 3 个 viz 加入同一个 Dashboard,保存
2. 在 Stack Management → Saved Objects 里找到这个 Dashboard
3. 查看其 JSON,在 references 数组里应能看到:
- 3 条 lens 类型的引用(对应 3 个 viz)
- 3 条 index-pattern 类型的引用(对应 3 个 Data View)
4. 记录 panelsJSON 里某个面板的 panelRefName,
在 references 里找到对应的 { type, id, name }

Part 2 — 观察 filter 联动:
1. 在 Dashboard 时间选择器里选 last 1 hour
2. 打开浏览器 DevTools → Network,过滤 _msearch 请求
3. 统计 viz-A、viz-B、viz-C 各自发出几个 ES 请求
4. 在搜索框里输入一个 KQL 过滤(如 log.level:error),
观察 3 个面板是否都重新发了请求,Network 里的请求数变化

Part 3 — 观察 URL 状态:
1. 记录初始 URL 的 _g 和 _a 参数值
2. 在搜索框里加一个条件,观察 _a 参数变化
3. 点击 viz-A 的某个 bar,应用为过滤,再观察 _a 变化
4. 把 _a 参数里新增的 filter 对象手工解析为 KQL,确认内容符合

将实验结果映射到内部对象

1
2
3
4
5
6
7
实验现象                  → 内部机制

3 个面板同时刷新 → FilterManager 广播 → 3 个 Embeddable.reload()
_a 参数变化 → AppStateService 序列化 filters 到 rison → 写入 URL history
面板 panelRefName 对应 → Saved Object references 的 name 字段
references 里有 index-pattern → Dashboard export 时把 Data View 也一起打包
Dashboard 加载时多个请求 → 每个 Embeddable 各自独立发 ES 请求(无合并)

Dashboard 的 View-Only 模式

Kibana 支持 Dashboard-only 模式(通过 Spaces 的 Feature Controls 或 URL 参数 embed=true)。在这个模式下,面板仍然可以接收 filter(通过 URL _g/_a 参数预设),但不显示编辑工具栏,适合嵌入外部系统(iframe 嵌入)或分发给只读用户。

embed=true 参数还会隐藏 Kibana 的顶部导航栏,只渲染 Dashboard 内容区域,常用于把 Kibana Dashboard 嵌入到内部运维平台或报表系统的页面里。

模式提炼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
模式:Embeddable 框架 + 共享状态广播

核心原则:
1. 容器对内容一无所知:Dashboard 通过 EmbeddableFactory 注册表
按 type 找到渲染逻辑,不硬编码任何面板类型
2. 状态集中,不是分散:过滤/时间/查询集中在 FilterManager,
面板不持有状态,只接收 input 并 reload
3. 引用不是嵌入:Dashboard Saved Object 里存的是指向 viz 的 references,
不是把 viz 的内容内联进来;导出/导入/迁移时,引用图要整体操作
4. URL 是状态快照:_g/_a 参数让任意时刻的 Dashboard 状态可复现、可分享

可迁移的设计原则:
- "容器不知道内容"的 Embeddable 模式等价于微前端的 slot/portal 机制
- "全局状态广播"优于"面板间点对点通信",在面板数量多时明显更简洁
- URL 状态序列化(rison/query params)让"分享当前视图"成为零成本操作

工程迁移表

Kibana 概念 Grafana 对应 React 应用对应 微前端对应
Embeddable 框架(type → factory → render) Panel Plugin 注册 React Context + 动态组件 微应用注册表 + 动态加载
FilterManager(全局共享过滤) Grafana 变量 + $__timeFilter Redux store / Context provider 共享状态层(Zustand/Jotai)
Dashboard references(依赖图) Dashboard JSON panel.datasource React prop 依赖 微应用依赖声明
rison(URL 状态编码) Grafana URL state(JSON + base64) query-string / URLSearchParams 路由状态序列化
_g(global state 跨 app) Grafana 全局变量(所有 panel 可见) React Router location state 全局路由状态
embed=true(iframe 嵌入模式) Grafana kiosk mode / share embed React standalone render 微前端 iframe 沙箱

常见误解

误解一:“Dashboard 面板共享一个 ES 请求”。每个面板各自独立发 ES 请求,没有跨面板的请求合并。一个有 10 个面板的 Dashboard 打开时会同时发出 10 个(或更多,因为有些面板发多个 sub-request)ES 请求。这是 Embeddable 各自独立 reload 的直接结果。

误解二:“修改一个面板不影响 Dashboard Saved Object”。在 Kibana 8.x 的 inline editing 模式下,直接在 Dashboard 内编辑 Lens 面板并保存,会同时更新面板对应的 Lens Saved Object 和 Dashboard 的 panelsJSON。Dashboard Saved Object 保存的是面板的位置/尺寸和对 viz 对象的 references,viz 内容的修改保存在 viz 自己的 Saved Object 里。

误解三:“URL 里的 rison 可以直接拼接过滤来"硬过滤"数据”。通过 URL 传入的过滤在 Kibana 的 FilterManager 里是可以被用户在界面上清除的(除非配置了 pinned filter 或特殊的 Controls 面板)。如果需要不可覆盖的过滤,应在 Kibana 的 Spaces 或 Data View 层面用 runtime field 或 index alias 做数据级别的隔离,而不是依赖 URL 参数。

误解四:“Dashboard 的 references 只有 visualization 类型”。Dashboard 可以引用 lens、visualization、search(Saved Search)、map(Maps 插件保存的地图)、index-pattern(Data View)等多种类型。Embeddable 框架的开放性决定了 references 的类型集合随已安装插件而变化。

练习

  1. 用 Kibana Saved Objects API 查看一个已有 Dashboard 的完整 JSON:GET api/saved_objects/dashboard/{id},画出它的 references 图(面板 → visualization → data view 的三层引用关系),标出每条引用的 type 和 id。

  2. 在 Dashboard 里配置一个 Controls 面板(Options List Control),绑定到 log.level 字段。观察当用户在 Controls 里选择一个值时,URL 的 _a 参数如何变化,以及其他面板是否收到了相同的 term filter。

  3. 把当前 Dashboard 的 URL(含 _g_a)保存下来,在另一个浏览器标签页粘贴打开,验证时间范围、搜索条件、已应用的过滤是否完整恢复。然后手工修改 _g 参数里的 time.from 值,刷新页面,观察时间范围变化——理解 URL 状态作为"可编程快照"的含义。

系列导航

序号 主题 状态
00 导读:Kibana 的状态存在 Elasticsearch 里
01 Kibana 架构:浏览器、Node.js server 与 New Platform
02 Saved Object:Kibana 一切状态的统一模型
03 Data View(Index Pattern):查询之前的字段抽象
04 Search Source 与查询翻译:KQL、Lucene 与 Query DSL
05 Discover:一次查询的完整执行路径
06 Visualize:手动聚合配置的模型
07 Lens:拖拽背后的自动聚合推断
08 TSVB、Timelion 与 Vega:时序与自定义可视化
09 Dashboard 与 Embeddable:面板的组合与依赖 本篇
10 Alerting 框架:Rule、Connector 与 Action
11 Reporting:从 Dashboard 到 PDF/PNG
12 Task Manager:分布式任务调度
13 Spaces 与安全:RBAC、Feature Controls
14 插件体系:New Platform 的 setup/start 生命周期
15 性能模型:bundle、bootstrap 与异步搜索会话
16 Kibana vs Grafana:两种可视化平台的设计取舍
17 演进:从纯前端到 New Platform 再到 Serverless

参考资料