SuggestedDashboards- Extend dashvalidator POC: Prometheus validator

This commit is contained in:
alexandra vargas
2025-12-31 17:06:49 +01:00
parent 9d980a9244
commit 8a0f2fa9f3
10 changed files with 923 additions and 11 deletions
@@ -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
}