Help build the future of open source observability software Open positions

Check out the open source projects we support Downloads

We cannot remember your choice unless you click the consent notice at the bottom.

Edit your Git-based Grafana dashboards locally

Edit your Git-based Grafana dashboards locally

2024-10-29 11 min

Grafana has grown to become one of the most prominent dashboarding tools available, with an extensive set of features that support organizations of all sizes. There can come a time, however, when you have too many dashboards.

As a software engineer, you might think, “Why can’t I do with dashboards what I do with my code?” That is, you know how to keep your code in version control (e.g., Git). You know how to share and review your code with colleagues (e.g., pull requests). And you know how to build automated deployment pipeline to deploy our code (i.e., CI/CD). With this, we get scale, we get history, and all our code is reviewed before deployment. So why can’t you do this with your Grafana installation(s)?

Actually, you can.

You can edit dashboards as JSON using your IDE, or create them using tools such as Grafonnet (in Jsonnet) or the Grafana Foundation SDK (Go, TypeScript, Python, Java, PHP). You can then publish them to your Grafana instance(s) with tools such as the Grafana Terraform provider, Grizzly, and the Grafana Operator for Kubernetes.

But once you’ve made (or updated) your dashboard, how do you confirm that it works as expected? And how will your colleagues confirm that it works as expected in order to review it? In this post, we’re going to focus on a tool that helps us review dashboards before they are ready to be published. This can help you save time, close the review feedback loop earlier and with less pain, and get a better overall experience.

How to review dashboards before they’re published

Grizzly, a command line tool for Grafana, has a new serve function that can be used to validate (or even edit!) our dashboards in a Grafana Cloud instance (or any other Grafana instance).

Configuring Grizzly

First, we configure Grizzly by pointing it at a Grafana instance. (You’ll need Grizzly 0.4.6 or higher to follow the steps outlined in this blog.)

For these purposes, I suggest a development Grafana instance—one that has meaningful data in it but isn’t used in production. For this to work, the data source UIDs must be the same as those on your production instance.

grr config create-context dev
grr config set grafana.url https://mystack.grafana.net  ← or my dev instance URL
grr config set grafana.token <my-service-account-token> ← get this from my Grafana instance

Now my instance is ready to work.

Grafana data source requirements

If you want to use a different Grafana instance for editing than the one you eventually plan to deploy your dashboards to, you need to take care of your data source UIDs, which must be the same on both Grafana instances.

To achieve this, you’ll need to create the data sources via the API—for example, using Grizzly or Terraform. You won’t need to replace the existing data sources. You could also add additional data sources pointing to the same backend, so this needn’t be an issue for existing dashboards.

Editing our ‘offline’ dashboards with Grafana and Grizzly

Next, let’s look at how to edit your dashboards locally, or as I like to think of it, “offline” editing. Let’s say we have a dashboard file, stored in JSON in our Git repository. We have a local checkout we want to make a change to it, but for some changes, that’s really hard (if not impossible) to achieve by editing a JSON file.

Instead, we can use Grizzly to help us. Let’s create our dashboard file and call it my-dashboard.json:

json
{
   "graphTooltip": 1,
   "panels": [
  	{
     	"datasource": {
        	"type": "datasource",
        	"uid": "-- Mixed --"
     	},
     	"fieldConfig": {
        	"defaults": {
           	"unit": "reqps"
        	}
     	},
     	"gridPos": {
        	"h": 8,
        	"w": 24
     	},
     	"id": 1,
     	"pluginVersion": "v11.0.0",
     	"targets": [
        	{
           	"datasource": {
              	"type": "datasource",
              	"uid": "grafana"
           	},
           	"queryType": "randomWalk"
        	}
     	],
     	"title": "Requests / sec",
     	"type": "timeseries"
  	}
   ],
   "schemaVersion": 39,
   "title": "Example dashboard",
   "uid": "example-dashboard"
}

Then we can review and edit this dashboard with:

grr serve my-dashboard.json

This will open a web server (by default) at http://localhost:8080. Visiting that URL in a browser will show the name of our dashboard within the Grizzly serve UI. Clicking on the link to our dashboard, we’ll see our dashboard in the configured Grafana instance.

Note: This dashboard DOES NOT exist in our Grafana instance. Grizzly runs client-side, intercepting requests to Grafana and reading your dashboard JSON from disk.

Next, make a change to your dashboard, then click Save. Again, the dashboard won’t exist in Grafana; saving it will update the file on disk.

Reviewing our generated dashboards with Grafana and Grizzly

So far, we’ve looked at using Grizzly and Grafana together to edit and save our dashboards to a local disk. What if we want to review our generated dashboards?

What is a generated dashboard?

If dashboards are represented as JSON, that means that we can create that JSON with code. However, the structure of that JSON is complex, making the task harder. Fortunately, there are libraries in a range of languages that can make this much easier, including working with code completion in your chosen IDE.

Fortunately, Grizzly supports “generated” dashboards. When we plan to make the change to a file directly—instead of changing the dashboard in the UI—we want to tell Grizzly to watch the filesystem for changes with the -w switch. Sometimes, we will want to tell Grizzly to watch a directory of files. Grizzly can do this too. Then, all it takes to see our changes is reloading the dashboard in our browser.

Grizzly supports Jsonnet directly. For other languages, our code will need to output the dashboards to stdout—either a single dashboard JSON, or as an array of dashboard JSONs. Then, we can reference a command to execute our code in our call to Grizzly. (We will give examples for each language below.)

Next, let’s look at how we create dashboard code and preview your dashboard, using the supported languages.

Requirements: go-jsonnet/jsonnet-bundler

First we want to install Grafonnet:

jb init 
jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main

Then, create main.jsonnet containing this code:

local g = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';
g.dashboard.new('Example dashboard')
+ g.dashboard.withUid('example-dashboard')
+ g.dashboard.withDescription('Example Dashboard for Grizzly')
+ g.dashboard.graphTooltip.withSharedCrosshair()
+ g.dashboard.withPanels([
g.panel.timeSeries.new('Requests / sec')
+ g.panel.timeSeries.queryOptions.withTargets([
	g.query.testData.withQueryType('randomWalk')
	+ g.query.testData.withDatasource()
])
+ g.panel.timeSeries.standardOptions.withUnit('reqps')
+ g.panel.timeSeries.gridPos.withW(24)
+ g.panel.timeSeries.gridPos.withH(8),
])

To preview this dashboard with Grizzly, we can run:

grr serve -w main.jsonnet

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change main.jsonnet (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

Try this out. Edit main.jsonnet (e.g., change the title - in the new() call), and when you save it, reload the dashboard and see the changes in your Grafana instance.

Requirements: Yarn/NodeJS/NPX

First, we must install TypeScript and the Grafana Foundation SDK:

yarn add --dev typescript ts-node @types/node
yarn add '@grafana/grafana-foundation-sdk@~11.2.0-cogv0.0.x.1728036865'

Then, create a simple tsconfig.json file:

json
{
  "compilerOptions": {
    "moduleResolution": "Node16",
    "module": "node16",
    "lib": ["ES2022"]
  }
}

Next, we create our TypeScript dashboard code, as main.ts:

typescript
import * as dashboard from '@grafana/grafana-foundation-sdk/dashboard';
import * as testdata from '@grafana/grafana-foundation-sdk/testdata';
import { PanelBuilder as TimeseriesBuilder } from '@grafana/grafana-foundation-sdk/timeseries';

function makeDashboard() {
  let builder = new dashboard.DashboardBuilder('Example dashboard')
    .uid('example-dashboard')
    .description('Example Dashboard for Grizzly')
    .tooltip(dashboard.DashboardCursorSync.Crosshair)
    .withPanel(
      new TimeseriesBuilder()
        .title('Requests / sec')
        .unit("reqps")
        .withTarget(
          new testdata.DataqueryBuilder()
            .queryType('randomWalk')
            .datasource({uid: "grafana", type: "grafana"})
        )
        .span(24)
        .height(8)
  );
  const dash = JSON.stringify(builder.build(), null, 2);
  return dash;
}

console.log(makeDashboard());

Now, let’s say we run our code with npx ts-node main.ts. Then we can tell Grizzly to watch for changes in our main.ts file, and then execute our npx command to regenerate the dashboard:

grr serve -w -S 'npx ts-node main.ts' main.ts

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change main.ts (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

Requirements: Golang

First, we must install the Grafana Foundation SDK:

go mod init example
go get github.com/grafana/grafana-foundation-sdk/go@v11.2.x+cog-v0.0.x

Next, we create our Golang dashboard code, as main.go:

Go
package main

import (
  "encoding/json"
  "fmt"

  "github.com/grafana/grafana-foundation-sdk/go/cog"
  "github.com/grafana/grafana-foundation-sdk/go/dashboard"
  "github.com/grafana/grafana-foundation-sdk/go/testdata"
  "github.com/grafana/grafana-foundation-sdk/go/timeseries"
)

func grafanaDatasourceRef() dashboard.DataSourceRef {
  return dashboard.DataSourceRef{
    Uid:  cog.ToPtr("grafana"),
    Type: cog.ToPtr("grafana"),
  }
}

func makeDashboard() string {
  builder := dashboard.NewDashboardBuilder("Example dashboard").
    Uid("example-dashboard").
    Description("Example Dashboard for Grizzly").
    Tooltip(dashboard.DashboardCursorSyncCrosshair).
    WithPanel(
      timeseries.NewPanelBuilder().
        Title("Requests / sec").
        Unit("reqps").
        WithTarget(
          testdata.NewDataqueryBuilder().
            QueryType("randomWalk").
            Datasource(grafanaDatasourceRef()),
        ).
        Span(24).
        Height(8),
    )

  dashboard, err := builder.Build()
  if err != nil {
    panic(err)
  }

  dashboardJson, err := json.MarshalIndent(dashboard, "", "  ")
  if err != nil {
    panic(err)
  }

  return string(dashboardJson)
}

func main() {
  fmt.Println(makeDashboard())
}

Now we can tell Grizzly to watch for changes in our main.go file, and then execute our code to regenerate the dashboard:

grr serve -w -S 'go run main.go' main.go

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change main.go (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

Requirements: Python 3.11+, Python venv

First, we must install the Grafana Foundation SDK:

python -m venv .
bin/pip install 'grafana_foundation_sdk==1728036865!11.2.0'

On some systems, the first command might need to be python3 -m venv ., which creates a Python virtual environment in the current directory.

Next, we create our Python dashboard code, as main.py:

python
from grafana_foundation_sdk.builders import dashboard, testdata, timeseries
from grafana_foundation_sdk.models.dashboard import DataSourceRef, DashboardCursorSync, DashboardLinkType
from grafana_foundation_sdk.cog.encoder import JSONEncoder


def make_dashboard():
  builder = (
    dashboard.Dashboard('Example dashboard')
    .uid('example-dashboard')
    .description('Example Dashboard for Grizzly')
    .tooltip(DashboardCursorSync.CROSSHAIR)
    .with_panel(
      timeseries.Panel()
      .title('Requests / sec')
      .unit("reqps")
      .with_target(
        testdata.Dataquery().query_type('randomWalk')
        .datasource(DataSourceRef(uid="grafana", type_val="grafana"))
  	)
  	.span(24)
  	.height(8)
    )
  )

  dash = JSONEncoder(sort_keys=True, indent=2).encode(builder.build())
  return dash


print(make_dashboard())

Now we can tell Grizzly to watch for changes in our main.py file, and then execute our code to regenerate the dashboard:

grr serve -w -S 'bin/python main.py' main.py

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change main.py (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

Requirements: Java 17+, Maven

First, we need to create a pom.xml file for Maven:

File Type: XML

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
     	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>example</groupId>
	<artifactId>java</artifactId>
	<version>1.0-SNAPSHOT</version>

	<properties>
    	<maven.compiler.source>17</maven.compiler.source>
    	<maven.compiler.target>17</maven.compiler.target>
	</properties>

	<dependencies>
    	<dependency>
        	<groupId>com.grafana</groupId>
        	<artifactId>grafana-foundation-sdk</artifactId>
        	<version>11.2.0-1728036865</version>
    	</dependency>
	</dependencies>

</project>

Then, we can install the Grafana Foundation SDK:

 mvn install

Once our dependencies have downloaded, we can create our Java dashboard code, as src/main/java/example/App.java:

java
package example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.grafana.foundation.dashboard.Dashboard;
import com.grafana.foundation.dashboard.DashboardCursorSync;
import com.grafana.foundation.dashboard.DataSourceRef;
import com.grafana.foundation.testdata.Dataquery;
import com.grafana.foundation.timeseries.PanelBuilder;

public class App {

  public static void main(String[] args) {
    DataSourceRef ref = new DataSourceRef();
    ref.type = "grafana";
    ref.uid = "grafana";

    Dashboard dashboard = new Dashboard.Builder("Example Dashboard").
        uid("example-dashboard").
        description("Example Dashboard for Grizzly").
        tooltip(DashboardCursorSync.CROSSHAIR).
        withPanel(
            new PanelBuilder().
                title("Requests / sec").
                unit("reqps").
                span(24).
                height(8).
                withTarget(
                    new Dataquery.Builder().
                        datasource(ref).
                        queryType("randomWalk")
                )
         ).build();

    try {
        System.out.println(dashboard.toJSON());
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }
  }
}

Now we can tell Grizzly to watch for changes in our src directory, and then execute our code to regenerate the dashboard:

grr serve -w -S 'mvn exec:java -q -Dexec.mainClass="example.App"' src

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change App.java (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

Requirements: PHP CLI, Composer

First, we must install the Grafana Foundation SDK:

 composer require grafana/foundation-sdk:dev-v11.2.x+cog-v0.0.x

Next, we create our PHP dashboard code, as main.php:

php
<?php


require_once(__DIR__ . "/vendor/autoload.php");

use Grafana\Foundation\Dashboard\DataSourceRef;
use Grafana\Foundation\Dashboard\DashboardBuilder;
use Grafana\Foundation\Dashboard\DashboardCursorSync;
use Grafana\Foundation\Testdata;
use Grafana\Foundation\Timeseries;

function makeDashboard(): string {
  $builder = (new DashboardBuilder(title: 'Example Dashboard'))
      ->uid('example-dashboard')
      ->description('Example Dashboard for Grizzly')
      ->tooltip(DashboardCursorSync::crosshair())
      ->withPanel(
          (new Timeseries\PanelBuilder())
          ->title('Requests / sec')
          ->unit('reqps')
          ->withTarget(
              (new Testdata\DataqueryBuilder())
              ->queryType('randomWalk')
              ->datasource((new DataSourceRef('grafana', 'grafana')))
          )
          ->span(24)
          ->height(8)
  	);

    $json = $builder->build();
    return json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL;
}

echo makeDashboard();

Now we can tell Grizzly to watch for changes in our main.php file, and then execute our code to regenerate the dashboard:

grr serve -w -S 'php main.php' main.php

We can then visit the Grizzly server at http://localhost:8080, and select our dashboard. Now, when we change main.php (e.g., change the dashboard title) and save it, we can reload the dashboard and see the changes in our Grafana instance.

See Grizzly serve in action

To take a closer look at the steps I’ve outlined above, check out the video below. In it, I demo the Grizzly serve functionality with static files as well as with Grafonnet, but the same basic principles will apply for any of the languages we have discussed above.

If you have any questions or want to discuss further, please reach out to the #dashboards-as-code channel in the Grafana community Slack.

Senior Software Engineer Selene Pinillos contributed to this blog post.

Link to the Observability Survey.

Grafana Cloud is the easiest way to get started with metrics, logs, traces, dashboards, and more, and works well with Grizzly’s serve feature. We have a generous forever-free tier and plans for every use case. Sign up for free now!