SuggestedDashboards- Extend dashvalidator POC: Prometheus validator
This commit is contained in:
@@ -6,6 +6,7 @@ require (
|
||||
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
|
||||
github.com/grafana/grafana-app-sdk v0.48.7
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7
|
||||
github.com/prometheus/prometheus v0.303.1
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
|
||||
)
|
||||
@@ -38,6 +39,7 @@ require (
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -91,6 +93,7 @@ require (
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
|
||||
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
@@ -84,6 +91,8 @@ github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -141,6 +150,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
|
||||
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
|
||||
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
|
||||
@@ -334,11 +345,17 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@@ -370,6 +387,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
@@ -554,6 +573,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -627,6 +648,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY=
|
||||
github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck=
|
||||
github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8=
|
||||
github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
@@ -872,6 +897,8 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
@@ -13,13 +14,61 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
validatorv1alpha1 "github.com/grafana/grafana/apps/dashvalidator/pkg/apis/dashvalidator/v1alpha1"
|
||||
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
|
||||
_ "github.com/grafana/grafana/apps/dashvalidator/pkg/validator/prometheus" // Register prometheus validator via init()
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
)
|
||||
|
||||
type DashValidatorConfig struct {
|
||||
DatasourceSvc datasources.DataSourceService
|
||||
PluginCtx *plugincontext.Provider
|
||||
DatasourceSvc datasources.DataSourceService
|
||||
PluginCtx *plugincontext.Provider
|
||||
HTTPClientProvider httpclient.Provider
|
||||
}
|
||||
|
||||
// checkRequest matches the CUE schema for POST /check request
|
||||
type checkRequest struct {
|
||||
DashboardJSON map[string]interface{} `json:"dashboardJson"`
|
||||
DatasourceMappings []datasourceMapping `json:"datasourceMappings"`
|
||||
}
|
||||
|
||||
// datasourceMapping represents a datasource to validate against
|
||||
type datasourceMapping struct {
|
||||
UID string `json:"uid"`
|
||||
Type string `json:"type"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// checkResponse matches the CUE schema for POST /check response
|
||||
type checkResponse struct {
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
DatasourceResults []datasourceResult `json:"datasourceResults"`
|
||||
}
|
||||
|
||||
// datasourceResult contains validation results for a single datasource
|
||||
type datasourceResult struct {
|
||||
UID string `json:"uid"`
|
||||
Type string `json:"type"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
TotalQueries int `json:"totalQueries"`
|
||||
CheckedQueries int `json:"checkedQueries"`
|
||||
TotalMetrics int `json:"totalMetrics"`
|
||||
FoundMetrics int `json:"foundMetrics"`
|
||||
MissingMetrics []string `json:"missingMetrics"`
|
||||
QueryBreakdown []queryResult `json:"queryBreakdown"`
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
}
|
||||
|
||||
// queryResult contains validation results for a single query
|
||||
type queryResult struct {
|
||||
PanelTitle string `json:"panelTitle"`
|
||||
PanelID int `json:"panelID"`
|
||||
QueryRefID string `json:"queryRefId"`
|
||||
TotalMetrics int `json:"totalMetrics"`
|
||||
FoundMetrics int `json:"foundMetrics"`
|
||||
MissingMetrics []string `json:"missingMetrics"`
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
}
|
||||
|
||||
func New(cfg app.Config) (app.App, error) {
|
||||
@@ -42,7 +91,7 @@ func New(cfg app.Config) (app.App, error) {
|
||||
Namespaced: true,
|
||||
Path: "check",
|
||||
Method: "POST",
|
||||
}: handleCheckRoute(log, specificConfig.DatasourceSvc, specificConfig.PluginCtx),
|
||||
}: handleCheckRoute(log, specificConfig.DatasourceSvc, specificConfig.PluginCtx, specificConfig.HTTPClientProvider),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -60,21 +109,149 @@ func handleCheckRoute(
|
||||
log logging.Logger,
|
||||
datasourceSvc datasources.DataSourceService,
|
||||
pluginCtx *plugincontext.Provider,
|
||||
httpClientProvider httpclient.Provider,
|
||||
) func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error {
|
||||
|
||||
return func(ctx context.Context, w app.CustomRouteResponseWriter, r *app.CustomRouteRequest) error {
|
||||
logger := log.WithContext(ctx)
|
||||
logger.Info("Received compatibility check request")
|
||||
|
||||
// TODO validation logic here
|
||||
// Step 1: Parse request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read request body", "error", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "failed to read request body",
|
||||
})
|
||||
}
|
||||
|
||||
// for now a simple response
|
||||
var req checkRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
logger.Error("Failed to parse request JSON", "error", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "invalid JSON in request body",
|
||||
})
|
||||
}
|
||||
|
||||
// Step 2: Build validator request
|
||||
validatorReq := validator.DashboardCompatibilityRequest{
|
||||
DashboardJSON: req.DashboardJSON,
|
||||
DatasourceMappings: make([]validator.DatasourceMapping, 0, len(req.DatasourceMappings)),
|
||||
}
|
||||
|
||||
logger.Info("Processing request", "dashboardTitle", req.DashboardJSON["title"], "numMappings", len(req.DatasourceMappings))
|
||||
|
||||
// Get namespace from request (needed for datasource lookup)
|
||||
namespace := r.ResourceIdentifier.Namespace
|
||||
|
||||
for _, dsMapping := range req.DatasourceMappings {
|
||||
// Convert optional name pointer to string
|
||||
name := ""
|
||||
if dsMapping.Name != nil {
|
||||
name = *dsMapping.Name
|
||||
}
|
||||
|
||||
// Fetch datasource from Grafana using app-platform method
|
||||
// Parameters: namespace, name (UID), group (datasource type)
|
||||
ds, err := datasourceSvc.GetDataSourceInNamespace(ctx, namespace, dsMapping.UID, dsMapping.Type)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get datasource", "namespace", namespace, "uid", dsMapping.UID, "type", dsMapping.Type, "error", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": fmt.Sprintf("datasource not found: %s", dsMapping.UID),
|
||||
})
|
||||
}
|
||||
|
||||
logger.Info("Retrieved datasource", "uid", ds.UID, "url", ds.URL, "type", ds.Type)
|
||||
|
||||
// Get authenticated HTTP transport for this datasource
|
||||
transport, err := datasourceSvc.GetHTTPTransport(ctx, ds, httpClientProvider)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get HTTP transport", "uid", ds.UID, "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": fmt.Sprintf("failed to configure datasource transport: %s", dsMapping.UID),
|
||||
})
|
||||
}
|
||||
|
||||
// Create HTTP client with authenticated transport
|
||||
httpClient := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
validatorReq.DatasourceMappings = append(validatorReq.DatasourceMappings, validator.DatasourceMapping{
|
||||
UID: dsMapping.UID,
|
||||
Type: dsMapping.Type,
|
||||
Name: name,
|
||||
URL: ds.URL,
|
||||
HTTPClient: httpClient, // Pass authenticated client
|
||||
})
|
||||
}
|
||||
|
||||
// Step 3: Validate dashboard compatibility
|
||||
result, err := validator.ValidateDashboardCompatibility(ctx, validatorReq)
|
||||
if err != nil {
|
||||
logger.Error("Validation failed", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": fmt.Sprintf("validation failed: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
// Step 4: Convert result to response format
|
||||
response := convertToCheckResponse(result)
|
||||
|
||||
// Step 5: Return response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "Handler working!",
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCheckResponse converts validator result to API response format
|
||||
func convertToCheckResponse(result *validator.DashboardCompatibilityResult) checkResponse {
|
||||
response := checkResponse{
|
||||
CompatibilityScore: result.CompatibilityScore,
|
||||
DatasourceResults: make([]datasourceResult, 0, len(result.DatasourceResults)),
|
||||
}
|
||||
|
||||
for _, dsResult := range result.DatasourceResults {
|
||||
// Convert name string to pointer
|
||||
var name *string
|
||||
if dsResult.Name != "" {
|
||||
name = &dsResult.Name
|
||||
}
|
||||
|
||||
// Convert query results
|
||||
queryBreakdown := make([]queryResult, 0, len(dsResult.QueryBreakdown))
|
||||
for _, qr := range dsResult.QueryBreakdown {
|
||||
queryBreakdown = append(queryBreakdown, queryResult{
|
||||
PanelTitle: qr.PanelTitle,
|
||||
PanelID: qr.PanelID,
|
||||
QueryRefID: qr.QueryRefID,
|
||||
TotalMetrics: qr.TotalMetrics,
|
||||
FoundMetrics: qr.FoundMetrics,
|
||||
MissingMetrics: qr.MissingMetrics,
|
||||
CompatibilityScore: qr.CompatibilityScore,
|
||||
})
|
||||
}
|
||||
|
||||
response.DatasourceResults = append(response.DatasourceResults, datasourceResult{
|
||||
UID: dsResult.UID,
|
||||
Type: dsResult.Type,
|
||||
Name: name,
|
||||
TotalQueries: dsResult.TotalQueries,
|
||||
CheckedQueries: dsResult.CheckedQueries,
|
||||
TotalMetrics: dsResult.TotalMetrics,
|
||||
FoundMetrics: dsResult.FoundMetrics,
|
||||
MissingMetrics: dsResult.MissingMetrics,
|
||||
QueryBreakdown: queryBreakdown,
|
||||
CompatibilityScore: dsResult.CompatibilityScore,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func GetKinds() map[schema.GroupVersion][]resource.Kind {
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DashboardCompatibilityRequest contains the dashboard and datasource mappings to validate
|
||||
type DashboardCompatibilityRequest struct {
|
||||
DashboardJSON map[string]interface{} // Dashboard JSON structure
|
||||
DatasourceMappings []DatasourceMapping // List of datasources to validate against
|
||||
}
|
||||
|
||||
// DatasourceMapping maps a datasource UID to its type and optionally name/URL
|
||||
type DatasourceMapping struct {
|
||||
UID string // Datasource UID
|
||||
Type string // Datasource type (prometheus, mysql, etc.)
|
||||
Name string // Optional: Datasource name
|
||||
URL string // Datasource URL
|
||||
HTTPClient *http.Client // Authenticated HTTP client
|
||||
}
|
||||
|
||||
// DashboardCompatibilityResult contains the validation results for a dashboard
|
||||
type DashboardCompatibilityResult struct {
|
||||
CompatibilityScore float64 // Overall compatibility (0.0 - 1.0)
|
||||
DatasourceResults []DatasourceValidationResult // Per-datasource results
|
||||
}
|
||||
|
||||
// DatasourceValidationResult contains validation results for one datasource
|
||||
type DatasourceValidationResult struct {
|
||||
UID string
|
||||
Type string
|
||||
Name string
|
||||
TotalQueries int
|
||||
CheckedQueries int
|
||||
TotalMetrics int
|
||||
FoundMetrics int
|
||||
MissingMetrics []string
|
||||
QueryBreakdown []QueryResult
|
||||
CompatibilityScore float64
|
||||
}
|
||||
|
||||
// ValidateDashboardCompatibility is the main entry point for validating dashboard compatibility
|
||||
// It extracts queries from the dashboard, validates them against each datasource, and returns aggregated results
|
||||
func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibilityRequest) (*DashboardCompatibilityResult, error) {
|
||||
result := &DashboardCompatibilityResult{
|
||||
DatasourceResults: make([]DatasourceValidationResult, 0, len(req.DatasourceMappings)),
|
||||
}
|
||||
|
||||
// Step 1: Extract queries from dashboard JSON
|
||||
queries, err := extractQueriesFromDashboard(req.DashboardJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract queries from dashboard: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Extracted %d queries from dashboard\n", len(queries))
|
||||
for i, q := range queries {
|
||||
fmt.Printf("[DEBUG] Query %d: DS=%s, RefID=%s, Query=%s\n", i, q.DatasourceUID, q.RefID, q.QueryText)
|
||||
}
|
||||
|
||||
// Step 2: Group queries by datasource UID
|
||||
queriesByDatasource := groupQueriesByDatasource(queries)
|
||||
|
||||
fmt.Printf("[DEBUG] Grouped queries by %d datasources\n", len(queriesByDatasource))
|
||||
for dsUID, dsQueries := range queriesByDatasource {
|
||||
fmt.Printf("[DEBUG] Datasource %s has %d queries\n", dsUID, len(dsQueries))
|
||||
}
|
||||
|
||||
// Step 3: Validate each datasource
|
||||
var totalCompatibility float64
|
||||
validatedCount := 0
|
||||
|
||||
for _, dsMapping := range req.DatasourceMappings {
|
||||
fmt.Printf("[DEBUG] Processing datasource mapping: UID=%s, Type=%s, URL=%s\n", dsMapping.UID, dsMapping.Type, dsMapping.URL)
|
||||
|
||||
// Get queries for this datasource
|
||||
dsQueries, ok := queriesByDatasource[dsMapping.UID]
|
||||
if !ok || len(dsQueries) == 0 {
|
||||
// No queries for this datasource, skip
|
||||
fmt.Printf("[DEBUG] No queries found for datasource %s, skipping\n", dsMapping.UID)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Found %d queries for datasource %s\n", len(dsQueries), dsMapping.UID)
|
||||
|
||||
// Get validator for this datasource type
|
||||
v, err := GetValidator(dsMapping.Type)
|
||||
if err != nil {
|
||||
// Unsupported datasource type, skip but log
|
||||
fmt.Printf("[DEBUG] Failed to get validator for type %s: %v\n", dsMapping.Type, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Got validator for type %s, starting validation\n", dsMapping.Type)
|
||||
|
||||
// Build Datasource struct
|
||||
ds := Datasource{
|
||||
UID: dsMapping.UID,
|
||||
Type: dsMapping.Type,
|
||||
Name: dsMapping.Name,
|
||||
URL: dsMapping.URL,
|
||||
HTTPClient: dsMapping.HTTPClient,
|
||||
}
|
||||
|
||||
// Validate queries
|
||||
validationResult, err := v.ValidateQueries(ctx, dsQueries, ds)
|
||||
if err != nil {
|
||||
// Validation failed for this datasource, skip but could log
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to DatasourceValidationResult
|
||||
dsResult := DatasourceValidationResult{
|
||||
UID: dsMapping.UID,
|
||||
Type: dsMapping.Type,
|
||||
Name: dsMapping.Name,
|
||||
TotalQueries: validationResult.TotalQueries,
|
||||
CheckedQueries: validationResult.CheckedQueries,
|
||||
TotalMetrics: validationResult.TotalMetrics,
|
||||
FoundMetrics: validationResult.FoundMetrics,
|
||||
MissingMetrics: validationResult.MissingMetrics,
|
||||
QueryBreakdown: validationResult.QueryBreakdown,
|
||||
CompatibilityScore: validationResult.CompatibilityScore,
|
||||
}
|
||||
|
||||
result.DatasourceResults = append(result.DatasourceResults, dsResult)
|
||||
totalCompatibility += validationResult.CompatibilityScore
|
||||
validatedCount++
|
||||
}
|
||||
|
||||
// Step 4: Calculate overall compatibility score
|
||||
if validatedCount > 0 {
|
||||
result.CompatibilityScore = totalCompatibility / float64(validatedCount)
|
||||
} else {
|
||||
result.CompatibilityScore = 1.0 // No datasources = perfect compatibility
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractQueriesFromDashboard parses the dashboard JSON and extracts all queries
|
||||
// Supports both v1 (legacy) and v2 (new) dashboard formats
|
||||
func extractQueriesFromDashboard(dashboardJSON map[string]interface{}) ([]DashboardQuery, error) {
|
||||
var queries []DashboardQuery
|
||||
|
||||
// Debug: Print what keys we have
|
||||
fmt.Printf("[DEBUG] Dashboard JSON keys: ")
|
||||
for key := range dashboardJSON {
|
||||
fmt.Printf("%s, ", key)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Detect dashboard version (v1 uses "panels", v2 uses different structure)
|
||||
// For MVP, we only support v1 (legacy format with panels array)
|
||||
if !isV1Dashboard(dashboardJSON) {
|
||||
fmt.Printf("[DEBUG] isV1Dashboard returned false, 'panels' key exists: %v\n", dashboardJSON["panels"] != nil)
|
||||
return nil, fmt.Errorf("unsupported dashboard format: only v1 dashboards are supported in MVP")
|
||||
}
|
||||
|
||||
// Extract panels array
|
||||
panels, ok := dashboardJSON["panels"].([]interface{})
|
||||
if !ok {
|
||||
// No panels in dashboard, return empty array
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// Iterate through all panels
|
||||
for _, panelInterface := range panels {
|
||||
panel, ok := panelInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract queries from this panel
|
||||
panelQueries := extractQueriesFromPanel(panel)
|
||||
queries = append(queries, panelQueries...)
|
||||
|
||||
// Handle nested panels in collapsed rows
|
||||
nestedPanels, hasNested := panel["panels"].([]interface{})
|
||||
if hasNested {
|
||||
for _, nestedPanelInterface := range nestedPanels {
|
||||
nestedPanel, ok := nestedPanelInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nestedQueries := extractQueriesFromPanel(nestedPanel)
|
||||
queries = append(queries, nestedQueries...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// isV1Dashboard checks if a dashboard is in v1 (legacy) format
|
||||
// v1 dashboards have a "panels" array at the top level
|
||||
func isV1Dashboard(dashboard map[string]interface{}) bool {
|
||||
_, hasPanels := dashboard["panels"]
|
||||
return hasPanels
|
||||
}
|
||||
|
||||
// extractQueriesFromPanel extracts all queries/targets from a single panel
|
||||
func extractQueriesFromPanel(panel map[string]interface{}) []DashboardQuery {
|
||||
var queries []DashboardQuery
|
||||
|
||||
// Get panel info for context
|
||||
panelTitle := getStringValue(panel, "title", "Untitled Panel")
|
||||
panelID := getIntValue(panel, "id", 0)
|
||||
|
||||
// Extract targets array (queries)
|
||||
targets, hasTargets := panel["targets"].([]interface{})
|
||||
if !hasTargets {
|
||||
return queries
|
||||
}
|
||||
|
||||
// Iterate through each target/query
|
||||
for _, targetInterface := range targets {
|
||||
target, ok := targetInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract datasource UID
|
||||
datasourceUID := extractDatasourceUID(target, panel)
|
||||
if datasourceUID == "" {
|
||||
// Skip queries without datasource
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract query text (different fields for different datasources)
|
||||
queryText := extractQueryText(target)
|
||||
if queryText == "" {
|
||||
// Skip empty queries
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract refId (A, B, C, etc.)
|
||||
refID := getStringValue(target, "refId", "")
|
||||
|
||||
// Build DashboardQuery
|
||||
query := DashboardQuery{
|
||||
DatasourceUID: datasourceUID,
|
||||
RefID: refID,
|
||||
QueryText: queryText,
|
||||
PanelTitle: panelTitle,
|
||||
PanelID: panelID,
|
||||
}
|
||||
|
||||
queries = append(queries, query)
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
// extractDatasourceUID gets the datasource UID from a target, falling back to panel datasource
|
||||
func extractDatasourceUID(target map[string]interface{}, panel map[string]interface{}) string {
|
||||
// Try target-level datasource first
|
||||
if ds, ok := target["datasource"]; ok {
|
||||
if uid := getDatasourceUIDFromValue(ds); uid != "" {
|
||||
return uid
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to panel-level datasource
|
||||
if ds, ok := panel["datasource"]; ok {
|
||||
if uid := getDatasourceUIDFromValue(ds); uid != "" {
|
||||
return uid
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDatasourceUIDFromValue extracts UID from datasource value (can be string or object)
|
||||
func getDatasourceUIDFromValue(ds interface{}) string {
|
||||
switch v := ds.(type) {
|
||||
case string:
|
||||
// Direct UID string
|
||||
return v
|
||||
case map[string]interface{}:
|
||||
// Structured datasource reference { uid: "...", type: "..." }
|
||||
return getStringValue(v, "uid", "")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// extractQueryText extracts the query text from a target
|
||||
// Different datasources use different field names (expr, query, rawSql, etc.)
|
||||
func extractQueryText(target map[string]interface{}) string {
|
||||
// Try common query field names
|
||||
queryFields := []string{"expr", "query", "rawSql", "rawQuery", "target", "measurement"}
|
||||
|
||||
for _, field := range queryFields {
|
||||
if queryText := getStringValue(target, field, ""); queryText != "" {
|
||||
return queryText
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getStringValue safely extracts a string value from a map
|
||||
func getStringValue(m map[string]interface{}, key string, defaultValue string) string {
|
||||
if value, ok := m[key]; ok {
|
||||
if s, ok := value.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getIntValue safely extracts an int value from a map
|
||||
func getIntValue(m map[string]interface{}, key string, defaultValue int) int {
|
||||
if value, ok := m[key]; ok {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case float64:
|
||||
return int(v)
|
||||
case int64:
|
||||
return int(v)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// DashboardQuery represents a query extracted from a dashboard panel
|
||||
type DashboardQuery struct {
|
||||
DatasourceUID string // Which datasource this query belongs to
|
||||
RefID string // Query reference ID
|
||||
QueryText string // The actual query
|
||||
PanelTitle string // Panel title
|
||||
PanelID int // Panel ID
|
||||
}
|
||||
|
||||
// groupQueriesByDatasource groups dashboard queries by their datasource UID
|
||||
func groupQueriesByDatasource(queries []DashboardQuery) map[string][]Query {
|
||||
grouped := make(map[string][]Query)
|
||||
|
||||
for _, dq := range queries {
|
||||
q := Query{
|
||||
RefID: dq.RefID,
|
||||
QueryText: dq.QueryText,
|
||||
PanelTitle: dq.PanelTitle,
|
||||
PanelID: dq.PanelID,
|
||||
}
|
||||
|
||||
grouped[dq.DatasourceUID] = append(grouped[dq.DatasourceUID], q)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Fetcher fetches available metrics from a Prometheus datasource
|
||||
type Fetcher struct{}
|
||||
|
||||
// NewFetcher creates a new Prometheus metrics fetcher
|
||||
func NewFetcher() *Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
// prometheusResponse represents the Prometheus API response structure
|
||||
type prometheusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []string `json:"data"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// FetchMetrics queries Prometheus to get all available metric names
|
||||
// It uses the /api/v1/label/__name__/values endpoint
|
||||
// The provided HTTP client should have proper authentication configured
|
||||
func (f *Fetcher) FetchMetrics(ctx context.Context, datasourceURL string, client *http.Client) ([]string, error) {
|
||||
// Build the API URL
|
||||
baseURL, err := url.Parse(datasourceURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid datasource URL: %w", err)
|
||||
}
|
||||
|
||||
// Prometheus metrics endpoint
|
||||
apiPath, err := url.Parse("/api/v1/label/__name__/values")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse API path: %w", err)
|
||||
}
|
||||
|
||||
fullURL := baseURL.ResolveReference(apiPath)
|
||||
|
||||
// Create the request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Execute the request using the provided authenticated client
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("Prometheus API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
var promResp prometheusResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&promResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode Prometheus response: %w", err)
|
||||
}
|
||||
|
||||
// Check Prometheus API status
|
||||
if promResp.Status != "success" {
|
||||
return nil, fmt.Errorf("Prometheus API returned error: %s", promResp.Error)
|
||||
}
|
||||
|
||||
return promResp.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
)
|
||||
|
||||
// Parser extracts metric names from PromQL queries
|
||||
type Parser struct{}
|
||||
|
||||
// NewParser creates a new PromQL parser
|
||||
func NewParser() *Parser {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
// ExtractMetrics parses a PromQL query and extracts all metric names
|
||||
// For example: "rate(http_requests_total[5m])" returns ["http_requests_total"]
|
||||
func (p *Parser) ExtractMetrics(query string) ([]string, error) {
|
||||
// Parse the PromQL expression
|
||||
expr, err := parser.ParseExpr(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse PromQL query: %w", err)
|
||||
}
|
||||
|
||||
// Extract metric names by walking the AST
|
||||
metrics := make(map[string]bool) // Use map to deduplicate
|
||||
parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error {
|
||||
// VectorSelector represents a metric selector like "up" or "up{job="foo"}"
|
||||
if vs, ok := node.(*parser.VectorSelector); ok {
|
||||
metrics[vs.Name] = true
|
||||
}
|
||||
// MatrixSelector represents range queries like "up[5m]"
|
||||
if ms, ok := node.(*parser.MatrixSelector); ok {
|
||||
if vs, ok := ms.VectorSelector.(*parser.VectorSelector); ok {
|
||||
metrics[vs.Name] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Convert map to slice
|
||||
result := make([]string, 0, len(metrics))
|
||||
for metric := range metrics {
|
||||
result = append(result, metric)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
|
||||
)
|
||||
|
||||
// Register Prometheus validator on package import
|
||||
func init() {
|
||||
validator.RegisterValidator("prometheus", func() validator.DatasourceValidator {
|
||||
return NewValidator()
|
||||
})
|
||||
}
|
||||
|
||||
// Validator implements validator.DatasourceValidator for Prometheus datasources
|
||||
type Validator struct {
|
||||
parser *Parser
|
||||
fetcher *Fetcher
|
||||
}
|
||||
|
||||
// NewValidator creates a new Prometheus validator
|
||||
func NewValidator() validator.DatasourceValidator {
|
||||
return &Validator{
|
||||
parser: NewParser(),
|
||||
fetcher: NewFetcher(),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateQueries validates Prometheus queries against the datasource
|
||||
func (v *Validator) ValidateQueries(ctx context.Context, queries []validator.Query, datasource validator.Datasource) (*validator.ValidationResult, error) {
|
||||
fmt.Printf("[DEBUG PROM] Starting validation for %d queries against datasource %s\n", len(queries), datasource.URL)
|
||||
|
||||
result := &validator.ValidationResult{
|
||||
TotalQueries: len(queries),
|
||||
QueryBreakdown: make([]validator.QueryResult, 0, len(queries)),
|
||||
}
|
||||
|
||||
// Step 1: Parse all queries to extract metrics
|
||||
allMetrics := make(map[string]bool) // Use map to deduplicate
|
||||
queryMetrics := make(map[int][]string)
|
||||
|
||||
for i, query := range queries {
|
||||
fmt.Printf("[DEBUG PROM] Parsing query %d: %s\n", i, query.QueryText)
|
||||
metrics, err := v.parser.ExtractMetrics(query.QueryText)
|
||||
if err != nil {
|
||||
// If we can't parse the query, we still continue with others
|
||||
// but we don't count this query as "checked"
|
||||
fmt.Printf("[DEBUG PROM] Failed to parse query %d: %v\n", i, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("[DEBUG PROM] Extracted %d metrics from query %d: %v\n", len(metrics), i, metrics)
|
||||
result.CheckedQueries++
|
||||
queryMetrics[i] = metrics
|
||||
|
||||
// Add to global metrics set
|
||||
for _, metric := range metrics {
|
||||
allMetrics[metric] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice for fetcher
|
||||
metricsToCheck := make([]string, 0, len(allMetrics))
|
||||
for metric := range allMetrics {
|
||||
metricsToCheck = append(metricsToCheck, metric)
|
||||
}
|
||||
result.TotalMetrics = len(metricsToCheck)
|
||||
|
||||
fmt.Printf("[DEBUG PROM] Total metrics to check: %d - %v\n", len(metricsToCheck), metricsToCheck)
|
||||
|
||||
// Step 2: Fetch available metrics from Prometheus
|
||||
fmt.Printf("[DEBUG PROM] Fetching available metrics from %s\n", datasource.URL)
|
||||
availableMetrics, err := v.fetcher.FetchMetrics(ctx, datasource.URL, datasource.HTTPClient)
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG PROM] Failed to fetch metrics: %v\n", err)
|
||||
return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err)
|
||||
}
|
||||
fmt.Printf("[DEBUG PROM] Fetched %d available metrics from Prometheus\n", len(availableMetrics))
|
||||
|
||||
// Build a set for O(1) lookup
|
||||
availableSet := make(map[string]bool)
|
||||
for _, metric := range availableMetrics {
|
||||
availableSet[metric] = true
|
||||
}
|
||||
|
||||
// Step 3: Calculate compatibility
|
||||
missingMetricsMap := make(map[string]bool)
|
||||
for _, metric := range metricsToCheck {
|
||||
if !availableSet[metric] {
|
||||
missingMetricsMap[metric] = true
|
||||
}
|
||||
}
|
||||
result.FoundMetrics = result.TotalMetrics - len(missingMetricsMap)
|
||||
|
||||
// Convert missing metrics map to slice
|
||||
result.MissingMetrics = make([]string, 0, len(missingMetricsMap))
|
||||
for metric := range missingMetricsMap {
|
||||
result.MissingMetrics = append(result.MissingMetrics, metric)
|
||||
}
|
||||
|
||||
// Step 4: Build per-query breakdown
|
||||
for i, query := range queries {
|
||||
metrics, ok := queryMetrics[i]
|
||||
if !ok {
|
||||
// Query wasn't parsed successfully, skip
|
||||
continue
|
||||
}
|
||||
|
||||
queryResult := validator.QueryResult{
|
||||
PanelTitle: query.PanelTitle,
|
||||
PanelID: query.PanelID,
|
||||
QueryRefID: query.RefID,
|
||||
TotalMetrics: len(metrics),
|
||||
}
|
||||
|
||||
// Check which metrics from this query are missing
|
||||
queryMissing := make([]string, 0)
|
||||
for _, metric := range metrics {
|
||||
if missingMetricsMap[metric] {
|
||||
queryMissing = append(queryMissing, metric)
|
||||
}
|
||||
}
|
||||
|
||||
queryResult.MissingMetrics = queryMissing
|
||||
queryResult.FoundMetrics = queryResult.TotalMetrics - len(queryMissing)
|
||||
|
||||
// Calculate query-level compatibility score
|
||||
if queryResult.TotalMetrics > 0 {
|
||||
queryResult.CompatibilityScore = float64(queryResult.FoundMetrics) / float64(queryResult.TotalMetrics)
|
||||
} else {
|
||||
queryResult.CompatibilityScore = 1.0 // No metrics = perfect compatibility
|
||||
}
|
||||
|
||||
result.QueryBreakdown = append(result.QueryBreakdown, queryResult)
|
||||
}
|
||||
|
||||
// Step 5: Calculate overall compatibility score
|
||||
if result.TotalMetrics > 0 {
|
||||
result.CompatibilityScore = float64(result.FoundMetrics) / float64(result.TotalMetrics)
|
||||
} else {
|
||||
result.CompatibilityScore = 1.0 // No metrics = perfect compatibility
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG PROM] Validation complete! Score: %.2f, Found: %d/%d metrics\n",
|
||||
result.CompatibilityScore, result.FoundMetrics, result.TotalMetrics)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DatasourceValidator validates dashboard queries against a datasource
|
||||
// Implementations exist per datasource type (Prometheus, MySQL, etc.)
|
||||
type DatasourceValidator interface {
|
||||
// ValidateQueries checks if queries are compatible with the datasource
|
||||
ValidateQueries(ctx context.Context, queries []Query, datasource Datasource) (*ValidationResult, error)
|
||||
}
|
||||
|
||||
// Query represents a dashboard query to validate
|
||||
type Query struct {
|
||||
RefID string // Query reference ID (A, B, C, etc.)
|
||||
QueryText string // The actual query text (PromQL, SQL, etc.)
|
||||
PanelTitle string // Panel title for user-friendly reporting
|
||||
PanelID int // Panel ID for reference
|
||||
}
|
||||
|
||||
// Datasource contains connection information for a datasource
|
||||
type Datasource struct {
|
||||
UID string // Datasource UID from dashboard
|
||||
Type string // Datasource type (prometheus, mysql, etc.)
|
||||
Name string // Datasource name for reporting
|
||||
URL string // Datasource URL for API calls
|
||||
HTTPClient *http.Client // Authenticated HTTP client for making requests
|
||||
}
|
||||
|
||||
// ValidationResult contains validation results for a datasource
|
||||
type ValidationResult struct {
|
||||
TotalQueries int // Total number of queries found
|
||||
CheckedQueries int // Number of queries successfully checked
|
||||
TotalMetrics int // Total metrics/entities referenced
|
||||
FoundMetrics int // Metrics found in datasource
|
||||
MissingMetrics []string // List of missing metrics
|
||||
QueryBreakdown []QueryResult // Per-query results
|
||||
CompatibilityScore float64 // Overall compatibility (0.0 - 1.0)
|
||||
}
|
||||
|
||||
// QueryResult contains validation results for a single query
|
||||
type QueryResult struct {
|
||||
PanelTitle string // Panel title
|
||||
PanelID int // Panel ID
|
||||
QueryRefID string // Query reference ID
|
||||
TotalMetrics int // Metrics in this query
|
||||
FoundMetrics int // Metrics found
|
||||
MissingMetrics []string // Missing metrics for this query
|
||||
CompatibilityScore float64 // Query compatibility (0.0 - 1.0)
|
||||
}
|
||||
|
||||
// validatorRegistry holds registered validator constructors
|
||||
// Validators register themselves using RegisterValidator in their init() functions
|
||||
var validatorRegistry = make(map[string]func() DatasourceValidator)
|
||||
|
||||
// RegisterValidator registers a validator constructor for a datasource type
|
||||
// This is called by validator implementations in their init() functions
|
||||
// Example: validator.RegisterValidator("prometheus", func() validator.DatasourceValidator { return NewValidator() })
|
||||
func RegisterValidator(dsType string, constructor func() DatasourceValidator) {
|
||||
validatorRegistry[dsType] = constructor
|
||||
}
|
||||
|
||||
// GetValidator returns a validator for the given datasource type
|
||||
// Returns an error if the datasource type is not supported
|
||||
func GetValidator(dsType string) (DatasourceValidator, error) {
|
||||
constructor, ok := validatorRegistry[dsType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported datasource type: %s", dsType)
|
||||
}
|
||||
return constructor(), nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
validatorapis "github.com/grafana/grafana/apps/dashvalidator/pkg/apis/manifestdata"
|
||||
validatorapp "github.com/grafana/grafana/apps/dashvalidator/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
@@ -24,11 +25,13 @@ type DashValidatorAppInstaller struct {
|
||||
func RegisterAppInstaller(
|
||||
datasourceSvc datasources.DataSourceService,
|
||||
pluginCtx *plugincontext.Provider,
|
||||
httpClientProvider httpclient.Provider,
|
||||
) (*DashValidatorAppInstaller, error) {
|
||||
// Create specific config for the app
|
||||
specificConfig := &validatorapp.DashValidatorConfig{
|
||||
DatasourceSvc: datasourceSvc,
|
||||
PluginCtx: pluginCtx,
|
||||
DatasourceSvc: datasourceSvc,
|
||||
PluginCtx: pluginCtx,
|
||||
HTTPClientProvider: httpClientProvider,
|
||||
}
|
||||
|
||||
// Create the app provider
|
||||
|
||||
Generated
+2
-2
@@ -832,7 +832,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider)
|
||||
dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider, httpclientProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1498,7 +1498,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider)
|
||||
dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider, httpclientProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user