使用 Grafonnet 管理 Grafana 仪表盘

2021年11月11日 Aashish Sharma

维护 Grafana 面板或更新面板 JSON 文件有时会变得混乱,因为 JSON 文件的大小变得太大而难以调试。正常的 Grafana 面板 json 文件的平均代码行数有时超过 700-800 行。大型 json 文件通常包含冗余属性,这些属性在整个文件中通常具有相似的值。事实上,我们收到的大部分外部贡献都集中在这个领域,因此,在不费力的情况下维护 Grafana 面板文件变得至关重要。在本博客中,您可以了解如何使用 Grafonnet 将 Grafana 面板作为代码进行管理。

Grafana Dashboards Grafana Dashboards

关于 Grafonnet

最初,在致力于寻找一种使 Grafana 面板不那么复杂的方法时,我遇到了 Grafanalib - 一个用于维护 Grafana 面板的 Python 库。当我浏览 grafanalib 时,我发现它缺少一些功能,例如 - vonage-status-panelgrafana-piechart-panel。我也浏览了 ceph-mixins,它已经存在于 Ceph 仓库中,并使用纯 jsonnet 提供 Ceph 特定的 Prometheus 规则文件,但我没有在那里找到任何关于 Grafana 面板的参考。因此,作为替代方案,我开始探索 Grafonnet,Grafana 官方维护它。如果您已经知道 Grafonnet 是什么,那么使用 grafonnet 会更容易。对于那些不知道的人来说:Grafonnet 是一个 Jsonnet 库 - Jsonnet 是 JSON 的扩展,用于使用变量、函数、条件等抽象创建文件。Grafonnet 包含不同的函数,我们可以使用这些函数来创建用于组合 Grafana 面板或面板的 JSON 对象。然后,将生成的 JSON 文件导入到 grafana 中。

使用 Grafonnet 创建面板

grafonnet 库公开了我们可以用来定义 Grafana 面板的函数。首先,我们需要 导入 Grafonnet。Grafana.libsonnet 作为入口点,并公开了库文件中所需的所有函数和接口。以下代码是创建仅需要标题的面板所需的最小代码。

local grafana = import 'grafonnet/grafana.libsonnet';
local dashboard = grafana.dashboard;
 
dashboard.new(
  title='Demo dashboard'
)

可以使用 $ jsonnet -o new.json new.jsonnet 编译上述代码,并将生成的 json 文件(new.json)导入到 Grafana 中。

添加模板变量

Grafonnet 库提供了函数 addTemplate() 和 addTemplates(),它们分别接收模板对象或模板数组。

local template = grafana.template;
 
dashboard.new(
  title='Demo Dashboard'
)
.addTemplate(
  template.new(
    name='service',
    datasource='LocalPrometheus',
    query='label_values(service)',
    allValues=null,
    current='all',
    refresh='load',
    includeAll=true,
    multi=true,
  )
)

添加面板

面板对象的使用方式与模板变量类似,通过使用 addPanel() 或 addPanels() 添加。除了创建面板对象之外,我们还通过添加 gridPos 属性来指定它们的位置。以下是添加一个文本面板和一个图形面板的附加代码。通过使用面板对象公开的 addTarget(),我们可以添加目标对象。

local prometheus = grafana.prometheus;

local template = grafana.template;
 
dashboard.new(
  title='Demo Dashboard'
)

.addAnnotation(addAnnotationSchema(1, '-- Grafana --', true, true, 'rgba(0, 211, 255, 1)', 'Annotations & Alerts', 'dashboard'))
.addRequired(type='grafana', id='grafana', name='Grafana', version='5.0.0')

// template object
.addTemplate(
  template.new(name='service',datasource='LocalPrometheus',query='label_values(service)',allValues=null,current='all',refresh='load',includeAll=true,
  multi=true)
)
 
// dashboard panel
.addPanels([
  textPanel.new(
    mode='html',
    content='<p2 align=\"center\">Services</p2>',
  ) + {gridPos: {h: 1, w: 0, x: 8, y: 0}},
  graphPanel.new(
    title='RGW Sync Overview',
    fill=2,
    formatY1='percentunit',
    datasource='Prometheus',
  )
  .addTargets([
    prometheus.target(
      expr='ceph_health_status == 2',
      intervalFactor=1,
      legendFormat='time_series',
    )
  ]) + {gridPos: {h: 0, w: 0, x: 8, y: 9}},
])

以下是上述 jsonnet 代码的等效 json 代码

显示 json 代码

{
   "__inputs": [ ],
   "__requires": [
      {
         "id": "grafana",
         "name": "Grafana",
         "type": "grafana",
         "version": "5.0.0"
      },
      {
         "id": "graph",
         "name": "Graph",
         "type": "panel",
         "version": "5.0.0"
      }
   ],
   "annotations": {
      "list": [
         {
            "builtIn": 1,
            "datasource": "-- Grafana --",
            "enable": true,
            "hide": true,
            "iconColor": "rgba(0, 211, 255, 1)",
            "name": "Annotations & Alerts",
            "showIn": 0,
            "tags": [ ],
            "type": "dashboard"
         }
      ]
   },
   "editable": false,
   "gnetId": null,
   "graphTooltip": 0,
   "hideControls": false,
   "id": null,
   "links": [ ],
   "panels": [
      {
         "aliasColors": { },
         "bars": false,
         "dashLength": 10,
         "dashes": false,
         "datasource": "$datasource",
         "fill": 1,
         "gridPos": {
            "h": 7,
            "w": 8,
            "x": 0,
            "y": 0
         },
         "id": 2,
         "legend": {
            "alignAsTable": false,
            "avg": false,
            "current": false,
            "max": false,
            "min": false,
            "rightSide": false,
            "show": true,
            "sideWidth": null,
            "total": false,
            "values": false
         },
         "lines": true,
         "linewidth": 1,
         "links": [ ],
         "nullPointMode": "null as zero",
         "percentage": false,
         "pointradius": 5,
         "points": false,
         "renderer": "flot",
         "repeat": null,
         "seriesOverrides": [ ],
         "spaceLength": 10,
         "stack": true,
         "steppedLine": false,
         "targets": [
            {
               "expr": "sum by (source_zone) (rate(ceph_data_sync_from_zone_fetch_bytes_sum[30s]))",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "",
               "refId": "A"
            }
         ],
         "thresholds": [ ],
         "timeFrom": null,
         "timeShift": null,
         "title": "Replication (throughput) from Source Zone",
         "tooltip": {
            "shared": true,
            "sort": 0,
            "value_type": "individual"
         },
         "type": "graph",
         "xaxis": {
            "buckets": null,
            "mode": "time",
            "name": null,
            "show": true,
            "values": [ ]
         },
         "yaxes": [
            {
               "format": "Bps",
               "label": null,
               "logBase": 1,
               "max": null,
               "min": 0,
               "show": true
            },
            {
               "format": "short",
               "label": null,
               "logBase": 1,
               "max": null,
               "min": 0,
               "show": true
            }
         ]
      },
   ],
   "refresh": "15s",
   "rows": [ ],
   "schemaVersion": 16,
   "style": "dark",
   "tags": [
      "overview"
   ],
   "templating": {
      "list": [
         {
            "allValue": null,
            "current": { },
            "datasource": "$datasource",
            "hide": 2,
            "includeAll": true,
            "label": null,
            "multi": false,
            "name": "rgw_servers",
            "options": [ ],
            "query": "prometehus",
            "refresh": 1,
            "regex": "",
            "sort": 1,
            "tagValuesQuery": "",
            "tags": [ ],
            "tagsQuery": "",
            "type": "query",
            "useTags": false
         },
         {
            "current": {
               "text": "default",
               "value": "default"
            },
            "hide": 0,
            "label": "Data Source",
            "name": "datasource",
            "options": [ ],
            "query": "prometheus",
            "refresh": 1,
            "regex": "",
            "type": "datasource"
         }
      ]
   },
   "time": {
      "from": "now-1h",
      "to": "now"
   },
   "timepicker": {
      "refresh_intervals": [
         "5s",
         "10s",
         "15s",
         "30s",
         "1m",
         "5m",
         "15m",
         "30m",
         "1h",
         "2h",
         "1d"
      ],
      "time_options": [
         "5m",
         "15m",
         "1h",
         "6h",
         "12h",
         "24h",
         "2d",
         "7d",
         "30d"
      ]
   },
   "timezone": "",
   "title": "RGW Sync Overview",
   "uid": "rgw-sync-overview",
   "version": 0
}

正如您从上面的示例中清楚看到的,使用 Grafonnet 可以将编码行数从大约 200 行减少到大约 40 行,从而减少约 85-90%。

为了可重用性而自定义函数

使用可重用的函数不仅可以使代码更易于阅读,而且更易于维护。以下是一个创建自定义函数的示例,这些函数接收诸如标题、uid 等属性,如果希望通过仅传递参数来创建许多面板,则可以在许多面板中重用这些函数。

local dashboardSchema(title, uid, time_from, refresh, schemaVersion, tags,timezone, timepicker) =
  g.dashboard.new(title=title, uid=uid, time_from=time_from, refresh=refresh, schemaVersion=schemaVersion, tags=tags, timezone=timezone, timepicker=timepicker);

local graphPanelSchema(title, nullPointMode, stack, formatY1, formatY2, labelY1, labelY2, min, fill, datasource) =
  g.graphPanel.new(title=title, nullPointMode=nullPointMode, stack=stack, formatY1=formatY1, formatY2=formatY2, labelY1=labelY1, labelY2=labelY2, min=min, fill=fill, datasource=datasource);

迁移

如果您希望从现有的 Grafana JSON 面板迁移到 Grafonnet,您只需要在应用程序中安装 jsonnet 包。您还需要做的事情是克隆 Grafonnet 库,我已经在关于部分中提到了该库的链接。对如何用 jsonnet 编写代码有一定的了解将大有裨益。如果不是,您可以从此链接获取一些想法 - jsonnet。您可以开始用 jsonnet 编写面板,并从该面板生成等效的 json 文件并将其导入到现有的 Grafana 面板中。

结果

如果使用 Grafana UI 生成面板过于繁琐或耗时,当使用许多类似的面板或面板时,Grafonnet 可能值得考虑,前提是您能在自定义和重用自定义模板之间找到合适的平衡。另一方面,所有更改都需要在 Jsonnet 文件中进行,因为 UI 中的更改不会更改相应的文件。