如何嵌入 Kibana 仪表板

像我这样的前端工程师,经常被要求将来自 Kibana® 等来源的现有仪表板嵌入到 JavaScript Web 应用程序中。这是我多次需要执行的一项任务,因为我们需要快速部署用户生成的视图,或者允许用户控制给定的视图。根据我们从优秀的开发者社区中经常收到的问题来看,我并不是唯一遇到这种情况的人。

Kibana 仪表板等数据可视化工具可以让即使是设计或技术能力最弱的用户也能在 Elasticsearch® 数据之上快速轻松地创建视图并制作原型视图。事实上,这意味着将仪表板嵌入现有 Web 应用程序是最难的部分 — 尤其是当我们想集成自定义 Web 控件来驱动数据视图,为用户提供一致的样式和体验时。

本文将通过代码示例,逐步讲解如何使用 HTML iframe 将 Kibana 仪表板嵌入到 Web 应用中。此外,还将介绍这些视图的 Kibana 身份验证,以及如何使用 JavaScript 将自定义控件连接到嵌入式视图。

什么是 iframe?

本文介绍的两个示例都使用了 iframe 来嵌入我们的仪表板。iframe(用 <iframe> HTML 标签表示)允许您将另一个网页嵌入到当前文档中。具体来说,我们将从示例航班数据数据集加载的全球航班仪表板嵌入到我们页面内的自有 Elastic® 部署中。

在应用程序中嵌入其他源时,务必确保这是用户应有权访问的可信数据源。我们必须使用适当的内容安全策略、利用沙箱属性的限制,以及限制嵌入式内容操作的权限。通过不在 iframe 中指定沙箱属性,我们在默认情况下包含了所有限制。

在应用程序中添加第三方内容时,性能也是需要考虑的因素。由于 iframe 比其他资源消耗更多带宽,因此在单个应用程序中使用大量 iframe 会降低整个应用程序的运行速度。对于希望在应用程序中嵌入多个 Kibana 仪表板的用户,应尽可能减少嵌入数量,并进行应用程序性能测试。虽然添加组件和仪表板很容易,但作为开发人员,我们需要确保提供用户需要的数据,而不是他们想要的所有花哨控件。因此,在选择仪表板和可视化工具时,应与使用者合作,明确他们的真正需求。

使用 HTML iframe 进行基本嵌入

本基本示例所述,在 Web 应用程序中包含全球航班仪表板的代码可通过 Kibana 的分享选项轻松生成:

Kibana 嵌入代码图

系统会生成一个 iframe 代码片段,其中包含您选择的相关选项以及当前筛选条件,您可以将其粘贴到您的 HTML 中:

<iframe src="https://my-deployment:9243/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?embed=true&_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-1y%2Fd%2Cto%3Anow))&show-top-menu=true&show-query-input=true&show-time-filter=true" height="600" width="800"></iframe>

生成的代码片段使用像素单位来测量 iframe 的宽度和高度。确保 iframe 尺寸与其中的内容相匹配一直是个挑战。最佳实践是考虑使用视口尺寸属性 vw 和 vh 来调整 iframe 的大小,或使用媒体查询来处理现代响应式设计中多种不同设备尺寸的需求。

由于可选的设置数量众多,要弄清楚自己需要什么可能会让人感到困惑。这些选项允许您配置仪表板的状态以及 iframe 中可见的控件。

要生成的 URL 类型可以是两种不同的选项之一:

  1. 快照:一个包含仪表板当前完整状态的 URL 编码,这意味着对仪表板的任何更改不会反映在嵌入式版本中。
  2. 已保存的对象:使用指向仪表板已保存对象 ID 的 URL,这意味着在生成 URL 后对仪表板所做的任何更改都将对 JavaScript 应用程序的用户可见。

根据作者的经验,这些仪表板可能会发生变化。因此,对于嵌入操作,使用“已保存对象”选项最为合适,以确保在生成 URL 后对仪表板所做的更改能够生效。

包含设置指示要在嵌入式仪表板顶部包含的其他控件:

Kibana 仪表板元素
  1. 顶部菜单:包含仪表板功能(如编辑和全屏)的设置,通过在 Kibana URL 中包含 show-top-menu=true 来控制。
  2. 查询:KQL 查询栏允许您筛选仪表板中可见的数据,由 show-query-input=true URL 参数表示。
  3. 时间筛选:用于选择仪表板中数据日期范围的日期选择器,通过 URL 中的 show-time-filter=true 实现。
  4. 筛选栏:隐藏用于添加数据筛选的设置,需要将 hide-filter-bar URL 参数设置为 true

如果没有使用公共 URL,我们将被提示登录才能访问仪表板。目前,用户体验并不流畅,但拥有登录凭据的用户可以访问仪表板。

嵌入式仪表板不支持匿名身份验证

自动登录

为确保仪表板能够自动显示,需要将身份验证与 Kibana 中的仪表板集成,从而避免用户需要在 JavaScript 应用程序和仪表板中分别输入凭据。这带来了无缝的用户体验。这可以通过以下两种方式之一来实现:

  1. 启用匿名身份验证,为任何无法提取身份验证令牌的传入请求提供一组默认的凭据和权限(在免费套餐中可用)。

  2. 增加对 SAML 单点登录 (SSO) 提供商的支持,将未经身份验证的用户重定向到 SSO 门户,并将经身份验证的用户直接传递到仪表板。这是一个授权功能

接下来我们将介绍匿名选项。首先,我们需要在 kibana.yml 文件中添加匿名身份验证提供程序 anonymous1

xpack.security.authc.providers:
  anonymous.anonymous1:
    order: 0
    credentials:
      username: "my_anonymous_user"
      password: "password"

还必须重新生成 iframe URL,以指定 auth_provider_hint 参数,从而将提供商 anonymous1 的配置凭据链接到嵌入式内容:

<iframe src="https://my-deployment-9f9945.kb.eu-west-2.aws.cloud.es.io:9243/app/dashboards?auth_provider_hint=anonymous1#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?embed=true&_g=(refreshInterval%3A(pause%3A!f%2Cvalue%3A120000)%2Ctime%3A(from%3Anow-1y%2Cto%3Anow))&show-time-filter=true" height="600" width="800"></iframe>

未包含 auth_provider_hint=anonymous1 参数将导致无法以访客身份继续访问仪表板。同样,如果未在 Kibana 中使用正确的用户名和密码注册相应的用户角色,将导致身份验证错误:

嵌入式身份验证无效凭据

为解决此问题,请确保您已注册一个用户,并使用与 kibana.yml 中提供程序配置相匹配的正确密码。建议将此账户的权限限制为最低必需权限,因为未经身份验证的用户也将获得访问权限。

Kibana 创建身份验证用户

此时,您可能认为一切就绪。但是,当您连接到您的仪表板时,您会看到一些奇怪的重复刷新事件发生:

嵌入式仪表板内容策略块

此问题是由浏览器阻止 Kibana 仪表板引起的。现代 Web 浏览器强制执行同源策略,以限制嵌入式内容。如果两个 URL 具有相同的协议、端口和主机,则它们属于同一来源。简而言之,除非内容策略允许,否则来自不同来源的任何内容默认都会被阻止。

要允许浏览器在启用安全功能的情况下(Elastic v8.x 的默认设置)将会话 Cookie 传输到 ELK 堆栈中的 Kibana 服务器,您必须在 kibana.yml 中配置 sameSiteCookies 选项 :

xpack.security.sameSiteCookies: "None"

完成最后一步后,我们就可以看到 Kibana 仪表板嵌入到我们的 JavaScript 应用程序中:

基本的嵌入式 Kibana 仪表板

使用自定义控件

您可能已经注意到,此仪表板使用了控件来筛选数据。允许用户探索数据并缩小选择范围,从而发现有价值的见解非常重要。

在某些情况下,使用仪表板内的控件可能不是正确的选择。您可能想在现有应用程序中使用自定义控件来实现设计一致性。或者,您可以将仪表板与您希望筛选的其他数据源和可视化内容并列显示,从而形成一致的体验。

在这个高级示例,我们将展示如何将日期选择器和下拉列表选项中的日期范围设置传递到仪表板,以强制更新仪表板:

高级嵌入式 Kibana 仪表板

使用自定义控件意味着我们需要了解仪表板 URL 的组成。让我们来看下面的示例:

https://elastic-deployment-9f9945.kb.eu-west-2.aws.cloud.es.io:9243/app/dashboards?auth_provider_hint=anonymous1#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?embed=true&_g=(filters:!(),refreshInterval:(pause:!f,value:0),time:(from:'${selectedStartDate}',to:'${selectedEndDate}'))&_a=(query:(language:kuery,query:'${carrierQuery}'))&hide-time-filter=true

除了基本示例中讨论的参数外,我们还需要操作筛选器。正如之前在社区中讨论的那样,Kibana 中有两个级别的筛选器:

  1. 全局状态(由 _g 参数表示)指的是在各个 Kibana 应用程序之间传递的状态。一个典型的示例是固定筛选器,包括选定的开始日期和结束日期。

  2. 状态仅限于当前仪表板等单个应用程序。这由 _a URL 参数表示。

要从任何日期选择器传递日期范围,当控件应用新的日期范围时,必须更新 URL 的 iframe 中的起始日期和结束日期。我们将这些值设置为过去一年的相对时间范围。以 easepick 为例,在初始化时注册的“选择”事件中捕获新选择的日期,并在使用新 URL 更新 iframe 的 src 属性之前转换为所需的 ISO 日期格式。

let selectedStartDate = 'now-1y';
let selectedEndDate = 'now';

const picker = new easepick.create({
    element: '#datepicker',
    css: [
        'https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css'
    ],
    zIndex: 10,
    firstDay: 0,
    autoApply: false,
    format: 'MMM DD, YYYY @ HH:MM:00',
    plugins: [
        'RangePlugin',
        'TimePlugin'
    ],
    setup(picker) {
        picker.on('select', (e) => {
            const dateFormat = 'YYYY-MM-DDTHH:MM:00.000Z';
            selectedStartDate =  picker.getStartDate().format(dateFormat);
            selectedEndDate =  picker.getEndDate().format(dateFormat);
            
            dashboardUri=getDashboardUri();
            iframe.setAttribute('src', dashboardUri);
        });
     }
});

就 URL 本身而言,全局筛选参数 _g 会根据所选范围进行更新,如 getDashboardUri() 辅助方法中所示:

function getDashboardUri() {
    return `https://my-deployment-9f9945.kb.eu-west-2.aws.cloud.es.io:9243/app/dashboards?auth_provider_hint=anonymous1#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?embed=true&_g=(filters:!(),refreshInterval:(pause:!f,value:0),time:(from:'${selectedStartDate}',to:'${selectedEndDate}'))&hide-time-filter=true`;
}

对于您希望在下拉列表等控件中筛选的任何数据字段,我们需要使用“查询”选项,将这些值传递到 _a 参数中。以以下 HTML“选择”控件为例:

<div class="carrier-select-container">
  <label for="carrier-select">Carrier</label>
  <select name="carrier-select" id="carrier-select" onchange="updateWithCarrier()">
    <option value="ES-Air">ES-Air</option>
    <option value="JetBeats">JetBeats</option>
    <option value="Kibana Airlines">Kibana Airlines</option>
    <option value="Logstash Airways">Logstash Airways</option>
  </select>
</div>

当选定值发生变化时,可以通过与 onchange 事件关联的 updateWithCarrier() 方法来提取该值。该事件是从事件处理程序中的“选择”控件中提取出来的:

function updateWithCarrier() {
    const carrierSelect = document.getElementById('carrier-select');
    selectedCarrier = carrierSelect.value || '';

    dashboardUri=getDashboardUri();
    iframe.setAttribute('src', dashboardUri);
}

请注意,我们仍然在使用 getDashboardUri() 辅助函数,该函数需要更新,以生成 KQL 查询,并通过应用程序筛选器中的“查询”选项将其传递给仪表板 URL:

function getDashboardUri() {
  const carrierQuery = rison.encode_object({Carrier : encodeURIComponent(selectedCarrier)});
  return `https://my-deployment-9f9945.kb.eu-west-2.aws.cloud.es.io:9243/app/dashboards?auth_provider_hint=anonymous1#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?embed=true&_g=(filters:!(),refreshInterval:(pause:!f,value:0),time:(from:'${selectedStartDate}',to:'${selectedEndDate}'))&_a=(query:(language:kuery,query:'${carrierQuery}'))&hide-time-filter=true`;
}

Kibana 使用了 Rison 和 URI 编码,需要在包含查询之前应用这些编码。这一点在上面的 carrierQuery 定义中已有说明,其中我们使用了 rison.js,并通过常规的 encodeURIComponent 方法对所选值进行转义。

连接成功后,每次选择新内容时,仪表板都会刷新显示。但请注意可能出现的 Rison 文件格式错误,例如我们论坛上报告的此类错误,这类问题可能难以进行故障排查。

请注意,URL 随时可能发生变化,因此,如果您选择嵌入任何第三方工具,可能会出现功能失效的情况。请务必检查每个 Kibana 版本是否存在重大更改,并仔细对您的应用程序进行回归测试。

Kibana 仪表板助您实现更大成就

本文深入探讨了嵌入式 Kibana 仪表板的世界。我们介绍了一个使用单个 HTML iframe 的简单示例;以及一个使用我们自定义的 JavaScript 组件向仪表板传递参数的复杂示例。所有代码均可在此 GitHub 存储库中找到,您可以轻松地将其适配到您喜爱的 Web 技术、JavaScript 框架或 TypeScript 中。

请在我们的社区论坛上共享您在嵌入仪表板时遇到的任何问题。我们随时乐意为您提供帮助。祝您使用仪表板愉快!