1. 从改个面板到动源码这次二次开发到底要解决什么Grafana 用久了几乎每个团队都会走到同一个岔路口标准面板不够用了。不是缺个图表类型就是缺个交互逻辑再不然就是数据源接入方式跟自家系统对不上。我这次接到的需求说起来也不复杂——要在现有 Grafana 里塞进一套自定义的可视化组件同时让面板支持一种内部特有的数据格式还得保证升级 Grafana 版本时改动不会全废。关键词就三个Grafana 二次开发、自定义面板插件、数据源适配。摘要一句话概括在不 fork 整个 Grafana 主仓库的前提下通过插件机制和少量前端注入实现一套贴合内部业务的可视化扩展。先泼一盆冷水。很多人一听二次开发第一反应是 clone 官方仓库改源码重新编译。这条路我走过也劝你别轻易走。Grafana 的前端代码量非常大Angular 和 React 混用的历史包袱还在构建链路又长你改一个组件可能要等十几分钟才能看到效果。更致命的是一旦你 fork 了主仓库后续官方每次发版你都要手动 merge冲突多到让人怀疑人生。所以这次我定的策略很明确能用插件解决的绝不碰主仓库必须碰主仓库的用最小侵入的方式打补丁。这个原则贯穿了整个开发过程也是后面所有技术选型的判断依据。适合看这篇内容的人大概是这样几类已经会用 Grafana 做基础看板、想进一步做定制的运维或前端同学被标准功能不够用卡住、正在评估二次开发成本的技术负责人以及单纯好奇 Grafana 插件体系是怎么跑起来、想动手试一把的开发者。不需要你是 Grafana 源码专家但至少得跑过一次本地实例知道面板、数据源、插件这几个概念大概指什么。下面我会按真实的开发顺序来讲先搞清楚 Grafana 的扩展点在哪再决定用什么姿势下手然后是环境搭建、插件开发、数据源适配、调试排错最后聊聊升级兼容这个绕不开的坑。每一步我都会说清楚为什么这么做而不是只丢一堆命令给你。2. 先摸清 Grafana 的扩展边界哪些能改哪些别碰动手之前花半天时间把 Grafana 的扩展机制摸清楚能帮你省掉后面好几天的返工。我见过太多人上来就改代码改到一半发现方向错了推倒重来。2.1 三种扩展方式的能力边界对比Grafana 对外提供的扩展能力大致可以分成三层能力从弱到强侵入性也从低到高扩展方式能做什么侵入性升级兼容性适用场景数据源插件接入自定义后端数据无极好内部系统数据接入面板插件自定义可视化组件无极好特殊图表、交互逻辑前端注入/源码补丁改 UI、改交互流程高差插件机制覆盖不到的地方我的建议是优先数据源插件其次面板插件最后才考虑源码补丁。这次的需求里数据格式适配用数据源插件解决可视化组件用面板插件解决只有一处全局的导航栏定制插件机制确实覆盖不到才用了前端注入。2.2 为什么插件机制是二次开发的首选插件机制最大的价值在于它把你的代码和Grafana 的代码彻底隔离开了。你的插件是一个独立的包Grafana 在运行时动态加载它。官方升级 Grafana只要插件的 API 契约没变你的插件基本不用动。这里有个关键概念叫Grafana Plugin SDK它定义了插件和宿主之间的接口。面板插件要实现的核心接口包括PanelPlugin、PanelModel这些数据源插件要实现DataSourceApi那一套。你只要按接口写宿主就能正确调用你。提示插件 API 在不同 Grafana 大版本之间会有变化尤其是从 7.x 到 8.x、9.x 这段前端框架从 Angular 往 React 迁移很多老 API 被标记废弃。选版本时一定要先查官方文档里对应版本的插件开发指南别照着三年前的教程写。2.3 源码补丁的代价我踩过的 merge 地狱说个真实教训。早期有个项目为了改一个面板的默认行为直接改了主仓库里对应的组件文件。当时跑得好好的结果官方发了个小版本那个文件被重构了我们的改动全丢还得重新定位、重新改。更麻烦的是团队里没人记得当初为什么这么改注释也没写清楚排查花了整整两天。从那以后我给自己定了规矩任何对主仓库的改动必须记录在案必须能说清楚为什么插件机制做不到。如果说不清楚那就说明你还没找对扩展点回去继续找。3. 环境搭建把开发链路先跑通再谈功能功能还没开始写环境先折腾了大半天这是常态。Grafana 插件开发的环境有几个容易卡住的点我一个个说。3.1 本地 Grafana 实例的两种跑法跑本地实例有两条路一是直接下载官方二进制包解压运行二是用容器跑。我推荐容器方式原因是干净、可复现、删了不留痕。docker run -d \ --name grafana-dev \ -p 3000:3000 \ -v $(pwd)/plugins:/var/lib/grafana/plugins \ -e GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINSmy-custom-panel \ grafana/grafana:latest这里有两个关键点。第一-v把本地插件目录挂进容器这样你改完插件代码重启容器就能加载新版本不用重新构建镜像。第二GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS这个环境变量必须设否则 Grafana 默认拒绝加载未签名的插件你会看到插件列表里空空如也还以为是代码问题。注意这个环境变量只在开发环境用。生产环境加载未签名插件有安全风险正式发布时应该走签名流程或者至少确保插件来源可信。3.2 插件脚手架的生成与目录结构官方提供了脚手架工具能帮你生成一个符合规范的插件骨架。我建议用它起步别自己从零建目录容易漏掉plugin.json里的必填字段。npx grafana/create-pluginlatest生成出来的目录结构大概是这样my-custom-panel/ ├── src/ │ ├── module.ts # 插件入口导出 PanelPlugin │ ├── plugin.json # 插件元信息 │ └── components/ │ └── SimplePanel.tsx ├── package.json └── tsconfig.jsonplugin.json是插件的身份证里面的id、type、name这些字段决定了 Grafana 怎么识别和展示你的插件。type字段尤其重要写panel就是面板插件写datasource就是数据源插件写错了加载直接失败。3.3 构建与热加载的取舍脚手架默认用的是 webpack 或者 vite 构建。开发阶段我强烈建议开启 watch 模式改完代码自动重新打包配合容器的挂载目录刷新页面就能看到效果。npm run dev但这里有个坑Grafana 对插件的加载是启动时扫描的你重新打包了插件文件Grafana 不一定能感知到。稳妥的做法是改完代码后重启一下 Grafana 容器或者用 Grafana 提供的插件重载接口。我一般图省事直接docker restart grafana-dev几秒钟的事比排查为什么改了没生效划算得多。4. 自定义面板插件从接口契约到渲染落地环境通了开始写真正的功能。面板插件是这次的重头戏我把它拆成几个关键环节来讲。4.1 PanelPlugin 的注册与生命周期面板插件的入口在module.ts核心就是导出一个PanelPlugin实例import { PanelPlugin } from grafana/data; import { SimplePanel } from ./components/SimplePanel; export const plugin new PanelPlugin(SimplePanel) .setPanelOptions((builder) { builder.addTextInput({ path: labelText, name: 标签文字, defaultValue: 默认标签, }); });setPanelOptions这段是给面板配置面板用的。你在 Grafana 右侧的编辑区看到的那些配置项就是这里定义的。path是配置项的键名后面在组件里通过props.options.labelText就能拿到。组件的生命周期很简单Grafana 拿到数据后把data和options作为 props 传给你的组件你负责渲染。数据变了、配置变了组件重新渲染。就这么直接。4.2 数据怎么进到面板里DataFrame 结构解析这是新手最容易懵的地方。Grafana 传给面板的数据不是简单的数组而是一个叫DataFrame的结构。你可以把它理解成一张带元信息的表格interface DataFrame { name?: string; fields: Field[]; // 每一列 length: number; // 行数 }每个Field有name、type、values这些属性。values是一个向量可能是普通数组也可能是 Arrow 格式的向量。取值的时候别直接下标访问用field.values.get(index)更稳妥能兼容不同底层实现。我一开始就是直接field.values[0]在简单数据下没问题换成大数据量、Arrow 向量之后就取不到值了排查了半天才发现是访问方式的问题。这个坑记住能省你不少时间。4.3 渲染层选型为什么我最终用了原生 SVG渲染这块可选方案有 ECharts、D3、原生 SVG/Canvas 几种。我这次的需求是画一个带交互的自定义图形数据量不大但交互逻辑复杂。ECharts 上手快但它的配置式 API 在复杂交互下会变得很别扭你想精细控制某个元素的响应得绕一大圈。D3 灵活但学习曲线陡团队里其他人接手成本高。最后我选了原生 SVG 配合 React理由是交互逻辑完全可控代码可读性好团队里会 React 的人都能改。代价是画图的基本功得自己写比如坐标映射、缩放、拖拽这些。但这次图形不复杂自己写反而更清爽。选型这事没有标准答案关键看你的图形复杂度和团队技术栈别盲目跟风。4.4 面板配置项的设计让使用者不用改代码面板插件好不好用一半看渲染一半看配置项设计。我的原则是凡是使用者可能想调的东西都做成配置项。比如颜色、尺寸、标签文字、是否显示网格线这些全通过setPanelOptions暴露出去。这样业务方换个颜色、调个阈值自己在 Grafana 界面上点几下就行不用来找你改代码重新发版。配置项的类型也很丰富文本、数字、下拉、开关、颜色选择器都有。用对了类型Grafana 会自动给你生成合适的输入控件体验很好。这里有个小技巧给配置项写清楚description鼠标悬停时会显示提示能大幅降低使用者的理解成本。5. 数据源适配把内部数据格式翻译成 Grafana 能懂的话面板画得再好数据进不来也是白搭。这次的数据源适配是另一个核心工作。5.1 DataSourceApi 必须实现的几个方法数据源插件要实现DataSourceApi接口其中最关键的是query方法async query(options: DataQueryRequestMyQuery): PromiseDataQueryResponse { const targets options.targets.filter((t) !t.hide); const data await Promise.all( targets.map((target) this.doQuery(target)) ); return { data }; }options.targets是用户在查询编辑器里配置的查询条件你要遍历它们逐个去后端取数最后返回一个DataFrame数组。Grafana 拿到这些 DataFrame再分发给面板去渲染。除了query还有testDatasource测试连接是否通、getTagKeys、getTagValues用于变量查询这些方法按需实现。testDatasource建议一定要实现用户配置数据源时点保存并测试能立刻知道配置对不对省去大量沟通成本。5.2 内部数据格式到 DataFrame 的映射我们内部系统的数据格式跟 Grafana 期望的不一样需要做一层转换。核心工作是把内部的时间序列数据映射成带time字段和value字段的 DataFrame。import { MutableDataFrame } from grafana/data; const frame new MutableDataFrame({ refId: target.refId, fields: [ { name: time, type: FieldType.time }, { name: value, type: FieldType.number }, ], }); internalData.forEach((point) { frame.appendRow([point.timestamp, point.value]); });这里有个性能注意点别用appendRow逐行追加大数据量几万行下来会明显卡顿。数据量大时直接构造fields的values数组一次性塞进去效率高得多。我实测过同样十万行数据逐行追加要好几秒批量构造基本瞬间完成。5.3 查询编辑器让用户能配出正确的查询数据源插件通常还要提供一个查询编辑器组件让用户配置查询条件。这个组件通过DataSourcePlugin注册export const plugin new DataSourcePlugin(MyDataSource) .setQueryEditor(MyQueryEditor);查询编辑器本质上就是个 React 组件接收query、onChange、onRunQuery这些 props。用户改了什么通过onChange回传Grafana 更新查询状态。设计得好不好直接决定用户配查询顺不顺手。我的经验是能下拉选的别让用户手输能自动补全的别让用户记把复杂度留给自己把简单留给用户。6. 调试与排错那些文档里不会写的坑功能写完了跑起来一堆问题。这一节我按真实排查顺序把遇到的坑一个个还原出来。6.1 插件加载失败从浏览器控制台找线索插件加载失败是最常见的第一个坑。现象是 Grafana 插件列表里看不到你的插件或者看到了但点进去报错。排查第一步打开浏览器开发者工具的控制台。Grafana 加载插件失败时控制台会有明确报错比如plugin.json格式错误、入口文件找不到、依赖缺失等。别急着改代码先看报错说什么。我遇到过一次控制台报Failed to load plugin module查了半天发现是构建产物路径跟plugin.json里声明的module字段对不上。改构建配置或者改plugin.json都行关键是让两边一致。6.2 数据不显示先确认数据到没到面板面板画不出来有两种可能数据没到或者数据到了但渲染逻辑有问题。怎么区分在面板组件里把props.data打印出来。如果props.data是空的问题在数据源或查询配置如果有数据但画面空白问题在渲染。这个二分法能帮你快速定位问题在哪一层别一上来就盯着渲染代码看。我踩过一个坑数据源query方法返回的DataFrame里time字段的类型写成了string而不是FieldType.timeGrafana 认不出这是时间轴面板就画不出来。类型字段一定要写对这是硬性约定。6.3 配置项不生效options 的传递链路检查用户改了配置项面板没反应。这种情况通常是options的传递链路断了。检查顺序是配置项定义里的path是否和组件里读取的键名一致setPanelOptions是否真的注册了组件是否在options变化时重新渲染。React 组件里options是作为 props 传进来的只要父组件更新你的组件就会重新渲染。如果你用了useMemo或者shouldComponentUpdate做了优化记得把options加进依赖否则改了配置不刷新。这个坑很隐蔽因为代码逻辑看起来完全正确。6.4 一个隐蔽的坑时区与时间戳单位这个坑我单独拎出来说因为它太容易被忽略。Grafana 内部时间戳默认是毫秒而我们内部系统的数据是秒。如果不做转换时间轴会显示成 1970 年或者所有点挤在一起。// 秒转毫秒 const timestampMs timestampSec * 1000;另外时区也要注意Grafana 有全局时区设置面板里也可以单独设。数据源返回的时间戳最好是 UTC让 Grafana 去负责展示层的时区转换别在数据源里就转成本地时间否则用户切换时区时会乱套。7. 升级兼容让这次改动活得更久一点二次开发最怕的不是写不出来而是写出来了官方一升级全废。这一节聊聊怎么让改动更抗造。7.1 锁定 API 版本别追新插件开发依赖的grafana/data、grafana/ui这些包版本要跟目标 Grafana 实例对齐。别用最新版也别用太老的版本。我的做法是先确定生产环境 Grafana 的版本然后装对应版本的 SDK 包。{ dependencies: { grafana/data: 9.5.0, grafana/ui: 9.5.0 } }版本对齐之后本地开发和生产环境行为一致能避免本地好好的上线就崩这种糟心事。7.2 把改动清单化为下次升级留后路所有对主仓库的改动我都记在一个单独的文档里写清楚改了哪个文件、改了什么、为什么插件机制做不到、升级时怎么验证。这份文档在下次升级时就是救命稻草。插件部分相对省心但也要记录清楚依赖的 SDK 版本和用到的 API。升级 Grafana 时先查官方 changelog 里有没有废弃你用的 API有的话提前改。7.3 用特性开关隔离风险改动对于不确定会不会出问题的改动我习惯加个特性开关。比如那个导航栏定制我通过配置项控制是否启用出问题了关掉就行不用回滚整个版本。这个习惯在二次开发里特别有用因为你的改动往往没有官方那么充分的测试覆盖。8. 一些实操下来觉得值得说的经验整个项目做下来有几个体会比较深分享给准备动手的人。第一先跑通最小闭环再堆功能。我一开始就想把面板、数据源、配置项全做完结果卡在环境上两天。后来改变策略先让一个最简单的面板能显示一行写死的数据跑通了再逐步加数据源、加配置、加交互。每加一块都能验证出问题也好定位。第二多看官方示例插件的源码。官方仓库里有一批示例插件代码质量高覆盖了各种典型场景。遇到不知道怎么实现的功能去翻示例比看文档快得多。文档告诉你有什么 API示例告诉你 API 怎么组合起来用。第三别怕用 TypeScript。Grafana 的 SDK 类型定义很完整用好类型能帮你在编译期发现大量问题。我一开始嫌类型麻烦用了any结果运行时各种莫名其妙的错误回头补类型反而花了更多时间。第四性能问题要早测。面板渲染和数据查询的性能别等到上线才发现。本地造点大数据量跑一跑看看卡不卡。我那个逐行追加数据的坑就是上线后用户反馈卡顿才发现的早测能早避免。最后说个心态上的事。Grafana 二次开发本质上是在别人的框架里做扩展你得接受有些事就是做不到或者做起来很别扭。遇到这种情况先退一步想想是不是需求本身可以调整而不是硬刚框架。很多时候换个交互方式、换个数据组织方式问题就绕过去了比死磕源码划算得多。