Featured image of post 通过API创建Grafana快照并进行iframe嵌入

通过API创建Grafana快照并进行iframe嵌入

通过SnapshotAPI创建Grafana快照并不如想象中的简单, JSON model的数据需要我们自己进行查询与填充,所以我最后使用了无头浏览器自动化的方式来完成这个需求, 本文记录了探索的过程, 并提供了一个浏览器自动化方案的简易实现.

先说结论, Grafana snapshot api 当前还是主要供 GrafanaUI 来调用的, 调用这个API时 Grafana 并不会如我们期望的那样创建一个包含了所有数据的快照, 实际上我们如果想通过 HTTP API 来创建快照, 我们需要先执行一系列的 Datasource query 查询请求, 得到每一个 Panel 的数据后再将这些数据填充入 Dashboard 的 JSON Model, 非常的繁琐. 因此我最终还是选择使用无头浏览器自动化的方式来实现创建快照的需求, 不关心过程的话可以直接跳到文章的最后或者访问 grafana-snapshot-exporter, 对于代码有什么问题或者有什么建议的话欢迎在github上提issue.

最近面临一个需求,在用户面板中通过 Grafana 向用户展示系统的一些状态,并且尽量让用户无需在网页上二次登录就能使用。在最开始的时候,我本想通过在面板中嵌入带有鉴权参数的 iframe url 来实现这个需求。但是经过搜索,发现这个并不如想象中那么容易实现。因为目前grafana目前不支持也暂不考虑支持"在应用程序中嵌入受身份验证保护的仪表板 embedding authentication-protected dashboards in applications. We will get into why that isn't supported in the security section.", 他们还写了一篇文章来阐述这个问题 How to embed Grafana dashboards into web applications。我们希望一定程度上限制 dashboard 的可见性,因此也不能直接采用 public dashbord 的形式,但是使用 OAuth 对于我们这个需求来说又有些过于复杂了,除此之外没有其他直接嵌入一个功能完整的 dashbord 了。考虑到我们需要展示的数据更接近于一段时间内的汇总,并不需要用户能够自定义各种查询的逻辑,因此权衡之下,我决定退而求其次,通过grafana的snapshot(快照)机制来实现这个需求。

在 Grafana 的 dashboard 中,点击页面头部的分享按钮,便可以看见 Grafana 为我们提供的一些分享方式,其中就包括了 snapshot 的方式,网页中的介绍如下:

A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries (metric, template, and annotation) and panel links, leaving only the visible metric data and series names embedded in your dashboard. Keep in mind, your snapshot can be viewed by anyone that has the link and can access the URL. Share wisely.

需要注意的一点是,snapshot 的 URL 是任何人都可以访问的,不过 URL 本身比较复杂包含了一个较长的随机字符串,而我们要暴露的数据本身也不是太敏感,且我们创建快照时都会指定有效期,因此这个问题不是太大。不过在浏览器中直接创建快照时我遇到了一个问题——“快照只有处于浏览器可视窗口之内的部分是有数据的,其他的部分都没有数据”, 并且暂时也没找到这个问题的解决方法,不过好在我们是通过 API 的方式来创建快照,而经过验证,通过 API 创建的快照是完整的,不会出现上述问题。

通过API创建快照

要通过 API 的方式访问 Grafana,我们首先需要创建服务账号以及获取对应的Key,在 Grafana 的 HOME > Administration > Users and access > Service accounts 中,我们可以创建一个服务账号,接着为该账号添加 account token, 我们便能获取到调用 API 所需的凭证了。

创建服务账号

创建服务账号, 添加key

通过如下的API可以创建出一个快照:

1
2
3
4
5
6
7
8
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer <Your-API-Key>" -d '{
  "name": "snapshot-1",
  "external": false,
  "dashboard": {
    "dashboard model": {}  // 这里应该是你的仪表板模型
  },
  "expires": 3600
}' http://<Your-Grafana-Host>/api/snapshots

我们注意到这里有一个名为 “dashboard” 的属性,在实际使用中这个JSON对象就是我们目标面板的结构,在对应的 dashboard 中点击 Settings > JSON Model 便可以看到,下面是一个只有标题与一个名为group的custom参数的面板结构。

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "grafana",
          "uid": "-- Grafana --"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "id": 12,
  "links": [],
  "liveNow": false,
  "panels": [],
  "refresh": "",
  "schemaVersion": 39,
  "tags": [],
  "templating": {
      "list": [
        {
          "current": {
            "selected": false,
            "text": "test",
            "value": "test"
          },
          "hide": 0,
          "includeAll": false,
          "label": "group",
          "multi": false,
          "name": "group",
          "options": [],
          "query": "",
          "queryValue": "test",
          "skipUrlSync": false,
          "type": "custom"
        }
      ]
  },
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "用户面板数据",
  "uid": "b05cf7ef-3094-4192-9471-80e6b403b2d7",
  "version": 2,
  "weekStart": ""
}

我们只需要将这个JSON对象作为 “dashboard” 的值替换即可,后续我们基本只需要修改time以及对应的参数值(上面为名为group的custom参数),便可创建出我们想要的快照了,另外需要注意的一点就是,要通过iframe嵌入grafana,需要将修改granafa的配置.

1
2
[security]
allow_embedding = true

然而,问题并没有那么简单

起初我通过上面的步骤创建了快照,并且在浏览器中也能正常访问了,但是当我通过浏览器的访客模式来访问这个快照时,发现和浏览器里面创建的快照不一样,这个快照还会尝试从Datasource(我这是Prometheus)获取数据,而快照当然没有权限获取数据,于是就出现了 Datasource is not found 的错误。这时我才注意到官网的 HTTP Snapshot API 中提到 When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI., 看来我们还不能指望 Grafana 的后端服务帮我们搞定快照创建时的数据获取问题,我们还得自己逐一去获取所有图标的数据并填充到快照的JSON model中——工作量又增加了不少。于是我们只能在我们的后端程序中模拟浏览器进行的操作来创建快照,分为下面几个步骤:

  1. 使用Dashboard API GET /api/dashboards/uid/:uid 获取目标dashboard的JSON model,当然你依旧可以可以直接将JSON model编码在程序中,不过我想既然后续也得进行一系列的查询,那么干脆就直接从grafana获取吧,这样修改面板的时候也不需要再对程序进行修改。
  2. 遍历获取到的dashboard的panels,对于每一个panel,调用 Datasource API POST /api/ds/query,获取到实际的数据。
  3. 将获取到的数据填充到dashboard的JSON model中,再调用Dashboard API创建快照。

f12

在浏览器中,打开开发者工具的情况下创建快照我们也能看见此时进行的一系列请求,以及最终的snapshot POST请求。可以以这些请求作为参考来进行上面的操作。

Grafana论坛中的 snapshot-using-http-api-does-nothing 也对这个问题进行了较多的讨论。

更简易的实现方式——无头浏览器

也许完全通过调用API来创建一个快照目前还是太麻烦了,论坛中也提供了另一种实现方式——使用浏览器自动化的方式来创建快照,这样的话我们就不太需要关心数据的结构与获取问题了,我也实现了一个简易的版本,使用了golang的chromedp库。

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/chromedp/chromedp"
)

const (
	GrafanaURL      = "https://example.com"
	GrafanaUserName = "auto"
	GrafanaPassword = "password"
)

func main() {
	// create chrome instance
	opts := append(
		chromedp.DefaultExecAllocatorOptions[:],
		chromedp.WindowSize(1920, 1080),
		chromedp.Flag("headless", false), // for debug
	)
	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
	defer cancel()
	ctx, _ := chromedp.NewContext(
		allocCtx,
		// chromedp.WithDebugf(log.Printf),
	)
	ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
	defer cancel()

	// get grafana session
	if err := chromedp.Run(ctx,
		loginGrafana(GrafanaUserName, GrafanaPassword),
	); err != nil {
		fmt.Printf("loginGrafana err: %s\n", err)
	}
	snapshotURL, err := CreateSnapshot(ctx, "b05cf7ef-3094-4192-9471-xxxxxxxxx", "orgId=1&var-user=test-user", 1710172800000, 1710259199000)
	if err != nil {
		fmt.Printf("CreateSnapshot err: %s\n", err)
	}
	fmt.Printf("snapshotURL: %s\n", snapshotURL)
}

func CreateSnapshot(ctx context.Context, dashboardId, query string, from, to int) (string, error) {
	var snapshotURL string
	if err := chromedp.Run(ctx,
		createSnapshot(dashboardId, query, from, to),
		chromedp.Value(`#snapshot-url-input`, &snapshotURL),
	); err != nil {
		return "", err
	}
	return snapshotURL, nil
}

func loginGrafana(username, password string) chromedp.Tasks {
	return chromedp.Tasks{
		chromedp.EmulateViewport(1920, 1080),
		chromedp.Navigate(fmt.Sprintf("%s/login", GrafanaURL)),
		chromedp.WaitVisible(`input[name='user']`),
		chromedp.SendKeys(`input[name='user']`, username),
		chromedp.SendKeys(`input[name='password']`, password),
		chromedp.Click(`button[type='submit']`),
		chromedp.WaitReady(`.page-dashboard`),
	}
}

func createSnapshot(dashboardId, query string, from, to int) chromedp.Tasks {
	return chromedp.Tasks{
		chromedp.Navigate(fmt.Sprintf("%s/d/%s/?from=%d&to=%d&%s", GrafanaURL, dashboardId, from, to, query)),
		chromedp.WaitVisible(`.page-dashboard`),
		chromedp.WaitVisible(`div[aria-label='Panel loading bar']`),    // wait for all panel loaded (for debug
		chromedp.WaitNotPresent(`div[aria-label='Panel loading bar']`), // wait for all panel loaded
		chromedp.Click(`button[aria-label='Share dashboard']`),
		chromedp.Click(`button[aria-label='Tab Snapshot']`),
		chromedp.Click(`.css-1i88p6p`),
		chromedp.WaitVisible(`#react-select-2-listbox`),
		chromedp.Click(`#react-select-2-option-1`), // choose 1 hour expire
		chromedp.Click(`//button[span[text()='Local Snapshot']]`, chromedp.BySearch),
		chromedp.WaitVisible(`#snapshot-url-input`),
	}
}

之后我实现了一个更加完整一些的版本,可以通过Web服务来创建Grafana的快照,详情可以查看 grafana-snapshot-exporter, 可以通过docker直接运行,也可以直接调用readme中提到的demo api来为你的面板创建快照(担心安全的话还是自己部署或者使用权限较低的账号).

Licensed under CC BY-NC-SA 4.0
最后更新于 Mar 13, 2024 19:56 +0800
comments powered by Disqus
本站访客数:
Built with Hugo
主题 StackJimmy 设计