用Grafana和Echarts实现可嵌入使用的iframe图表

用Grafana和Echarts实现可嵌入使用的iframe图表

HomeServer仪表盘新欢——Glance文章中提到在Glance中嵌入使用Grafana图表。这一篇详细说一下Grafana图表的相关配置。

曾经在Dashy和Baserow中分别用ChartJS和Echarts实现过一些数据的可视化。由于仪表盘迁移至Glance,Dashy中的图表不再可用。而因个人用途在Baserow中专门开发插件又无性价比1,且不可复用。因此使用Grafana中的Echarts(Business Charts)插件曲线救国2,生成图表后用iframe插入其它工具中,虽然性能差了一些,但好在不需要花费太多时间就能实现,工具直接相互解耦,复用和迁移都非常方便。

Grafana数据源 #

个人常用的数据源插件如下,基本能覆盖个人服务器的使用场景了,对于有Restful API的应用,用Infinity可以很方便地解决数据获取的问题,比如获取Baserow的数据;SQLite可以读取本地sqlite数据库文件,比如我自己写的少数派用户数据;WebSocket API用于实时数据更新,比如获取Openwrt Clash Dashboard面板数据。灵活使用浏览器的开发者工具(ctrl+shift+c)可以抓取分析网站请求,进而自由地将数据输入至Grafana。

  • Infinity查询和可视化来自 JSON、CSV、GraphQL、XML 和 HTML 端点的数据;
  • SQLite使用本地 SQLite 数据库作为数据源;
  • WebSocket API WebSocket 数据源插件,用于实时数据更新;

安装也很便捷,只需在Grafana的插件面板中搜索点击安装,重启Grafana即可,或根据插件主页指引进行安装。

Business Charts (ECharts)插件 #

Grafana 在做监控仪表盘这类场景里很强大,但在图表的美观性和交互体验上还是有些欠缺。默认的图表样式比较“工程化”,颜色和排版没那么精致。图表交互也比较基础,很多时候就是看看趋势,无法做数据下钻、动态筛选之类的操作。

Grafana很“工程”的样式 Grafana很“工程”的样式

Business Charts允许我们将 Apache ECharts创建的图表和图形集成到 Grafana 仪表板中。极大地扩展了Grafana的图表功能。在Business Charts Demo上可以直接预览支持的图表类型。在小组件的右上角的展开设置,可以直接复制右边栏目的Code的内容至自己的Grafana面板,供本地测试。

复制使用官方用例 复制使用官方用例

使用Theme Builder - Apache ECharts可方便地自定义主题,在Theme Builder的Basic-Download-JSON version窗口直接复制主题配置,黏贴至Grafana面板的Theme代码框内,ctrl-s即可生效。

配置主题 配置主题

具体例子,使用Baserow作为数据源生成旭日图 #

Business Charts的demo中使用的都是静态数据。这里举一个例子,演示如何将Baserow多维表格的数据转化为Grafana的旭日图。用到的服务有Baserow,Grafana,Grafana插件有InfinityBusiness Charts

  • 在Baserow中,有如下表格,显示了多肉品种名称、科、属,两个列的类别可以设置为Single Select。此时,浏览器地址应为http://{BASEROW_SITE}/database/{database_id}/table/{table_id}/{view_id},复制备用;
  • 在Baserow中,通过My settings-Database tokens生成访问Token并赋予read权限,复制Token备用;
Name
条纹十二卷 阿福花科(Asphodelaceae) 十二卷属(Haworthia)
黄丽 景天科(Crassulaceae) 景天属(Sedum)
黄金草 景天科(Crassulaceae) 景天属(Sedum)
枝干兔子 番杏科(Aizoaceae) 枝干番杏属(Mitrophyllum)
圆叶洋葵 牻牛儿苗科(Geraniaceae) 天竺葵属(Pelargonium)
宽叶不死鸟 景天科(Crassulaceae) 落地生根属(Bryophyllum)
熊童子 景天科(Crassulaceae) 银波锦属(Cotyledon)
生石花 番杏科(Aizoaceae) 生石花属(Lithops)
蓝龙 景天科(Crassulaceae) 拟石莲花属(Echeveria)
草玉露 阿福花科(Asphodelaceae) 十二卷属(Haworthia)
吸财树 景天科(Crassulaceae) 青锁龙属(Crassula)
樱水晶 阿福花科(Asphodelaceae) 十二卷属(Haworthia)
桃蛋 景天科(Crassulaceae) 风车草属(Graptopetalum)
大和锦 景天科(Crassulaceae) 拟石莲花属(Echeveria)
照波 番杏科(Aizoaceae) 照波属(Bergeranthus)
情人泪 菊科(Asteraceae) 千里光属(Senecio)
钱串 景天科(Crassulaceae) 青锁龙属(Crassula)
龟甲龙 薯蓣科(Dioscoreaceae) 龟甲龙属(Dioscorea)
  • 在Grafana中,新建数据源,选择Infinity,分别点击API Key Value pair并设置KeyAuthorizationValueToken YOUR_BASEROW_TOKENallowed hosts为baserow的url,保存;

  • 在Grafana中,任意仪表盘内点击添加-可视化进入编辑界面,在数据源中,选择刚才保存的Infinity数据源,Format改为Data FrameURL设置为http://{BASEROW_SITE}/api/database/rows/table/{table_id}/?user_field_names=true,此时,在Table view窗口就会显示获取的baserow数据了;

  • 在Grafana编辑界面,如上图步骤依次设置,点击Code-Function的编辑框,填入以下代码,ctrl+s保存,在预览框反勾选Table view,旭日图应该会正确显示了;

function transformPlantData(plants) {
  const familyMap = new Map();
  plants.forEach(plant => {
    if (!plant. || !plant.) return;
    const family = {
      ...plant.,
      value: plant..value.replace(/([^)]*)/, '')
    };
    const genus = {
      ...plant.,
      value: plant..value.replace(/([^)]*)/, '')
    };
    const name = plant.Name;
    if (!familyMap.has(family.value)) {
      familyMap.set(family.value, {
        name: family.value,
        children: new Map()
      });
    }
    const familyNode = familyMap.get(family.value);
    if (!familyNode.children.has(genus.value)) {
      familyNode.children.set(genus.value, {
        name: genus.value,
        children: []
      });
    }
    const genusNode = familyNode.children.get(genus.value);
    genusNode.children.push({
      name: name,
      value: 1,
    });
  });
  return Array.from(familyMap.values()).map(family => ({
    ...family,
    children: Array.from(family.children.values()).map(genus => ({
      ...genus,
      children: genus.children
    }))
  }));
}
context.panel.data.series.map((s) => {
  results = s.fields.find((f) => f.name === "results").values;
});
const value = transformPlantData(JSON.parse(results[0]));

return {
  series: {
    type: 'sunburst',
    data: value,
    radius: [0, '95%'],
    sort: undefined,
    emphasis: {
      focus: 'ancestor'
    },
    levels: [
      {},
      {
        r0: '8%',
        r: '45%',
        itemStyle: {
          borderWidth: 2
        },
      },
      {
        r0: '45%',
        r: '75%',
        label: {
          align: 'right'
        }
      },
      {
        r0: '75%',
        r: '78%',
        label: {
          position: 'outside',
          padding: 2,
          color: '#aabbc3',
        },
      }
    ]
  }
};
  • Theme Builder - Apache ECharts中任意选择复制主题代码至Theme-Configuration代码框中,ctrl+s保存生效;
  • 在编辑界面右上角点击Save dashboard保存,退回仪表盘面板,点击组件右上角展开选项,选择分享-Share embeded即可复制iframe链接使用了。

以下是嵌入Glance和Obsidian中的效果图。

Glance页面 Glance页面

Obsidian页面 Obsidian页面

Code编辑模式的补充说明 #

Business Charts提供了Visual模式,只需要在UI界面上点选即可完成图表的构建,目前支持的图表类型只有Bar,Boxplot,Line,Radar,Scatter,Sunburst,且无法深度定义图表样式,因此还是推荐使用Code模式进行编辑。参照官方教程进行编辑即可Volkov Labs

其实LLM可以很好的完成编辑工作,只需提供例子、数据格式即可。提示词可以这样写:

这是grafana的echart插件的官方用例{Volkov Demo},现在我有一个数据源{你的数据},帮我生成折线图的代码。

我们也可以先使用如下代码获取"results"的数据,并用console.log(results)将结果打印出来,并提供给LLM,console.log(results)的结果可以在浏览器的开发者工具ctrl+shift+c中的控制台标签页查看。

context.panel.data.series.map((s) => {
  results = s.fields.find((f) => f.name === "results").values;
});
console.log(results)

Grafana的iframe的必要配置 #

为了确保Grafana图表作为iframe组件美观而安全地嵌入其它网站,需要做背景更改和访问权限的配置。

更改背景颜色 #

无论浅色还是声色背景,grafana图标作为iframe组件嵌入其它网站时都无法做到背景一致融合,考虑到复用性,我们直接将背景设为透明,步骤如下。

  • docker cp grafana:/usr/share/grafana/public/views/index.html ./
  • <style>下添加
  :root{
	color-scheme: light dark !important;
  }
  body,
  .dashboard-container,
  .panel-content,
  .panel-container,
  .panel-solo,
  .dashboard-solo{
	background: none !important;
	background-color: none !important;
  }
  • docker cp index.html grafana:/usr/share/grafana/public/views/

访问权限 #

在默认设置下,grafana图表作为iframe小组件嵌入其它网站时会无权访问,要求登录,有以下两种方法设置权限。其中第一种仅供调试使用。

方法一,匿名免登录访问(不推荐) #

这种方法直接允许匿名访问,通过device_limit设置允许访问设备数量,风险在于任何知道grafana url地址的人都可以任意访问仪表盘

  • docker cp grafana:/etc/grafana/grafana.ini ./
# /etc/grafana/grafana.ini
[auth.anonymous]
# enable anonymous access
enabled = true
# specify organization name that should be used for unauthenticated users
org_name = Main Org.
# specify role for unauthenticated users
org_role = Viewer
# mask the Grafana version number for unauthenticated users
hide_version = false
# number of devices in total
device_limit = 5
  • docker cp grafana.ini grafana:/etc/grafana/

方法二,jwt免登录访问 #

# 更改 grafana.ini
[security]
allow_embedding = true

[auth.jwt]
enabled = true
enable_login_token = true
header_name = X-Forwarded-Access-Token
username_claim = sub
jwk_set_file = /var/lib/grafana/jwks-public.json
role_attribute_path = role
auto_sign_up = true
url_login = true
  • 访问 https://mkjwk.org/ ,参考如下值,keyId自行填写。将中间部分(含public、private的)保存为jwks.json文件,用于生成token。右边的public key保存为jwks-public.json,用于对token进行认证。注意外围也要加上{"keys": [ ]},否则grafana会找不到keys。

  • docker cp jwks-public.json grafana:/var/lib/grafana/
  • 使用如下程序生成用户名为anonymous,角色为Viewer,有效期为365天的token
import json
from datetime import datetime, timedelta, timezone
import jwt
from authlib.jose import JsonWebKey

class JwtGenerator:
    def __init__(self, jwks_path):
        self.jwks = self._load_jwks(jwks_path)
        self.private_key, self.key_id = self._get_private_key_and_kid()

    def _load_jwks(self, jwks_path):
        with open(jwks_path, 'r') as f:
            return json.load(f)

    def _get_private_key_and_kid(self):
        key_data = self.jwks['keys'][0]
        key_id = key_data.get('kid')
        jwk = JsonWebKey.import_key(key_data)
        return jwk.get_private_key(), key_id

    def get_jwt(self):
        now = datetime.now(timezone.utc)
        expiration = now + timedelta(days=365)
        claims = {
            "sub": "anonymous",
            "exp": int(expiration.timestamp()),
            "nbf": int(now.timestamp()),
            "iat": int(now.timestamp()),
            "role": "Viewer",
        }
        token = jwt.encode(
            claims,
            self.private_key,
            algorithm='RS256',
            headers={
                'typ': 'JWT',
                'kid': self.key_id
            }
        )
        return token
    
if __name__ == "__main__":
    generator = JwtGenerator("/path/to/jwks.json")
    print(generator.get_jwt())
  • url-token访问 http://grafana:33303/datasources?auth_token=YOUR_TOKEN
  • header访问 headers携带X-Forwarded-Access-Token的token,内容为:Bearer YOUR_TOKEN

参考资料。345


  1. Baserow官方正在开发图表功能 ↩︎

  2. 也尝试过GitHub - chartbrew,用的ChartJS库,支持图表类型有限,样式无法深度自定义,但优点在于UI设计的好,数据导入非常便捷,能很快地做出一个图表 ↩︎

  3. grafana第三方集成以及使用jwt(jwks)实现免登录访问 - 且炼时光 ↩︎

  4. 配置 JWT 身份验证 | Grafana 文档 ↩︎

  5. grafana/grafana-iframe-oauth-sample ↩︎

前一篇 HomeServer... 随机阅读