github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/scraper/catalogue_helpers_test.go (about)

     1  package scraper
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"slices"
    12  	"sort"
    13  	"strings"
    14  	"testing"
    15  
    16  	dto "github.com/prometheus/client_model/go"
    17  	"github.com/prometheus/common/expfmt"
    18  
    19  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    20  	"github.com/grafana/synthetic-monitoring-agent/internal/prober"
    21  )
    22  
    23  type fixtureSetup func(context.Context, *testing.T) (prober.Prober, model.Check, func())
    24  
    25  type fixtureSpec struct {
    26  	setup            fixtureSetup
    27  	basicMetricsOnly bool
    28  }
    29  
    30  type MetricLabelCatalogue map[string][]string
    31  
    32  type MetricLabelMismatch struct {
    33  	MissingLabels    []string
    34  	UnexpectedLabels []string
    35  }
    36  
    37  type CatalogueComparison struct {
    38  	MissingMetrics    []string
    39  	UnexpectedMetrics []string
    40  	LabelMismatches   map[string]MetricLabelMismatch
    41  }
    42  
    43  func validateMetricSetups() map[string]fixtureSetup {
    44  	return map[string]fixtureSetup{
    45  		"ping":       setupPingProbe,
    46  		"http":       setupHTTPProbe,
    47  		"http_ssl":   setupHTTPSSLProbe,
    48  		"dns":        setupDNSProbe,
    49  		"tcp":        setupTCPProbe,
    50  		"tcp_ssl":    setupTCPSSLProbe,
    51  		"traceroute": setupTracerouteProbe,
    52  		"scripted":   setupScriptedProbe,
    53  		"multihttp":  setupMultiHTTPProbe,
    54  		"grpc":       setupGRPCProbe,
    55  		"grpc_ssl":   setupGRPCSSLProbe,
    56  		"browser":    setupBrowserProbe,
    57  	}
    58  }
    59  
    60  func fixtureCatalogueSpecs() map[string]fixtureSpec {
    61  	specs := make(map[string]fixtureSpec)
    62  
    63  	for name, setup := range validateMetricSetups() {
    64  		if name == "traceroute" {
    65  			continue
    66  		}
    67  
    68  		specs[name] = fixtureSpec{setup: setup, basicMetricsOnly: false}
    69  		specs[name+"_basic"] = fixtureSpec{setup: setup, basicMetricsOnly: true}
    70  	}
    71  
    72  	return specs
    73  }
    74  
    75  func runtimeCatalogueSpecs() map[string]fixtureSpec {
    76  	specs := fixtureCatalogueSpecs()
    77  
    78  	return map[string]fixtureSpec{
    79  		"scripted":        specs["scripted"],
    80  		"scripted_basic":  specs["scripted_basic"],
    81  		"multihttp":       specs["multihttp"],
    82  		"multihttp_basic": specs["multihttp_basic"],
    83  		"browser":         specs["browser"],
    84  		"browser_basic":   specs["browser_basic"],
    85  	}
    86  }
    87  
    88  func loadMetricLabelCatalogues(t *testing.T, filename string) map[string]MetricLabelCatalogue {
    89  	t.Helper()
    90  
    91  	fh, err := os.Open(filepath.Join("testdata", filename))
    92  	if err != nil {
    93  		t.Fatalf("open metric catalogue %s: %v", filename, err)
    94  	}
    95  	defer fh.Close()
    96  
    97  	var got map[string]MetricLabelCatalogue
    98  	if err := json.NewDecoder(fh).Decode(&got); err != nil {
    99  		t.Fatalf("decode metric catalogue %s: %v", filename, err)
   100  	}
   101  
   102  	return got
   103  }
   104  
   105  func CatalogueFromMetricFamilies(mfs []*dto.MetricFamily) MetricLabelCatalogue {
   106  	catalogue := make(MetricLabelCatalogue, len(mfs))
   107  
   108  	for _, mf := range mfs {
   109  		if mf == nil || mf.GetName() == "" {
   110  			continue
   111  		}
   112  
   113  		labels := make([]string, 0)
   114  
   115  		for _, metric := range mf.GetMetric() {
   116  			for _, label := range metric.GetLabel() {
   117  				labels = append(labels, label.GetName())
   118  			}
   119  		}
   120  
   121  		catalogue[mf.GetName()] = uniqueSorted(labels)
   122  	}
   123  
   124  	return catalogue
   125  }
   126  
   127  func CatalogueFromTimeseries(tss TimeSeries) MetricLabelCatalogue {
   128  	catalogue := make(MetricLabelCatalogue)
   129  
   130  	for _, ts := range tss {
   131  		metricName := ""
   132  
   133  		labels := make([]string, 0, len(ts.Labels))
   134  		for _, label := range ts.Labels {
   135  			if label.Name == "__name__" {
   136  				metricName = label.Value
   137  				continue
   138  			}
   139  
   140  			labels = append(labels, label.Name)
   141  		}
   142  
   143  		if metricName == "" {
   144  			continue
   145  		}
   146  
   147  		catalogue[metricName] = uniqueSorted(append(catalogue[metricName], labels...))
   148  	}
   149  
   150  	return catalogue
   151  }
   152  
   153  func CatalogueFromReader(r io.Reader) (MetricLabelCatalogue, error) {
   154  	dec := expfmt.NewDecoder(r, expfmt.NewFormat(expfmt.TypeTextPlain))
   155  	mfs := make([]*dto.MetricFamily, 0)
   156  
   157  	for {
   158  		mf := new(dto.MetricFamily)
   159  
   160  		err := dec.Decode(mf)
   161  		if errors.Is(err, io.EOF) {
   162  			return CatalogueFromMetricFamilies(mfs), nil
   163  		}
   164  
   165  		if err != nil {
   166  			return nil, fmt.Errorf("decode Prometheus exposition: %w", err)
   167  		}
   168  
   169  		mfs = append(mfs, mf)
   170  	}
   171  }
   172  
   173  func CompareMetricCatalogue(expected, observed MetricLabelCatalogue) CatalogueComparison {
   174  	result := CatalogueComparison{
   175  		MissingMetrics:    make([]string, 0),
   176  		UnexpectedMetrics: make([]string, 0),
   177  		LabelMismatches:   make(map[string]MetricLabelMismatch),
   178  	}
   179  
   180  	for metric, expectedLabels := range expected {
   181  		observedLabels, ok := observed[metric]
   182  		if !ok {
   183  			result.MissingMetrics = append(result.MissingMetrics, metric)
   184  			continue
   185  		}
   186  
   187  		missingLabels := difference(expectedLabels, observedLabels)
   188  
   189  		unexpectedLabels := difference(observedLabels, expectedLabels)
   190  		if len(missingLabels) == 0 && len(unexpectedLabels) == 0 {
   191  			continue
   192  		}
   193  
   194  		result.LabelMismatches[metric] = MetricLabelMismatch{
   195  			MissingLabels:    missingLabels,
   196  			UnexpectedLabels: unexpectedLabels,
   197  		}
   198  	}
   199  
   200  	for metric := range observed {
   201  		if _, ok := expected[metric]; ok {
   202  			continue
   203  		}
   204  
   205  		result.UnexpectedMetrics = append(result.UnexpectedMetrics, metric)
   206  	}
   207  
   208  	sort.Strings(result.MissingMetrics)
   209  	sort.Strings(result.UnexpectedMetrics)
   210  
   211  	return result
   212  }
   213  
   214  func (r CatalogueComparison) Success() bool {
   215  	return len(r.MissingMetrics) == 0 && len(r.UnexpectedMetrics) == 0 && len(r.LabelMismatches) == 0
   216  }
   217  
   218  func (r CatalogueComparison) Summary() string {
   219  	if r.Success() {
   220  		return ""
   221  	}
   222  
   223  	parts := make([]string, 0, 3)
   224  	if len(r.MissingMetrics) > 0 {
   225  		parts = append(parts, fmt.Sprintf("missing metrics: %s", strings.Join(r.MissingMetrics, ", ")))
   226  	}
   227  
   228  	if len(r.UnexpectedMetrics) > 0 {
   229  		parts = append(parts, fmt.Sprintf("unexpected metrics: %s", strings.Join(r.UnexpectedMetrics, ", ")))
   230  	}
   231  
   232  	if len(r.LabelMismatches) > 0 {
   233  		metrics := make([]string, 0, len(r.LabelMismatches))
   234  		for metric := range r.LabelMismatches {
   235  			metrics = append(metrics, metric)
   236  		}
   237  
   238  		sort.Strings(metrics)
   239  
   240  		mismatches := make([]string, 0, len(metrics))
   241  		for _, metric := range metrics {
   242  			diff := r.LabelMismatches[metric]
   243  			mismatches = append(mismatches, fmt.Sprintf("%s missing=[%s] unexpected=[%s]", metric, strings.Join(diff.MissingLabels, ", "), strings.Join(diff.UnexpectedLabels, ", ")))
   244  		}
   245  
   246  		parts = append(parts, fmt.Sprintf("label mismatches: %s", strings.Join(mismatches, "; ")))
   247  	}
   248  
   249  	return strings.Join(parts, " | ")
   250  }
   251  
   252  func difference(left, right []string) []string {
   253  	result := make([]string, 0)
   254  
   255  	for _, item := range uniqueSorted(left) {
   256  		if slices.Contains(right, item) {
   257  			continue
   258  		}
   259  
   260  		result = append(result, item)
   261  	}
   262  
   263  	return result
   264  }
   265  
   266  func uniqueSorted(items []string) []string {
   267  	if len(items) == 0 {
   268  		return []string{}
   269  	}
   270  
   271  	sorted := append([]string(nil), items...)
   272  	sort.Strings(sorted)
   273  
   274  	out := sorted[:1]
   275  	for _, item := range sorted[1:] {
   276  		if item == out[len(out)-1] {
   277  			continue
   278  		}
   279  
   280  		out = append(out, item)
   281  	}
   282  
   283  	return out
   284  }