【Grafana】grafana-image-renderer配合python脚本实现仪表盘导出pdf

背景

  • os:centos7
  • Grafana:v12
  • grafana-image-renderer:v4.0.10
  • 插件:否

grafana-image-renderer可以以插件形式启动,也可以以单独服务启动,在centos7插件启动时,报错glibc版本太低,未找到Glibc_2.27,所以以单独服务启动。

一、安装Grafana

# 解压部署
tar zxvf grafana-12.1.0.linux-amd64.tar.gz -C /usr/local/
cd /usr/local/grafana-v12.1.0/
# 编辑配置文件
vi conf/defaults.ini
# 调试简单
[auth.anonymous]
enabled = true
# 配置rendering地址
[rendering]
server_url = http://192.168.113.138:8081/render
callback_url = http://192.168.113.138:3000/
# 启动服务
nohup bin/grafana server &

二、启动grafana-image-renderer

docker network create grafana
docker pull docker.1ms.run/grafana/grafana-image-renderer:v4.0.10
docker run --network grafana --name renderer -p 8081:8081 --rm --detach docker.1ms.run/grafana/grafana-image-renderer:v4.0.10

在这里插入图片描述

三、测试接口

直接访问:http://192.168.113.138:3000/render/d-solo/bdd48668-7073-4adc-b548-276133845e71?orgId=1&panelId=2&width=1000&height=500&from=now-24h&to=now
在这里插入图片描述

四、编写py脚本及效果

脚本由AI大模型生成。如果有能力可以修改Grafana源码,页面传入DASHBOARD_UID 即可实现点击下载。

import requests
import time
from typing import List, Dict
from PIL import Image, ImageDraw, ImageFont  # ✅ 确保包含 ImageFont
import img2pdf
from datetime import datetime
import os# ================== 配置区 ==================
GRAFANA_HOST = "http://192.168.113.138:3000"
API_KEY = "YOUR_API_KEY"  # Service Account Token 或 API Key
DASHBOARD_UID = "bdd48668-7073-4adc-b548-276133845e71"  # 替换为你的 Dashboard UID
OUTPUT_DIR = "panels_output"  # 输出目录
DELAY_BETWEEN_SHOTS = 1.0  # 每次截图间隔(秒),避免渲染服务压力过大
FROM = "now-24h"
TO = "now"
WIDTH = 1200
HEIGHT = 600
TIMEOUT = 30
# ==========================================
# 创建输出目录
os.makedirs(OUTPUT_DIR, exist_ok=True)
headers = {"Authorization": f"Bearer {API_KEY}","User-Agent": "Grafana-Report-Service/v1.0"
}def get_dashboard_json(uid: str) -> Dict:"""获取 Dashboard JSON 结构"""url = f"{GRAFANA_HOST}/api/dashboards/uid/{uid}"response = requests.get(url, headers=headers, timeout=10)if response.status_code != 200:raise Exception(f"获取 Dashboard 失败: {response.status_code} {response.text}")return response.json()def extract_panels(dashboard_data: Dict) -> List[Dict]:"""精准提取所有可渲染的 panel(排除 row、text 等不可截图类型)基于 Grafana v12 的 Dashboard JSON 结构"""panels = []dashboard = dashboard_data["dashboard"]skip_types = {"row", "text", "alertlist", "dashboard-link", "separator"}def is_renderable_panel(obj):# 必须是字典,有 id 和 typeif not isinstance(obj, dict):return Falseif "id" not in obj or "type" not in obj:return Falsepanel_type = obj["type"]# 排除不可渲染的类型if panel_type in skip_types:return False# 确保 id 是数字(真实的 panel id)if not isinstance(obj["id"], int):return Falsereturn Truedef walk(obj):if isinstance(obj, dict):# 如果当前对象是可渲染 panel,加入结果if is_renderable_panel(obj):title = obj.get("title") or f"Panel_{obj['id']}"panels.append({"id": obj["id"],"title": title.strip(),"type": obj["type"]})# 递归遍历所有值for value in obj.values():walk(value)elif isinstance(obj, list):for item in obj:walk(item)walk(dashboard)return panelsdef sanitize_filename(name: str) -> str:"""清理文件名,移除不合法字符"""return "".join(c if c.isalnum() or c in " _-." else "_" for c in name)def render_panel_to_png(panel_id: int, title: str, output_path: str) -> bool:"""渲染单个 panel 为 PNG"""slug = DASHBOARD_UIDrender_url = f"{GRAFANA_HOST}/render/d-solo/{DASHBOARD_UID}/{slug}"params = {"orgId": 1,"panelId": panel_id,"width": WIDTH,"height": HEIGHT,"tz": "Asia/Shanghai","from": FROM,"to": TO,"deviceScaleFactor": 1.5,"renderFormat": "png"}try:response = requests.get(render_url,params=params,headers=headers,timeout=TIMEOUT)if response.status_code == 200:with open(output_path, "wb") as f:f.write(response.content)print(f"✅ [{panel_id}] '{title}' → 保存成功: {output_path}")return Trueelse:print(f"❌ [{panel_id}] '{title}' → 渲染失败: {response.status_code}")print(f"   响应: {response.text[:200]}")return Falseexcept Exception as e:print(f"❌ [{panel_id}] '{title}' → 异常: {e}")return Falsedef generate_pdf_report(dashboard_title: str, panel_images: list, output_pdf: str):"""使用 img2pdf + Pillow 生成 PDF 报告panel_images: 图片文件路径列表(字符串)"""try:# ========== 1. 创建封面页 ==========try:# 尝试使用黑体(Windows 通常有 simhei.ttf)font_title = ImageFont.truetype("simhei.ttf", 60)font_text = ImageFont.truetype("simhei.ttf", 40)except:# 如果没有中文字体,用默认(可能显示乱码)font_title = ImageFont.load_default()font_text = ImageFont.load_default()# 使用与面板图像相同的宽度panel_width = WIDTH  # 从配置中获取宽度,默认为1200# 保持宽高比计算封面高度(使用A4纸的宽高比)cover_height = int(panel_width * 1.414)  # A4纸的宽高比约为1:1.414cover = Image.new('RGB', (panel_width, cover_height), 'white')draw = ImageDraw.Draw(cover)# 绘制标题title_pos = (panel_width // 2, cover_height // 3)  # 居中 X,Y=1/3处time_pos = (panel_width // 2, cover_height // 2)  # 居中 X,Y=1/2处# 获取文本边界框以居中bbox_title = draw.textbbox((0, 0), dashboard_title, font=font_title)title_width = bbox_title[2] - bbox_title[0]draw.text((panel_width // 2 - title_width // 2, cover_height // 3),dashboard_title, fill="black", font=font_title)generate_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")time_text = f"生成时间:{generate_time}"bbox_time = draw.textbbox((0, 0), time_text, font=font_text)time_width = bbox_time[2] - bbox_time[0]draw.text((panel_width // 2 - time_width // 2, cover_height // 2),time_text, fill="gray", font=font_text)# 保存封面cover_path = os.path.join(OUTPUT_DIR, "cover_page.png")cover.save(cover_path, "PNG")print(f"🎨 封面页已生成: {cover_path} (宽度: {panel_width}px)")# ========== 2. 构建图片路径列表 ==========# 确保所有 panel_images 都是字符串路径,且文件存在image_paths = [cover_path]  # 先加封面for img_path in panel_images:if isinstance(img_path, str) and os.path.exists(img_path):image_paths.append(img_path)else:print(f"⚠️ 跳过不存在的图片: {img_path}")if len(image_paths) < 2:print("❌ 没有足够的图片生成 PDF")return False# ========== 3. 转换为 PDF ==========with open(output_pdf, "wb") as f:f.write(img2pdf.convert(image_paths))print(f"📄 PDF 报告已生成: {output_pdf}")return Trueexcept Exception as e:print(f"❌ PDF 生成失败: {e}")import tracebacktraceback.print_exc()return Falsedef main():print(f"🔍 正在获取 Dashboard: {DASHBOARD_UID}")try:data = get_dashboard_json(DASHBOARD_UID)dashboard_title = data["dashboard"]["title"]print(f"📊 获取成功: '{dashboard_title}'")panels = extract_panels(data)print(f"📦 共找到 {len(panels)} 个可渲染的 panel\n")success_count = 0image_paths = []for i, panel in enumerate(panels, 1):title_clean = sanitize_filename(panel["title"])output_file = f"panel_{panel['id']}_{title_clean}.png"output_path = os.path.join(OUTPUT_DIR, output_file)print(f"🖼️  [{i}/{len(panels)}] 渲染中: {panel['title']}")if render_panel_to_png(panel["id"], panel["title"], output_path):success_count += 1image_paths.append(output_path)  # 收集成功生成的图片time.sleep(DELAY_BETWEEN_SHOTS)print(f"\n🎉 批量截图完成!成功: {success_count}/{len(panels)} 个")# ==== 生成 PDF ====if success_count > 0:output_pdf = f"report_{DASHBOARD_UID}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"generate_pdf_report(dashboard_title, sorted(image_paths), output_pdf)else:print("❌ 没有成功截图,跳过 PDF 生成")except Exception as e:print(f"💥 执行失败: {e}")if __name__ == "__main__":main()

Grafana可视化:
在这里插入图片描述

效果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/93874.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/93874.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

静/动态库 IIC(arm) day58

十七&#xff1a;动态库和静态库 库&#xff1a;一堆可执行二进制文件的集合&#xff0c;由若干个.o文件归并生成 一&#xff1a;静态(链接)库&#xff1a;libxxx.a 生成一个独立的可执行程序(运行时仅需要一个文件即可) 使用方便 不需要安装 文件比较大 多个程序使用同一个静态…

uniapp 手写签名组件开发全攻略

引言在移动应用开发中&#xff0c;手写签名功能是一个常见的需求&#xff0c;特别是在电子合同、审批流程、金融交易等场景中。本文将详细介绍如何基于uni-app框架开发一个高性能、功能丰富的手写签名组件&#xff0c;并分享开发过程中的技术要点和最佳实践。组件概述这个签名组…

理解JavaScript中的函数赋值和调用

&#x1f468; 作者简介&#xff1a;大家好&#xff0c;我是Taro&#xff0c;全栈领域创作者 ✒️ 个人主页&#xff1a;唐璜Taro &#x1f680; 支持我&#xff1a;点赞&#x1f44d;&#x1f4dd; 评论 ⭐️收藏 文章目录前言一、函数赋值二、函数调用三、 代码示例总结前言…

交叉编译 手动安装 SQLite 库 移植ARM

# 下载源码 wget https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz tar -xzf sqlite-autoconf-3420000.tar.gz cd sqlite-autoconf-3420000cd /home/lxh/sqlite-autoconf-3420000 make distclean //清除下&#xff0c;因为我安装失败过。 ./configure --hostarm-…

翻译记忆库(TMX)与机器翻译的结合应用

更多内容请见: 机器翻译修炼-专栏介绍和目录 文章目录 一、核心概念解析 1.1 翻译记忆库 (Translation Memory, TM) 1.2 翻译记忆交换格式 (Translation Memory eXchange, TMX) 二、为何要将两者结合? 2.1 TM和MT的优势是高度互补的 2.2 TMX在结合中的关键作用 2.3 TMX与MT的…

SpringBoot中集成eclipse.paho.client.mqttv3实现mqtt客户端并支持断线重连、线程池高并发改造、存储入库mqsql和redis示例业务流程,附资源下载

场景 SpringBoot整合MQTT服务器实现消息的发送与订阅(推送消息与接收推送)&#xff1a; SpringBoot整合MQTT服务器实现消息的发送与订阅(推送消息与接收推送)_服务端接收mqtt消息-CSDN博客 上面SpringBoot集成MQTT使用的是spring-integration-mqtt依赖&#xff0c;也是经常使…

【考研408数据结构-08】 图论基础:存储结构与遍历算法

&#x1f4da; 【考研408数据结构-08】 图论基础&#xff1a;存储结构与遍历算法 &#x1f3af; 考频&#xff1a;⭐⭐⭐⭐⭐ | 题型&#xff1a;选择题、综合应用题、算法设计题 | 分值&#xff1a;约8-15分 引言 想象你正在规划一次跨省自驾游&#xff0c;面前摊开一张复杂的…

SQL查询语句的执行顺序

好的&#xff0c;我们来详细讲解一下 SQL 查询语句的执行顺序。 很多人会误以为 SQL 的执行顺序就是我们写的顺序&#xff08;SELECT -> FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY&#xff09;&#xff0c;但实际上&#xff0c;数据库引擎在底层处理查询…

【Android】OKHttp网络请求原理和弱网优化

【Android】OKHttp网络请求原理和弱网优化 1. OkHttp 网络请求原理 OkHttp 的请求过程可以分为 四个关键阶段&#xff1a; &#xff08;假设你是通过 OkHttpClient.newCall(request).enqueue(callback) 发的请求&#xff09; OkHttpClient│▼ Dispatcher (调度器)│▼ RealC…

概率论基础教程第4章 随机变量(四)

4.7 泊松随机变量 定义 泊松随机变量&#xff1a;如果一个取值于 $ 0, 1, 2, \ldots $ 的随机变量对某一个 $ \lambda > 0 $&#xff0c;其分布列为&#xff1a; p(i)P{Xi}e−λλii!i0,1,2,⋯(7.1) \boxed{p(i) P\{X i\} e^{-\lambda} \frac{\lambda^i}{i!} \qquad i 0…

Unity高级开发:反射原理深入解析与实践指南 C#

Unity高级开发&#xff1a;反射原理深入解析与实践指南 在Unity游戏开发中&#xff0c;反射&#xff08;Reflection&#xff09; 是一项强大的元编程技术&#xff0c;它允许程序在运行时动态地获取类型信息、创建对象和调用方法。根据Unity官方统计&#xff0c;超过78%的商业游…

任务五 推荐页面功能开发

一、推荐页面需求分析 由推荐页面效果图,可以看出,推荐页面主要由顶部轮播图和歌单列表页面组成 二、推荐页面轮播图组件封装 由于轮播图,可能在项目多个地方用到,因此可以将轮播图抽调成一个组件,然后各个页面调用这个组件。 在开发轮播图组件时,需要安装better-scro…

【工具使用-Docker容器】构建自己的镜像和容器

1. 镜像和容器介绍 镜像&#xff08;Image&#xff09;是一个只读的模板&#xff0c;包含了运行某个应用所需的全部内容&#xff0c;比如&#xff1a; 操作系统&#xff08;比如 Ubuntu&#xff09;应用程序代码运行环境&#xff08;如 Python、Java、Node.js 等&#xff09;库…

Apache Shiro550 漏洞(CVE-2016-4437):原理剖析与实战 SOP

在 Web 安全领域&#xff0c;反序列化漏洞一直是威胁等级极高的存在&#xff0c;而 Apache Shiro 框架中的 Shiro550 漏洞&#xff08;CVE-2016-4437&#xff09;&#xff0c;更是因利用门槛低、影响范围广&#xff0c;成为渗透测试中频繁遇到的经典漏洞。本文将从 “原理拆解”…

安卓开发者自学鸿蒙开发3持久化/数据与UI绑定

AppStorage,PersistentStorage与StorageLink AppStorage是应用全局状态管理器,数据存储于内存中,常见的如全局的黑暗模式,StorageLink是用来绑定AppStorage的键到ui上的工具,省去了用户手写代码的无聊过程,PersistentStorage可以绑定AppStorage的键,自动持久化到磁盘,同时支持多…

GitHub宕机生存指南:从应急协作到高可用架构设计

GitHub宕机生存指南&#xff1a;从应急协作到高可用架构设计 摘要&#xff1a; GitHub作为全球开发者的协作中心&#xff0c;其服务稳定性至关重要。然而&#xff0c;任何在线服务都无法保证100%的可用性。本文深入探讨了当GitHub意外宕机时&#xff0c;开发团队应如何应对。我…

机器学习算法篇(十三)------词向量转化的算法思想详解与基于词向量转换的文本数据处理的好评差评分类实战(NPL基础实战)

目录 一、词向量原理介绍 (1). 词向量的核心概念 (2). 传统文本表示的局限性 1. 独热编码&#xff08;One-Hot Encoding&#xff09; 2. 词袋模型&#xff08;Bag of Words&#xff09; 3. TF-IDF (3). 词向量的核心原理 (4). 主流词向量模型 1. Word2Vec&#xff08;20…

JS自定义函数(2)

1. 变量的作用域全局变量定义&#xff1a;在函数外声明的变量作用范围&#xff1a;在整个JS文档中生效生命周期&#xff1a;页面关闭时销毁局部变量定义&#xff1a;在函数内用 var 声明的变量作用范围&#xff1a;只能在函数内部使用生命周期&#xff1a;函数执行完毕时销毁作…

【数据集】Argoverse 数据集:自动驾驶研究的强大基石

Argoverse数据集&#xff1a;自动驾驶研究的强大基石 在自动驾驶技术蓬勃发展的当下&#xff0c;高质量的数据集对于推动相关算法研究和模型训练起着举足轻重的作用。Argoverse 数据集便是其中的佼佼者&#xff0c;它为自动驾驶领域的众多任务提供了丰富且优质的数据资源。 一、…

--- 哈希表和哈希冲突 ---

哈希&#xff08;散列&#xff09;方法是对插入的数据通过哈希函数计算出一个哈希地值&#xff0c;并将这个哈希地址作为储存改数据的地址&#xff0c;这样下次再查找这个数据时&#xff0c;只需要通过哈希函数再获取到该地址然后直接去拿就好这样就做到了不经过任何比较&#…