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

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插件有Infinity
,Business 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
并设置Key
为Authorization
,Value
为Token YOUR_BASEROW_TOKEN
,allowed hosts
为baserow的url,保存; -
在Grafana中,任意仪表盘内点击
添加
-可视化
进入编辑界面,在数据源中,选择刚才保存的Infinity数据源,Format
改为Data Frame
,URL
设置为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中的效果图。


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
-
Baserow官方正在开发图表功能 ↩︎
-
也尝试过GitHub - chartbrew,用的ChartJS库,支持图表类型有限,样式无法深度自定义,但优点在于UI设计的好,数据导入非常便捷,能很快地做出一个图表 ↩︎