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 }