Files
Peter Štibraný 198375599e Fix/ignore some common dashboard problems (#114296)
* Fix some common problems found when parsing dashboards for indexing
2025-11-21 14:41:45 +01:00

666 lines
18 KiB
Go

package dashboard
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/infra/log"
)
type templateVariable struct {
current struct {
value any
}
name string
query any
variableType string
}
type datasourceVariableLookup struct {
variableNameToRefs map[string][]DataSourceRef
dsLookup DatasourceLookup
}
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []DataSourceRef {
switch value {
case "default":
// can be the default DS, or a DS with UID="default"
candidateDs := d.dsLookup.ByRef(&DataSourceRef{UID: value})
if candidateDs == nil {
// get the actual default DS
candidateDs = d.dsLookup.ByRef(nil)
}
if candidateDs != nil {
return []DataSourceRef{*candidateDs}
}
return []DataSourceRef{}
case "$__all":
// TODO: filter datasources by template variable's regex
return d.dsLookup.ByType(datasourceType)
case "":
return []DataSourceRef{}
case "No data sources found":
return []DataSourceRef{}
default:
// some variables use `ds.name` rather than `ds.uid`
if ref := d.dsLookup.ByRef(&DataSourceRef{
UID: value,
}); ref != nil {
return []DataSourceRef{*ref}
}
// discard variable
return []DataSourceRef{}
}
}
func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
var refs []DataSourceRef
datasourceType, isDataSourceTypeValid := templateVariable.query.(string)
if !isDataSourceTypeValid {
d.variableNameToRefs[templateVariable.name] = refs
return
}
if values, multiValueVariable := templateVariable.current.value.([]any); multiValueVariable {
for _, value := range values {
if valueAsString, ok := value.(string); ok {
refs = append(refs, d.getDsRefsByTemplateVariableValue(valueAsString, datasourceType)...)
}
}
}
if value, stringValue := templateVariable.current.value.(string); stringValue {
refs = append(refs, d.getDsRefsByTemplateVariableValue(value, datasourceType)...)
}
d.variableNameToRefs[templateVariable.name] = unique(refs)
}
func unique(refs []DataSourceRef) []DataSourceRef {
var uniqueRefs []DataSourceRef
uidPresence := make(map[string]bool)
for _, ref := range refs {
if !uidPresence[ref.UID] {
uidPresence[ref.UID] = true
uniqueRefs = append(uniqueRefs, ref)
}
}
return uniqueRefs
}
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []DataSourceRef {
refs, ok := d.variableNameToRefs[name]
if ok {
return refs
}
return []DataSourceRef{}
}
func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableLookup {
return &datasourceVariableLookup{
variableNameToRefs: make(map[string][]DataSourceRef),
dsLookup: dsLookup,
}
}
// ReadDashboard will take a byte stream and return dashboard info
func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardSummaryInfo, error) {
return ReadDashboardWithLogContext(stream, lookup, nil)
}
func ReadDashboardWithLogContext(stream io.Reader, lookup DatasourceLookup, logContext map[string]any) (*DashboardSummaryInfo, error) {
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
return readDashboardIter("$", iter, lookup, logContext)
}
// nolint:gocyclo
func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup DatasourceLookup, lc map[string]any) (*DashboardSummaryInfo, error) {
dash := &DashboardSummaryInfo{}
if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.ObjectValue) {
return dash, errors.New("expected JSON object at " + jsonPath)
}
datasourceVariablesLookup := newDatasourceVariableLookup(lookup)
for field := iter.ReadObject(); field != ""; field = iter.ReadObject() {
// Skip null values so we don't need special int handling
if iter.WhatIsNext() == jsoniter.NilValue {
iter.Skip()
continue
}
switch field {
// k8s metadata wrappers (skip)
case "metadata", "kind", "apiVersion":
iter.Skip()
// recursively read the spec as dashboard json
case "spec":
return readDashboardIter(jsonPath+".spec", iter, lookup, lc)
case "id":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue) {
continue
}
dash.ID = iter.ReadInt64()
case "title":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".title", lc, jsoniter.StringValue) {
continue
}
dash.Title = iter.ReadString()
case "description":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".description", lc, jsoniter.StringValue) {
continue
}
dash.Description = iter.ReadString()
case "schemaVersion":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".schemaVersion", lc, jsoniter.NumberValue, jsoniter.StringValue) {
continue
}
switch iter.WhatIsNext() {
case jsoniter.NumberValue:
dash.SchemaVersion = iter.ReadInt64()
case jsoniter.StringValue:
val := iter.ReadString()
if v, err := strconv.ParseInt(val, 10, 64); err == nil {
dash.SchemaVersion = v
}
default:
iter.Skip()
}
case "timezone":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".timezone", lc, jsoniter.StringValue) {
continue
}
dash.TimeZone = iter.ReadString()
case "editable":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".editable", lc, jsoniter.StringValue, jsoniter.BoolValue) {
continue
}
switch iter.WhatIsNext() {
case jsoniter.BoolValue:
dash.ReadOnly = !iter.ReadBool()
case jsoniter.StringValue:
dash.ReadOnly = iter.ReadString() != "true"
default:
iter.Skip()
}
case "refresh":
// "refresh" used to be boolean. We will silently skip it in such case.
if !checkAndSkipUnexpectedElement(iter, jsonPath+".refresh", lc, jsoniter.StringValue, jsoniter.BoolValue) {
continue
}
if iter.WhatIsNext() == jsoniter.BoolValue {
iter.Skip()
continue
}
dash.Refresh = iter.ReadString()
case "tags":
tagsPath := jsonPath + ".tags"
// Only support string array tags. Ignore everything else.
if !checkAndSkipUnexpectedElement(iter, tagsPath, lc, jsoniter.ArrayValue) {
continue
}
for ix := 0; iter.ReadArray(); ix++ {
if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s[%d]", tagsPath, ix), lc, jsoniter.StringValue) {
continue
}
dash.Tags = append(dash.Tags, iter.ReadString())
}
case "links":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".links", lc, jsoniter.ArrayValue) {
continue
}
for iter.ReadArray() {
iter.Skip()
dash.LinkCount++
}
case "time":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".time", lc, jsoniter.ObjectValue) {
continue
}
obj, ok := iter.Read().(map[string]any)
if ok {
if timeFrom, ok := obj["from"].(string); ok {
dash.TimeFrom = timeFrom
}
if timeTo, ok := obj["to"].(string); ok {
dash.TimeTo = timeTo
}
}
case "panels":
panelsPath := jsonPath + ".panels"
if !checkAndSkipUnexpectedElement(iter, panelsPath, lc, jsoniter.ArrayValue) {
continue
}
for ix := 0; iter.ReadArray(); ix++ {
p, ok := readpanelInfo(iter, lookup, fmt.Sprintf("%s[%d]", panelsPath, ix), lc)
if ok {
dash.Panels = append(dash.Panels, p)
}
}
case "templating":
templatingPath := jsonPath + ".templating"
if !checkAndSkipUnexpectedElement(iter, templatingPath, lc, jsoniter.ObjectValue) {
continue
}
for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() {
// Skip all null values silently.
if iter.WhatIsNext() == jsoniter.NilValue {
iter.Skip()
continue
}
if sub == "list" {
templatingListPath := templatingPath + ".list"
if !checkAndSkipUnexpectedElement(iter, templatingListPath, lc, jsoniter.ArrayValue) {
continue
}
for ix := 0; iter.ReadArray(); ix++ {
// Skip all null elements silently.
if iter.WhatIsNext() == jsoniter.NilValue {
iter.Skip()
continue
}
tv := templateVariable{}
templatingListElementPath := fmt.Sprintf("%s[%d]", templatingListPath, ix)
if !checkAndSkipUnexpectedElement(iter, templatingListElementPath, lc, jsoniter.ObjectValue) {
continue
}
for k := iter.ReadObject(); k != ""; k = iter.ReadObject() {
switch k {
case "name":
if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".name", lc, jsoniter.StringValue) {
continue
}
name := iter.ReadString()
dash.TemplateVars = append(dash.TemplateVars, name)
tv.name = name
case "type":
if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".type", lc, jsoniter.StringValue) {
continue
}
tv.variableType = iter.ReadString()
case "query":
tv.query = iter.Read()
case "current":
if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".current", lc, jsoniter.ObjectValue, jsoniter.ArrayValue) {
continue
}
if iter.WhatIsNext() == jsoniter.ArrayValue {
iter.Skip()
continue
}
for c := iter.ReadObject(); c != ""; c = iter.ReadObject() {
if c == "value" {
tv.current.value = iter.Read()
} else {
iter.Skip()
}
}
default:
iter.Skip()
}
}
if tv.variableType == "datasource" {
datasourceVariablesLookup.add(tv)
}
}
} else {
iter.Skip()
}
}
// Ignore everything else
default:
iter.Skip()
}
}
replaceDatasourceVariables(dash, datasourceVariablesLookup)
fillDefaultDatasources(dash, lookup)
filterOutSpecialDatasources(dash)
targets := newTargetInfo(lookup)
for idx, panel := range dash.Panels {
if panel.Type == "row" {
dash.Panels[idx].Datasource = nil
continue
}
targets.addPanel(panel)
}
dash.Datasource = targets.GetDatasourceInfo()
return dash, iter.Error
}
var logger = log.New("services.store.kind.dashboard")
// checkAndSkipUnexpectedElement verifies if the next JSON element matches any allowed value types for the specified JSON path.
// If the type matches, it returns true, otherwise it skips the element, logs an error, and returns false.
func checkAndSkipUnexpectedElement(iter *jsoniter.Iterator, jsonPath string, logContext map[string]any, allowedValues ...jsoniter.ValueType) bool {
next := iter.WhatIsNext()
for _, a := range allowedValues {
if next == a {
return true
}
}
// Skip unexpected element.
iter.Skip()
// Prepare log message.
params := []any{
"jsonPath", jsonPath,
"got", valueTypesToString(next),
"expected", valueTypesToString(allowedValues...),
}
// Map iteration is random, so a log message may look different each time if there are multiple entries in the map. That's fine.
for k, v := range logContext {
params = append(params, k, v)
}
logger.Error("Unexpected element in Dashboard JSON", params...)
return false
}
func valueTypesToString(allowedValues ...jsoniter.ValueType) string {
expected := strings.Builder{}
for ix, a := range allowedValues {
if ix > 0 {
expected.WriteString(", ")
}
switch a {
case jsoniter.NilValue:
expected.WriteString("null")
case jsoniter.StringValue:
expected.WriteString("string")
case jsoniter.NumberValue:
expected.WriteString("number")
case jsoniter.BoolValue:
expected.WriteString("bool")
case jsoniter.ArrayValue:
expected.WriteString("array")
case jsoniter.ObjectValue:
expected.WriteString("object")
default:
expected.WriteString(fmt.Sprintf("unknown: %d", a))
}
}
return expected.String()
}
func panelRequiresDatasource(panel PanelSummaryInfo) bool {
return panel.Type != "row"
}
func fillDefaultDatasources(dash *DashboardSummaryInfo, lookup DatasourceLookup) {
for i, panel := range dash.Panels {
if len(panel.Datasource) != 0 || !panelRequiresDatasource(PanelSummaryInfo{}) {
continue
}
defaultDs := lookup.ByRef(nil)
if defaultDs != nil {
dash.Panels[i].Datasource = []DataSourceRef{*defaultDs}
}
}
}
func filterOutSpecialDatasources(dash *DashboardSummaryInfo) {
for i, panel := range dash.Panels {
var dsRefs []DataSourceRef
// partition into actual datasource references and variables
for _, ds := range panel.Datasource {
switch ds.UID {
case "-- Mixed --":
// The actual datasources used as targets will remain
continue
case "-- Dashboard --":
// The `Dashboard` datasource refers to the results of the query used in another panel
continue
case "grafana":
// this is the uid for the -- Grafana -- datasource
continue
default:
dsRefs = append(dsRefs, ds)
}
}
dash.Panels[i].Datasource = dsRefs
}
}
func replaceDatasourceVariables(dash *DashboardSummaryInfo, datasourceVariablesLookup *datasourceVariableLookup) {
for i, panel := range dash.Panels {
var dsVariableRefs []DataSourceRef
var dsRefs []DataSourceRef
// partition into actual datasource references and variables
for i := range panel.Datasource {
uid := panel.Datasource[i].UID
if isVariableRef(uid) {
dsVariableRefs = append(dsVariableRefs, panel.Datasource[i])
} else {
dsRefs = append(dsRefs, panel.Datasource[i])
}
}
variables := findDatasourceRefsForVariables(dsVariableRefs, datasourceVariablesLookup)
dash.Panels[i].Datasource = append(dsRefs, variables...)
}
}
func isSpecialDatasource(uid string) bool {
return uid == "-- Mixed --" || uid == "-- Dashboard --"
}
func isVariableRef(uid string) bool {
return strings.HasPrefix(uid, "$")
}
func getDataSourceVariableName(dsVariableRef DataSourceRef) string {
if strings.HasPrefix(dsVariableRef.UID, "${") {
return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${")
}
return strings.TrimPrefix(dsVariableRef.UID, "$")
}
func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []DataSourceRef {
var referencedDs []DataSourceRef
for _, dsVariableRef := range dsVariableRefs {
variableName := getDataSourceVariableName(dsVariableRef)
refs := datasourceVariablesLookup.getDatasourceRefs(variableName)
referencedDs = append(referencedDs, refs...)
}
return referencedDs
}
// nolint:gocyclo
func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup, jsonPath string, lc map[string]any) (PanelSummaryInfo, bool) {
panel := PanelSummaryInfo{}
if !checkAndSkipUnexpectedElement(iter, jsonPath, lc, jsoniter.ObjectValue) {
return panel, false
}
targets := newTargetInfo(lookup)
for field := iter.ReadObject(); field != ""; field = iter.ReadObject() {
if iter.WhatIsNext() == jsoniter.NilValue {
if field == "datasource" {
targets.addDatasource(iter, jsonPath+".datasource", lc)
continue
}
// Skip null values so we don't need special int handling
iter.Skip()
continue
}
switch field {
case "id":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue, jsoniter.StringValue) {
continue
}
if iter.WhatIsNext() == jsoniter.StringValue {
id, err := strconv.ParseInt(iter.ReadString(), 10, 64)
if err == nil {
panel.ID = id
}
} else {
panel.ID = iter.ReadInt64()
}
case "type":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".type", lc, jsoniter.StringValue) {
continue
}
panel.Type = iter.ReadString()
case "title":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".title", lc, jsoniter.StringValue) {
continue
}
panel.Title = iter.ReadString()
case "description":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".description", lc, jsoniter.StringValue) {
continue
}
panel.Description = iter.ReadString()
case "pluginVersion":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".pluginVersion", lc, jsoniter.StringValue) {
continue
}
panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model)
case "libraryPanel":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".libraryPanel", lc, jsoniter.ObjectValue) {
continue
}
var v map[string]interface{}
iter.ReadVal(&v)
if uid, ok := v["uid"]; ok {
if u, isString := uid.(string); isString {
panel.LibraryPanel = u
}
}
case "datasource":
targets.addDatasource(iter, jsonPath+".datasource", lc)
case "targets":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".targets", lc, jsoniter.ArrayValue, jsoniter.ObjectValue) {
continue
}
switch iter.WhatIsNext() {
case jsoniter.ArrayValue:
for ix := 0; iter.ReadArray(); ix++ {
targets.addTarget(iter, fmt.Sprintf("%s.targets[%d]", jsonPath, ix), lc)
}
case jsoniter.ObjectValue:
for fn := iter.ReadObject(); fn != ""; fn = iter.ReadObject() {
targets.addTarget(iter, jsonPath+".targets."+fn, lc)
}
default:
iter.Skip()
}
case "transformations":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".transformations", lc, jsoniter.ArrayValue) {
continue
}
for ix := 0; iter.ReadArray(); ix++ {
if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s.transformations[%d]", jsonPath, ix), lc, jsoniter.ObjectValue) {
continue
}
for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() {
if sub == "id" {
if !checkAndSkipUnexpectedElement(iter, fmt.Sprintf("%s.transformations[%d].id", jsonPath, ix), lc, jsoniter.StringValue) {
continue
}
panel.Transformer = append(panel.Transformer, iter.ReadString())
} else {
iter.Skip()
}
}
}
// Rows have nested panels
case "panels":
if !checkAndSkipUnexpectedElement(iter, jsonPath+".panels", lc, jsoniter.ArrayValue) {
continue
}
for ix := 0; iter.ReadArray(); ix++ {
p, ok := readpanelInfo(iter, lookup, fmt.Sprintf("%s.panels[%d]", jsonPath, ix), lc)
if ok {
panel.Collapsed = append(panel.Collapsed, p)
}
}
case "options", "gridPos", "fieldConfig":
iter.Skip()
default:
iter.Skip()
}
}
panel.Datasource = targets.GetDatasourceInfo()
return panel, true
}