Skip to content

Commit

Permalink
update dashboard linter (#313)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostinsoba authored Aug 12, 2024
1 parent 15ce2c7 commit 29e0cb2
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 24 deletions.
120 changes: 113 additions & 7 deletions tools/validation/grafana_dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ func isGrafanaDashboard(fileName string) bool {
func validateGrafanaDashboardFile(fileName string, fileContent []byte) *Messages {
msgs := NewMessages()

dashboardPanels := extractDashboardPanels(fileContent)
dashboard := gjson.ParseBytes(fileContent)
dashboardPanels := extractDashboardPanels(dashboard)
dashboardTemplates := extractDashboardTemplates(dashboard)

for _, panel := range dashboardPanels {
panelTitle := panel.Get("title").String()
Expand Down Expand Up @@ -114,7 +116,7 @@ func validateGrafanaDashboardFile(fileName string, fileContent []byte) *Messages
),
)
}
legacyDatasourceUIDs, hardcodedDatasourceUIDs := evaluateDeprecatedDatasourceUIDs(panel)
legacyDatasourceUIDs, hardcodedDatasourceUIDs, invalidPrometheusDatasourceUIDs := evaluateDeprecatedDatasourceUIDs(panel)
for _, datasourceUID := range legacyDatasourceUIDs {
msgs.Add(
NewError(
Expand All @@ -135,13 +137,47 @@ func validateGrafanaDashboardFile(fileName string, fileContent []byte) *Messages
),
)
}
for _, datasourceUID := range invalidPrometheusDatasourceUIDs {
msgs.Add(
NewError(
fileName,
"invalid prometheus datasource uid",
fmt.Sprintf("Panel %s contains invalid datasource uid: '%s', required to be: '%s'",
panelTitle, datasourceUID, prometheusDatasourceValidUID),
),
)
}
}

var hasPrometheusDatasourceVariable bool
for _, dashboardTemplate := range dashboardTemplates {
if evaluatePrometheusDatasourceTemplateVariable(dashboardTemplate) {
hasPrometheusDatasourceVariable = true
}
if queryVariable, ok := evaluateInvalidPrometheusDatasourceQueryTemplateVariable(dashboardTemplate); ok {
msgs.Add(
NewError(
fileName,
"invalid prometheus datasource query variable",
fmt.Sprintf("Dashboard variable '%s' must use '%s' as it's datasource", queryVariable, prometheusDatasourceValidUID),
),
)
}
}
if !hasPrometheusDatasourceVariable {
msgs.Add(
NewError(
fileName,
"missing prometheus datasource variable",
fmt.Sprintf("Dashboard must contain prometheus variable with query type: '%s' and name: '%s'",
prometheusDatasourceQuery, prometheusDatasourceValidName),
),
)
}
return msgs
}

func extractDashboardPanels(fileContent []byte) []gjson.Result {
dashboard := gjson.ParseBytes(fileContent)

func extractDashboardPanels(dashboard gjson.Result) []gjson.Result {
dashboardPanels := make([]gjson.Result, 0)
dashboardRows := dashboard.Get("rows").Array()
for _, dashboardRow := range dashboardRows {
Expand All @@ -162,10 +198,30 @@ func extractDashboardPanels(fileContent []byte) []gjson.Result {
return dashboardPanels
}

func evaluateDeprecatedDatasourceUIDs(panel gjson.Result) (legacyUIDs, hardcodedUIDs []string) {
func extractDashboardTemplates(dashboard gjson.Result) []gjson.Result {
dashboardTemplating := dashboard.Get("templating")
if !dashboardTemplating.Exists() {
return []gjson.Result{}
}
dashboardTemplatesList := dashboardTemplating.Get("list")
if !dashboardTemplatesList.Exists() || !dashboardTemplatesList.IsArray() {
return []gjson.Result{}
}
return dashboardTemplatesList.Array()
}

const (
prometheusDatasourceType = "prometheus"
prometheusDatasourceQuery = "prometheus"
prometheusDatasourceValidName = "ds_prometheus"
prometheusDatasourceValidUID = "${" + prometheusDatasourceValidName + "}"
)

func evaluateDeprecatedDatasourceUIDs(panel gjson.Result) (legacyUIDs, hardcodedUIDs, invalidPrometheusUIDs []string) {
targets := panel.Get("targets").Array()
legacyUIDs = make([]string, 0)
hardcodedUIDs = make([]string, 0)
invalidPrometheusUIDs = make([]string, 0)
for _, target := range targets {
datasource := target.Get("datasource")
if datasource.Exists() {
Expand All @@ -181,9 +237,16 @@ func evaluateDeprecatedDatasourceUIDs(panel gjson.Result) (legacyUIDs, hardcoded
if !strings.HasPrefix(uidStr, "$") {
hardcodedUIDs = append(hardcodedUIDs, uidStr)
}
datasourceType := datasource.Get("type")
if datasourceType.Exists() {
datasourceTypeStr := datasourceType.String()
if datasourceTypeStr == prometheusDatasourceType && uidStr != prometheusDatasourceValidUID {
invalidPrometheusUIDs = append(invalidPrometheusUIDs, uidStr)
}
}
}
}
return hardcodedUIDs, legacyUIDs
return hardcodedUIDs, legacyUIDs, invalidPrometheusUIDs
}

var (
Expand Down Expand Up @@ -226,3 +289,46 @@ func evaluateDeprecatedPanelType(panelType string) (replaceWith string, isDeprec
replaceWith, isDeprecated = deprecatedPanelTypes[panelType]
return replaceWith, isDeprecated
}

func evaluatePrometheusDatasourceTemplateVariable(dashboardTemplate gjson.Result) bool {
templateType := dashboardTemplate.Get("type")
if !templateType.Exists() {
return false
}
if templateType.String() != "datasource" {
return false
}
queryType := dashboardTemplate.Get("query")
if queryType.String() != prometheusDatasourceQuery {
return false
}
templateName := dashboardTemplate.Get("name")
if templateName.String() != prometheusDatasourceValidName {
return false
}
return true
}

func evaluateInvalidPrometheusDatasourceQueryTemplateVariable(dashboardTemplate gjson.Result) (string, bool) {
templateType := dashboardTemplate.Get("type")
if !templateType.Exists() {
return "", false
}
if templateType.String() != "query" {
return "", false
}
datasource := dashboardTemplate.Get("datasource")
if !datasource.Exists() {
return "", false
}
datasourceType := datasource.Get("type")
if datasourceType.String() != prometheusDatasourceType {
return "", false
}
datasourceUID := datasource.Get("uid")
if datasourceUID.String() == prometheusDatasourceValidUID {
return "", false
}
templateName := dashboardTemplate.Get("name")
return templateName.String(), true
}
67 changes: 50 additions & 17 deletions tools/validation/grafana_dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,35 @@ func TestValidateGrafanaDashboardFile(t *testing.T) {
"style": "dark",
"tags": [],
"templating": {
"list": []
"list": [
{
"current": {
"isNone": true,
"selected": false,
"text": "None",
"value": ""
},
"datasource": {
"type": "prometheus",
"uid": "prometheus_datasource_uid"
},
"definition": "metric_name",
"hide": 0,
"includeAll": false,
"multi": false,
"name": "dashboard_variable",
"options": [],
"query": {
"query": "metric_name",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-6h",
Expand All @@ -486,22 +514,27 @@ func TestValidateGrafanaDashboardFile(t *testing.T) {
"version": 1,
"weekStart": ""
}`
expected := &Messages{[]Message{
NewError("dashboard.json", "deprecated interval", "Panel Single Panel contains deprecated interval: 'interval_rv', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy alert rule", "Panel Single Panel contains legacy alert rule: 'Alert Rule Inside Single Panel', consider using external alertmanager"),
NewError("dashboard.json", "legacy datasource uid", "Panel Single Panel contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "hardcoded datasource uid", "Panel Single Panel contains hardcoded datasource uid: 'prometheus_datasource_uid', consider using grafana variable of type 'Datasource'"),
NewError("dashboard.json", "deprecated panel type", "Panel Plugin Single Panel is of deprecated type: 'graph', consider using 'timeseries'"),
NewError("dashboard.json", "deprecated interval", "Panel Plugin Single Panel contains deprecated interval: 'interval_rv', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy datasource uid", "Panel Plugin Single Panel contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "deprecated interval", "Panel Panel Inside Row contains deprecated interval: 'interval_sx3', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy alert rule", "Panel Panel Inside Row contains legacy alert rule: 'Panel Inside Row Alert Rule', consider using external alertmanager"),
NewError("dashboard.json", "legacy datasource uid", "Panel Panel Inside Row contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "hardcoded datasource uid", "Panel Panel Inside Row contains hardcoded datasource uid: 'prometheus_datasource_uid', consider using grafana variable of type 'Datasource'"),
NewError("dashboard.json", "deprecated panel type", "Panel Plugin Panel Inside Row is of deprecated type: 'flant-statusmap-panel', consider using 'state-timeline'"),
NewError("dashboard.json", "deprecated interval", "Panel Plugin Panel Inside Row contains deprecated interval: 'interval_sx4', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy datasource uid", "Panel Plugin Panel Inside Row contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
}}
expected := &Messages{
messages: []Message{
NewError("dashboard.json", "deprecated interval", "Panel Single Panel contains deprecated interval: 'interval_rv', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy alert rule", "Panel Single Panel contains legacy alert rule: 'Alert Rule Inside Single Panel', consider using external alertmanager"),
NewError("dashboard.json", "legacy datasource uid", "Panel Single Panel contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "hardcoded datasource uid", "Panel Single Panel contains hardcoded datasource uid: 'prometheus_datasource_uid', consider using grafana variable of type 'Datasource'"),
NewError("dashboard.json", "deprecated panel type", "Panel Plugin Single Panel is of deprecated type: 'graph', consider using 'timeseries'"),
NewError("dashboard.json", "deprecated interval", "Panel Plugin Single Panel contains deprecated interval: 'interval_rv', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy datasource uid", "Panel Plugin Single Panel contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "invalid prometheus datasource uid", "Panel Plugin Single Panel contains invalid datasource uid: 'prometheus_datasource_uid', required to be: '${ds_prometheus}'"),
NewError("dashboard.json", "deprecated interval", "Panel Panel Inside Row contains deprecated interval: 'interval_sx3', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy alert rule", "Panel Panel Inside Row contains legacy alert rule: 'Panel Inside Row Alert Rule', consider using external alertmanager"),
NewError("dashboard.json", "legacy datasource uid", "Panel Panel Inside Row contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "hardcoded datasource uid", "Panel Panel Inside Row contains hardcoded datasource uid: 'prometheus_datasource_uid', consider using grafana variable of type 'Datasource'"),
NewError("dashboard.json", "deprecated panel type", "Panel Plugin Panel Inside Row is of deprecated type: 'flant-statusmap-panel', consider using 'state-timeline'"),
NewError("dashboard.json", "deprecated interval", "Panel Plugin Panel Inside Row contains deprecated interval: 'interval_sx4', consider using '$__rate_interval'"),
NewError("dashboard.json", "legacy datasource uid", "Panel Plugin Panel Inside Row contains legacy datasource uid: 'prometheus_datasource_uid', consider resaving dashboard using newer version of Grafana"),
NewError("dashboard.json", "invalid prometheus datasource uid", "Panel Plugin Panel Inside Row contains invalid datasource uid: 'prometheus_datasource_uid', required to be: '${ds_prometheus}'"),
NewError("dashboard.json", "invalid prometheus datasource query variable", "Dashboard variable 'dashboard_variable' must use '${ds_prometheus}' as it's datasource"),
NewError("dashboard.json", "missing prometheus datasource variable", "Dashboard must contain prometheus variable with query type: 'prometheus' and name: 'ds_prometheus'"),
}}

actual := validateGrafanaDashboardFile("dashboard.json", []byte(in))
if !reflect.DeepEqual(actual, expected) {
Expand Down

0 comments on commit 29e0cb2

Please sign in to comment.