SuggestedDashboards- Extend dashvalidator POC: Prometheus validator
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user