Skip to content

Instantly share code, notes, and snippets.

@shayelkin
Created October 31, 2025 06:58
Show Gist options
  • Save shayelkin/099ac92d2f5b82fd455b337566f9b2c4 to your computer and use it in GitHub Desktop.
Save shayelkin/099ac92d2f5b82fd455b337566f9b2c4 to your computer and use it in GitHub Desktop.
A tool to convert grafana dashboard json to grafonnet
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
const (
indentString = " "
)
// ConvertDashboard converts a dashboard JSON to Jsonnet
func ConvertDashboard(data []byte) (string, error) {
var dashboard map[string]any
if err := json.Unmarshal(data, &dashboard); err != nil {
return "", fmt.Errorf("failed to parse dashboard JSON: %w", err)
}
var sb strings.Builder
// Import statement
sb.WriteString("local g = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';\n\n")
// Convert dashboard
dashboardJsonnet, err := convertDashboardObject(dashboard)
if err != nil {
return "", err
}
sb.WriteString(dashboardJsonnet)
return sb.String(), nil
}
func convertDashboardObject(dashboard map[string]any) (string, error) {
var sb strings.Builder
// Start with dashboard.new
title, _ := dashboard["title"].(string)
if title == "" {
title = "untitled"
}
sb.WriteString(fmt.Sprintf("g.dashboard.new('%s')\n", escapeString(title)))
// Add other dashboard properties
for key, value := range dashboard {
if value == nil {
continue
}
var jsonnetValue string
switch (key) {
case "title":
// Title is a special case that is handled above
continue
case "templating":
templating, ok := dashboard["templating"].(map[string]any)
if !ok {
return "", errors.New("failed to parse templates")
}
list, ok := templating["list"].([]any)
if !ok {
return "", errors.New("failed to parse templates list")
}
jsonnetValue = convertVariables(list)
case "panels":
panels, ok := dashboard["panels"].([]any)
if !ok {
return "", errors.New("failed to parse panels list")
}
var err error
jsonnetValue, err = convertPanelsWithIndent(panels, 1)
if err != nil {
return "", fmt.Errorf("failed to convert panels: %w", err)
}
default:
jsonnetValue = convertValue(value)
}
if jsonnetValue != "" {
sb.WriteString(fmt.Sprintf("+ g.dashboard.%s(%s)\n",
fieldToMethod(key), jsonnetValue))
}
}
return sb.String(), nil
}
func convertPanelsWithIndent(panels []any, indentLevel int) (string, error) {
if len(panels) == 0 {
return "", nil
}
var sb strings.Builder
sb.WriteString("[\n")
for i, panel := range panels {
panelMap, ok := panel.(map[string]any)
if !ok {
return "", fmt.Errorf("failed to convert panel number %d", i)
}
jsonnetValue, err := convertPanel(panelMap, indentLevel)
if err != nil {
return "", err
}
sb.WriteString(jsonnetValue)
if i < len(panels)-1 {
sb.WriteString(",\n")
}
}
sb.WriteString("]")
return sb.String(), nil
}
func convertPanel(panel map[string]any, indentLevel int) (string, error) {
var sb strings.Builder
indent := strings.Repeat(indentString, indentLevel)
panelType, _ := panel["type"].(string)
title, _ := panel["title"].(string)
if panelType == "" {
panelType = "timeseries" // default
}
// Start with panel.new
sb.WriteString(fmt.Sprintf("%sg.panel.%s.new('%s')",
indent, panelType, escapeString(title)))
// Process all other fields
for key, value := range panel {
if value == nil {
continue
}
var jsonnetValue string
switch key {
case "type", "title":
// Already handled above, skip
continue
case "targets":
// Handle targets (queries)
if targets, ok := value.([]any); ok {
jsonnetValue = convertTargets(targets, panelType, indentLevel)
}
case "panels":
// Handle nested panels (for row panels)
if panels, ok := value.([]any); ok && len(panels) > 0 {
var err error
jsonnetValue, err = convertPanelsWithIndent(panels, indentLevel+1)
if err != nil {
return "", err
}
}
default:
jsonnetValue = convertValue(value)
}
if jsonnetValue != "" {
sb.WriteString(fmt.Sprintf("\n%s+ g.panel.%s.%s(%s)",
indent, panelType, fieldToMethod(key), jsonnetValue))
}
}
return sb.String(), nil
}
func convertTargets(targets []any, panelType string, indentLevel int) string {
if len(targets) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("[\n")
indent := strings.Repeat(indentString, indentLevel+1)
for i, target := range targets {
if targetMap, ok := target.(map[string]any); ok {
datasource, _ := targetMap["datasource"].(map[string]any)
dsType := ""
if datasource != nil {
dsType, _ = datasource["type"].(string)
}
if dsType == "" {
dsType = "prometheus" // default
}
// Use query builder for the datasource type
sb.WriteString(fmt.Sprintf("%sg.query.%s.new(", indent, dsType))
// Add datasource parameter if present
if datasource != nil {
if uid, ok := datasource["uid"].(string); ok {
sb.WriteString(fmt.Sprintf("'%s', ", escapeString(uid)))
}
}
// Add query expression
if expr, ok := targetMap["expr"].(string); ok {
sb.WriteString(fmt.Sprintf("'%s'", escapeString(expr)))
}
sb.WriteString(")")
// Add other target properties
for key, value := range targetMap {
if key != "datasource" && key != "expr" && value != nil {
jsonnetValue := convertValue(value)
if jsonnetValue != "" {
sb.WriteString(fmt.Sprintf("\n%s+ g.query.%s.%s(%s)",
indent, dsType, fieldToMethod(key), jsonnetValue))
}
}
}
if i < len(targets)-1 {
sb.WriteString(",\n")
}
}
}
sb.WriteString("]")
return sb.String()
}
func convertVariables(variables []any) string {
if len(variables) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("[\n")
for i, variable := range variables {
if varMap, ok := variable.(map[string]any); ok {
varType, _ := varMap["type"].(string)
if varType == "" {
varType = "query"
}
name, _ := varMap["name"].(string)
sb.WriteString(fmt.Sprintf("%sg.dashboard.variable.%s.new('%s')",
indentString, varType, escapeString(name)))
// Add other variable properties
for key, value := range varMap {
if key != "type" && key != "name" && value != nil {
jsonnetValue := convertValue(value)
if jsonnetValue != "" {
sb.WriteString(fmt.Sprintf("\n%s+ g.dashboard.variable.%s.%s(%s)",
indentString, varType, fieldToMethod(key), jsonnetValue))
}
}
}
if i < len(variables)-1 {
sb.WriteString(",\n")
}
}
}
sb.WriteString("]")
return sb.String()
}
func convertValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case float64:
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%f", v)
case int:
return fmt.Sprintf("%d", v)
case bool:
return fmt.Sprintf("%t", v)
case []any:
return convertArray(v)
case map[string]any:
return convertObject(v)
default:
return ""
}
}
func convertArray(arr []any) string {
if len(arr) == 0 {
return "[]"
}
var sb strings.Builder
sb.WriteString("[")
for i, item := range arr {
sb.WriteString(convertValue(item))
if i < len(arr)-1 {
sb.WriteString(", ")
}
}
sb.WriteString("]")
return sb.String()
}
func convertObject(obj map[string]any) string {
if len(obj) == 0 {
return "{}"
}
var sb strings.Builder
sb.WriteString("{")
i := 0
for key, value := range obj {
sb.WriteString(fmt.Sprintf("%s: %s", key, convertValue(value)))
if i < len(obj)-1 {
sb.WriteString(", ")
}
i++
}
sb.WriteString("}")
return sb.String()
}
// fieldToMethod converts a JSON field name to a Jsonnet method name
// e.g., "uid" -> "withUid", "refresh" -> "withRefresh"
func fieldToMethod(field string) string {
if field == "" {
return ""
}
// Common special cases
switch field {
case "annotations":
return "withAnnotation"
case "fieldConfig":
return "fieldConfig"
case "id":
return "withId"
case "uid":
return "withUid"
case "templating":
return "withVariables"
}
// Capitalize first letter and add "with" prefix
return "with" + strings.ToUpper(field[:1]) + field[1:]
}
// escapeString escapes special characters in strings for Jsonnet
func escapeString(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "'", "\\'")
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "\r", "\\r")
s = strings.ReplaceAll(s, "\t", "\\t")
return s
}
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
)
func usage(code int) {
exec := filepath.Base(os.Args[0])
fmt.Printf("Usage: %s -input <dashboard.json> -output <dashboard.jsonnet>\n", exec)
os.Exit(code)
}
func main() {
var inputFile, outputFile string
flag.StringVar(&inputFile, "input", "", "Input dashboard JSON file (required)")
flag.StringVar(&outputFile, "output", "", "Output Jsonnet file (required)")
flag.Parse()
if inputFile == "" || outputFile == "" {
log.Println("error: both -input and -output flags are required")
usage(1)
}
// Read input file
data, err := os.ReadFile(inputFile)
if err != nil {
log.Fatalf("error reading input file: %v\n", err)
}
// Convert
jsonnet, err := ConvertDashboard(data)
if err != nil {
log.Fatalf("error converting dashboard: %v\n", err)
}
// Write output file
if err := os.WriteFile(outputFile, []byte(jsonnet), 0644); err != nil {
log.Fatalf("error writing output file: %v\n", err)
}
fmt.Printf("Successfully converted %s to %s\n", inputFile, outputFile)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment