Created
          October 31, 2025 06:58 
        
      - 
      
 - 
        
Save shayelkin/099ac92d2f5b82fd455b337566f9b2c4 to your computer and use it in GitHub Desktop.  
    A tool to convert grafana dashboard json to grafonnet
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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 | |
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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