github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/scraper/scraper_test.go (about) 1 package scraper 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/tls" 7 "errors" 8 "flag" 9 "fmt" 10 "io" 11 "maps" 12 "net" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "path/filepath" 17 "slices" 18 "sort" 19 "strconv" 20 "strings" 21 "sync/atomic" 22 "testing" 23 "time" 24 25 "github.com/go-logfmt/logfmt" 26 "github.com/google/uuid" 27 logproto "github.com/grafana/loki/pkg/push" 28 "github.com/grafana/synthetic-monitoring-agent/internal/k6runner" 29 "github.com/grafana/synthetic-monitoring-agent/internal/model" 30 "github.com/grafana/synthetic-monitoring-agent/internal/prober" 31 "github.com/grafana/synthetic-monitoring-agent/internal/prober/browser" 32 dnsProber "github.com/grafana/synthetic-monitoring-agent/internal/prober/dns" 33 grpcProber "github.com/grafana/synthetic-monitoring-agent/internal/prober/grpc" 34 httpProber "github.com/grafana/synthetic-monitoring-agent/internal/prober/http" 35 "github.com/grafana/synthetic-monitoring-agent/internal/prober/icmp" 36 "github.com/grafana/synthetic-monitoring-agent/internal/prober/logger" 37 "github.com/grafana/synthetic-monitoring-agent/internal/prober/multihttp" 38 "github.com/grafana/synthetic-monitoring-agent/internal/prober/scripted" 39 "github.com/grafana/synthetic-monitoring-agent/internal/prober/tcp" 40 "github.com/grafana/synthetic-monitoring-agent/internal/prober/traceroute" 41 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 42 "github.com/grafana/synthetic-monitoring-agent/internal/telemetry" 43 "github.com/grafana/synthetic-monitoring-agent/internal/testhelper" 44 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 45 "github.com/miekg/dns" 46 "github.com/mmcloughlin/geohash" 47 "github.com/prometheus/client_golang/prometheus" 48 dto "github.com/prometheus/client_model/go" 49 "github.com/prometheus/common/expfmt" 50 prom "github.com/prometheus/common/model" 51 "github.com/prometheus/prometheus/model/labels" 52 "github.com/prometheus/prometheus/prompb" 53 "github.com/prometheus/prometheus/promql/parser" 54 "github.com/rs/zerolog" 55 "github.com/stretchr/testify/require" 56 "google.golang.org/grpc" 57 "google.golang.org/grpc/credentials" 58 grpchealth "google.golang.org/grpc/health/grpc_health_v1" 59 "kernel.org/pub/linux/libs/security/libcap/cap" 60 ) 61 62 var updateGolden = flag.Bool("update-golden", false, "update golden files") 63 64 var metricParser = parser.NewParser(parser.Options{ 65 EnableExperimentalFunctions: false, 66 ExperimentalDurationExpr: false, 67 EnableExtendedRangeSelectors: false, 68 EnableBinopFillModifiers: false, 69 }) 70 71 // TestValidateMetrics verify that the exposed metrics have not changed. 72 // 73 // It does this by setting up local targets (HTTP server, DNS server, 74 // TCP server) that the BBE probers can run against. The resulting 75 // metrics are then compared (only names and labels, not values) against 76 // known outputs. If metrics are added or removed, this test will fail. 77 // 78 // The golden files can be updated by running: 79 // 80 // go test -v -race -run TestValidateMetrics ./internal/scraper/ 81 func TestValidateMetrics(t *testing.T) { 82 for name, setup := range validateMetricSetups() { 83 t.Run(name, func(t *testing.T) { 84 verifyProberMetrics(t, name, setup, false) 85 }) 86 t.Run(name+"_basic", func(t *testing.T) { 87 verifyProberMetrics(t, name+"_basic", setup, true) 88 }) 89 } 90 } 91 92 // verifyProberMetrics runs the specified prober against the server (if 93 // any) started by the setup function. The resup function provides the 94 // target as well as a clean up function that will be called once the 95 // test ends. 96 // 97 // Optionally, this function will update the golden files if the 98 // -update-golden flag was passed to the test. 99 func verifyProberMetrics( 100 t *testing.T, 101 name string, 102 setup func(context.Context, *testing.T) (prober.Prober, model.Check, func()), 103 basicMetricsOnly bool, 104 ) { 105 timeout := 10 * time.Second 106 107 ctx, cancel := context.WithTimeout(context.Background(), timeout) 108 defer cancel() 109 110 summaries := make(map[uint64]prometheus.Summary) 111 histograms := make(map[uint64]prometheus.Histogram) 112 logger := &testLogger{w: io.Discard} 113 114 if os.Getenv("CI") == "true" { 115 logger.w = os.Stdout 116 } 117 118 prober, check, stop := setup(ctx, t) 119 defer stop() 120 121 success, mfs, err := getProbeMetrics( 122 ctx, 123 prober, 124 check.Target, 125 timeout, 126 nil, 127 summaries, 128 histograms, 129 logger, 130 basicMetricsOnly, 131 "test-execution-id", 132 scheduledScrapeClock(time.Now()), 133 ) 134 135 require.NoError(t, err, "probe failed") 136 require.Truef(t, success, "probe failed: %s", prober.Name()) 137 138 fn := filepath.Join("testdata", name+".txt") 139 140 if *updateGolden { 141 var buf bytes.Buffer 142 143 enc := expfmt.NewEncoder(&buf, expfmt.NewFormat(expfmt.TypeTextPlain)) 144 145 for _, m := range mfs { 146 if err := enc.Encode(m); err != nil { 147 t.Fatalf("encoding golden file: %s", err.Error()) 148 } 149 } 150 151 fh, err := os.Create(fn) 152 if err != nil { 153 t.Fatalf("cannot create file %s: %s", fn, err.Error()) 154 } 155 defer fh.Close() 156 157 if _, err := buf.WriteTo(fh); err != nil { 158 t.Fatalf("cannot write to file %s: %s", fn, err.Error()) 159 } 160 161 return 162 } 163 164 actualMetrics := map[string]struct{}{} 165 166 for _, m := range mfs { 167 addMetricToIndex(m, actualMetrics) 168 } 169 170 expectedMetrics, err := readGoldenFile(fn) 171 require.NoError(t, err, "reading golden file") 172 173 require.Equal(t, expectedMetrics, actualMetrics, "maps must be equal") 174 } 175 176 func addMetricToIndex(mf *dto.MetricFamily, index map[string]struct{}) { 177 for _, metric := range mf.GetMetric() { 178 labels := make([]string, 0, len(metric.GetLabel())) 179 for _, label := range metric.GetLabel() { 180 labels = append(labels, fmt.Sprintf(`%q=%q`, label.GetName(), label.GetValue())) 181 } 182 183 sort.Strings(labels) 184 index[mf.GetName()+"{"+strings.Join(labels, ",")+"}"] = struct{}{} 185 } 186 } 187 188 func readGoldenFile(fn string) (map[string]struct{}, error) { 189 fh, err := os.Open(fn) 190 if err != nil { 191 return nil, fmt.Errorf("cannot open file %s: %w", fn, err) 192 } 193 defer fh.Close() 194 195 dec := expfmt.NewDecoder(fh, expfmt.NewFormat(expfmt.TypeTextPlain)) 196 197 metrics := map[string]struct{}{} 198 199 for { 200 var mf dto.MetricFamily 201 202 switch err := dec.Decode(&mf); { 203 case errors.Is(err, io.EOF): 204 return metrics, nil 205 206 case err != nil: 207 return nil, fmt.Errorf("cannot decode golden file %s: %w", fn, err) 208 } 209 210 addMetricToIndex(&mf, metrics) 211 } 212 } 213 214 func setupPingProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 215 check := model.Check{ 216 Check: sm.Check{ 217 Target: "127.0.0.1", 218 Timeout: 2000, 219 Settings: sm.CheckSettings{ 220 Ping: &sm.PingSettings{ 221 IpVersion: sm.IpVersion_V4, 222 }, 223 }, 224 }, 225 } 226 227 prober, err := icmp.NewProber(check) 228 if err != nil { 229 t.Fatalf("cannot create ICMP prober: %s", err) 230 } 231 232 return prober, check, func() {} 233 } 234 235 func setupHTTPProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 236 httpSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 237 w.WriteHeader(http.StatusOK) 238 })) 239 httpSrv.Start() 240 241 check := model.Check{ 242 Check: sm.Check{ 243 Target: httpSrv.URL, 244 Timeout: 2000, 245 Settings: sm.CheckSettings{ 246 Http: &sm.HttpSettings{ 247 IpVersion: sm.IpVersion_V4, 248 }, 249 }, 250 }, 251 } 252 253 var store testhelper.NoopSecretStore 254 255 prober, err := httpProber.NewProber( 256 ctx, 257 check, 258 zerolog.New(io.Discard), 259 http.Header{}, 260 store, 261 ) 262 if err != nil { 263 t.Fatalf("cannot create HTTP prober: %s", err) 264 } 265 266 return prober, check, httpSrv.Close 267 } 268 269 // TestHTTPTimeoutTelemetryContract pins the production HTTP prober behavior 270 // that deterministic history generators must reproduce. A request that does 271 // not receive response headers before the deadline has no HTTP status, emits 272 // the request-error log, and reports failure at approximately the timeout. 273 func TestHTTPTimeoutTelemetryContract(t *testing.T) { 274 const timeout = 75 * time.Millisecond 275 276 httpSrv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { 277 <-req.Context().Done() 278 })) 279 defer httpSrv.Close() 280 281 check := model.Check{Check: sm.Check{ 282 Target: httpSrv.URL, 283 Timeout: timeout.Milliseconds(), 284 Settings: sm.CheckSettings{Http: &sm.HttpSettings{ 285 IpVersion: sm.IpVersion_V4, 286 }}, 287 }} 288 289 var store testhelper.NoopSecretStore 290 291 p, err := httpProber.NewProber(context.Background(), check, zerolog.New(io.Discard), http.Header{}, store) 292 require.NoError(t, err) 293 294 var logs bytes.Buffer 295 296 success, mfs, err := getProbeMetrics( 297 context.Background(), 298 p, 299 check.Target, 300 timeout, 301 nil, 302 make(map[uint64]prometheus.Summary), 303 make(map[uint64]prometheus.Histogram), 304 &testLogger{w: &logs}, 305 false, 306 "timeout-contract", 307 scheduledScrapeClock(time.Now()), 308 ) 309 require.NoError(t, err) 310 require.False(t, success) 311 require.Zero(t, metricGaugeValue(t, mfs, "probe_success", nil)) 312 require.Zero(t, metricGaugeValue(t, mfs, "probe_http_status_code", nil)) 313 require.Zero(t, metricGaugeValue(t, mfs, "probe_http_duration_seconds", map[string]string{"phase": "processing"})) 314 require.Zero(t, metricGaugeValue(t, mfs, "probe_http_duration_seconds", map[string]string{"phase": "transfer"})) 315 require.InDelta(t, timeout.Seconds(), metricGaugeValue(t, mfs, "probe_duration_seconds", nil), 0.050) 316 require.Contains(t, logs.String(), "Error for HTTP request") 317 require.NotContains(t, logs.String(), "Received HTTP response") 318 } 319 320 func metricGaugeValue(t *testing.T, families []*dto.MetricFamily, name string, labels map[string]string) float64 { 321 t.Helper() 322 323 for _, family := range families { 324 if family.GetName() != name { 325 continue 326 } 327 328 for _, metric := range family.GetMetric() { 329 matched := true 330 331 for label, value := range labels { 332 found := false 333 334 for _, pair := range metric.GetLabel() { 335 if pair.GetName() == label && pair.GetValue() == value { 336 found = true 337 break 338 } 339 } 340 341 if !found { 342 matched = false 343 break 344 } 345 } 346 347 if matched && metric.GetGauge() != nil { 348 return metric.GetGauge().GetValue() 349 } 350 } 351 } 352 353 t.Fatalf("metric %q with labels %v not found", name, labels) 354 355 return 0 356 } 357 358 func setupHTTPSSLProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 359 httpSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 360 w.Header().Set("Last-Modified", "Wed, 16 Apr 2026 19:00:00 GMT") 361 w.Header().Set("ETag", `"fixture-http-ssl"`) 362 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 363 w.WriteHeader(http.StatusOK) 364 _, _ = io.WriteString(w, "fixture ok") 365 })) 366 httpSrv.StartTLS() 367 368 check := model.Check{ 369 Check: sm.Check{ 370 Target: httpSrv.URL, 371 Timeout: 2000, 372 Settings: sm.CheckSettings{ 373 Http: &sm.HttpSettings{ 374 IpVersion: sm.IpVersion_V4, 375 TlsConfig: &sm.TLSConfig{ 376 InsecureSkipVerify: true, 377 }, 378 }, 379 }, 380 }, 381 } 382 383 var store testhelper.NoopSecretStore 384 385 prober, err := httpProber.NewProber( 386 ctx, 387 check, 388 zerolog.New(io.Discard), 389 http.Header{}, 390 store, 391 ) 392 if err != nil { 393 t.Fatalf("cannot create HTTP prober: %s", err) 394 } 395 396 return prober, check, httpSrv.Close 397 } 398 399 func setupDNSProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 400 srv, clean := setupDNSServer(t) 401 check := model.Check{ 402 Check: sm.Check{ 403 Target: "example.org", 404 Timeout: 2000, 405 Settings: sm.CheckSettings{ 406 Dns: &sm.DnsSettings{ 407 Server: srv, 408 IpVersion: sm.IpVersion_V4, 409 Protocol: sm.DnsProtocol_UDP, 410 RecordType: sm.DnsRecordType_SOA, 411 }, 412 }, 413 }, 414 } 415 416 prober, err := dnsProber.NewProber(check) 417 if err != nil { 418 clean() 419 t.Fatalf("cannot create DNS prober: %s", err) 420 } 421 422 return prober, check, clean 423 } 424 425 func setupTCPProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 426 srv, clean := setupTCPServer(t) 427 check := model.Check{ 428 Check: sm.Check{ 429 Target: srv, 430 Timeout: 2000, 431 Settings: sm.CheckSettings{ 432 Tcp: &sm.TcpSettings{ 433 IpVersion: sm.IpVersion_V4, 434 }, 435 }, 436 }, 437 } 438 439 prober, err := tcp.NewProber( 440 ctx, 441 check, 442 zerolog.New(io.Discard)) 443 if err != nil { 444 clean() 445 t.Fatalf("cannot create TCP prober: %s", err) 446 } 447 448 return prober, check, clean 449 } 450 451 func setupTCPSSLProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 452 srv, clean := setupTCPServerWithSSL(t) 453 check := model.Check{ 454 Check: sm.Check{ 455 Target: srv, 456 Timeout: 2000, 457 Settings: sm.CheckSettings{ 458 Tcp: &sm.TcpSettings{ 459 IpVersion: sm.IpVersion_V4, 460 Tls: true, 461 TlsConfig: &sm.TLSConfig{ 462 InsecureSkipVerify: true, 463 }, 464 }, 465 }, 466 }, 467 } 468 469 prober, err := tcp.NewProber( 470 ctx, 471 check, 472 zerolog.New(io.Discard)) 473 if err != nil { 474 clean() 475 t.Fatalf("cannot create TCP prober: %s", err) 476 } 477 478 return prober, check, clean 479 } 480 481 func setupTracerouteProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 482 checkCap := func(set *cap.Set, v cap.Value) { 483 if permitted, err := set.GetFlag(cap.Permitted, v); err != nil { 484 t.Fatalf("cannot get %s flag: %s", v, err) 485 } else if !permitted { 486 t.Skipf("traceroute cannot run, process doesn't have %s capability", v) 487 } 488 } 489 c := cap.GetProc() 490 checkCap(c, cap.NET_ADMIN) 491 checkCap(c, cap.NET_RAW) 492 493 check := model.Check{ 494 Check: sm.Check{ 495 Target: "127.0.0.1", 496 Settings: sm.CheckSettings{ 497 Traceroute: &sm.TracerouteSettings{}, 498 }, 499 }, 500 } 501 502 p, err := traceroute.NewProber(check, zerolog.New(io.Discard)) 503 if err != nil { 504 t.Fatalf("cannot create traceroute prober %s", err) 505 } 506 507 return p, check, func() {} 508 } 509 510 func setupScriptedProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 511 httpSrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 512 w.Header().Set("Content-Type", "text/plain") 513 w.WriteHeader(http.StatusOK) 514 _, _ = w.Write([]byte("scripted fixture ok")) 515 })) 516 517 check := model.Check{ 518 Check: sm.Check{ 519 Target: httpSrv.URL, 520 Timeout: 2000, 521 Settings: sm.CheckSettings{ 522 Scripted: &sm.ScriptedSettings{ 523 Script: []byte(fmt.Sprintf(` 524 import http from 'k6/http'; 525 import { check } from 'k6'; 526 527 export const options = { 528 insecureSkipTLSVerify: true, 529 }; 530 531 export default function() { 532 const response = http.get(%q); 533 check(response, { 534 ['status is 200']: (r) => r.status === 200, 535 }); 536 } 537 `, httpSrv.URL)), 538 }, 539 }, 540 }, 541 } 542 543 var ( 544 runner k6runner.Runner 545 err error 546 ) 547 548 if k6Path := os.Getenv("K6_PATH"); k6Path != "" { 549 runner, err = k6runner.New(k6runner.RunnerOpts{Uri: k6Path}) 550 } else { 551 runner = &testRunner{ 552 metrics: testhelper.MustReadFile(t, "testdata/k6.dat"), 553 logs: nil, 554 } 555 } 556 557 if err != nil { 558 t.Fatalf("creating runner: %v", err) 559 } 560 561 var store testhelper.NoopSecretStore 562 563 prober, err := scripted.NewProber( 564 ctx, 565 check, 566 zerolog.New(zerolog.NewTestWriter(t)), 567 runner, 568 store, 569 ) 570 if err != nil { 571 t.Fatalf("cannot create scripted prober: %s", err) 572 } 573 574 return prober, check, httpSrv.Close 575 } 576 577 func setupMultiHTTPProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 578 httpSrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 579 w.Header().Set("Content-Type", "application/json") 580 w.WriteHeader(http.StatusOK) 581 _, _ = w.Write([]byte(`{"status":"ok","message":"multihttp fixture ok"}`)) 582 })) 583 584 check := model.Check{ 585 Check: sm.Check{ 586 Target: httpSrv.URL, 587 Timeout: 2000, 588 Settings: sm.CheckSettings{ 589 Multihttp: &sm.MultiHttpSettings{ 590 Entries: []*sm.MultiHttpEntry{ 591 { 592 Request: &sm.MultiHttpEntryRequest{ 593 Method: sm.HttpMethod_GET, 594 Url: httpSrv.URL, 595 }, 596 Assertions: []*sm.MultiHttpEntryAssertion{ 597 { 598 Type: sm.MultiHttpEntryAssertionType_TEXT, 599 Subject: sm.MultiHttpEntryAssertionSubjectVariant_HTTP_STATUS_CODE, 600 Condition: sm.MultiHttpEntryAssertionConditionVariant_EQUALS, 601 Value: "200", 602 }, 603 { 604 Type: sm.MultiHttpEntryAssertionType_TEXT, 605 Subject: sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 606 Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS, 607 Value: "multihttp fixture ok", 608 }, 609 }, 610 }, 611 }, 612 }, 613 }, 614 }, 615 } 616 617 var ( 618 runner k6runner.Runner 619 err error 620 ) 621 622 if k6Path := os.Getenv("K6_PATH"); k6Path != "" { 623 t.Setenv("K6_INSECURE_SKIP_TLS_VERIFY", "true") 624 625 runner, err = k6runner.New(k6runner.RunnerOpts{Uri: k6Path}) 626 } else { 627 runner = &testRunner{ 628 metrics: testhelper.MustReadFile(t, "testdata/multihttp.dat"), 629 logs: nil, 630 } 631 } 632 633 if err != nil { 634 t.Fatalf("building k6 runner: %v", err) 635 } 636 637 prober, err := multihttp.NewProber( 638 ctx, 639 check, 640 zerolog.New(zerolog.NewTestWriter(t)), 641 runner, 642 http.Header{}, 643 testhelper.NoopSecretStore{}, 644 ) 645 if err != nil { 646 t.Fatalf("cannot create MultiHTTP prober: %s", err) 647 } 648 649 return prober, check, httpSrv.Close 650 } 651 652 func setupBrowserProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 653 mux := http.NewServeMux() 654 mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 655 w.Header().Set("Content-Type", "text/html; charset=utf-8") 656 _, _ = io.WriteString(w, `<!doctype html> 657 <html> 658 <head> 659 <title>browser fixture</title> 660 <link rel="stylesheet" href="/style.css"> 661 <script defer src="/app.js"></script> 662 </head> 663 <body> 664 <h1 id="title">Synthetic Monitoring</h1> 665 <p id="status">browser fixture ok</p> 666 <button id="interact" type="button">Interact</button> 667 <img src="/pixel.png" alt="pixel"> 668 </body> 669 </html>`) 670 }) 671 mux.HandleFunc("/style.css", func(w http.ResponseWriter, req *http.Request) { 672 w.Header().Set("Content-Type", "text/css") 673 _, _ = io.WriteString(w, "body { font-family: sans-serif; }") 674 }) 675 mux.HandleFunc("/app.js", func(w http.ResponseWriter, req *http.Request) { 676 w.Header().Set("Content-Type", "application/javascript") 677 _, _ = io.WriteString(w, `window.addEventListener('load', () => { 678 const status = document.getElementById('status'); 679 const button = document.getElementById('interact'); 680 if (status) status.textContent = 'browser fixture ready'; 681 if (button && status) { 682 button.addEventListener('click', () => { 683 const start = performance.now(); 684 while (performance.now() - start < 120) { 685 // Busy wait long enough to ensure the browser runtime records an input delay. 686 } 687 requestAnimationFrame(() => { 688 status.textContent = 'interaction complete'; 689 }); 690 }); 691 } 692 });`) 693 }) 694 mux.HandleFunc("/pixel.png", func(w http.ResponseWriter, req *http.Request) { 695 w.Header().Set("Content-Type", "image/png") 696 _, _ = w.Write([]byte{ 697 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 698 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 699 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 700 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 701 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 702 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 703 0x00, 0x03, 0x01, 0x01, 0x00, 0xc9, 0xfe, 0x92, 704 0xef, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 705 0x44, 0xae, 0x42, 0x60, 0x82, 706 }) 707 }) 708 httpSrv := httptest.NewServer(mux) 709 710 check := model.Check{ 711 Check: sm.Check{ 712 Target: httpSrv.URL, 713 Timeout: 10000, 714 Settings: sm.CheckSettings{ 715 Browser: &sm.BrowserSettings{ 716 Script: []byte(fmt.Sprintf(` 717 import { browser } from 'k6/browser'; 718 import { check, sleep } from 'k6'; 719 720 export const options = { 721 scenarios: { 722 ui: { 723 executor: 'shared-iterations', 724 options: { 725 browser: { 726 type: 'chromium', 727 }, 728 }, 729 }, 730 }, 731 }; 732 733 export default async function () { 734 const context = await browser.newContext(); 735 const page = await context.newPage(); 736 737 try { 738 await page.goto(%q, { waitUntil: 'networkidle' }); 739 const title = await page.locator('#title').textContent(); 740 check(title, { 741 ['heading is present']: (value) => value === 'Synthetic Monitoring', 742 }); 743 const button = page.locator('#interact'); 744 await button.click(); 745 await page.waitForFunction(() => { 746 const status = document.getElementById('status'); 747 return status && status.textContent === 'interaction complete'; 748 }); 749 sleep(2); 750 } finally { 751 await page.close(); 752 } 753 } 754 `, httpSrv.URL)), 755 }, 756 }, 757 }, 758 } 759 760 var ( 761 runner k6runner.Runner 762 err error 763 ) 764 765 if k6Path := os.Getenv("K6_PATH"); k6Path != "" { 766 runner, err = k6runner.New(k6runner.RunnerOpts{Uri: k6Path}) 767 } else { 768 runner = &testRunner{ 769 metrics: testhelper.MustReadFile(t, "testdata/browser.dat"), 770 logs: nil, 771 } 772 } 773 774 if err != nil { 775 t.Fatalf("building k6 runner: %v", err) 776 } 777 778 prober, err := browser.NewProber( 779 ctx, 780 check, 781 zerolog.New(zerolog.NewTestWriter(t)), 782 runner, 783 testhelper.NoopSecretStore{}, 784 ) 785 if err != nil { 786 t.Fatalf("cannot create scripted prober: %s", err) 787 } 788 789 return prober, check, httpSrv.Close 790 } 791 792 func setupGRPCProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 793 srv, clean := setupGRPCServer(t) 794 check := model.Check{ 795 Check: sm.Check{ 796 Target: srv, 797 Timeout: 2000, 798 Settings: sm.CheckSettings{ 799 Grpc: &sm.GrpcSettings{ 800 IpVersion: sm.IpVersion_V4, 801 }, 802 }, 803 }, 804 } 805 806 prober, err := grpcProber.NewProber( 807 ctx, 808 check, 809 zerolog.New(io.Discard)) 810 if err != nil { 811 clean() 812 t.Fatalf("cannot create gRPC prober: %s", err) 813 } 814 815 return prober, check, clean 816 } 817 818 func setupGRPCSSLProbe(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) { 819 srv, clean := setupGRPCServerWithSSL(t) 820 check := model.Check{ 821 Check: sm.Check{ 822 Target: srv, 823 Timeout: 2000, 824 Settings: sm.CheckSettings{ 825 Grpc: &sm.GrpcSettings{ 826 IpVersion: sm.IpVersion_V4, 827 Tls: true, 828 TlsConfig: &sm.TLSConfig{ 829 InsecureSkipVerify: true, 830 }, 831 }, 832 }, 833 }, 834 } 835 836 prober, err := grpcProber.NewProber( 837 ctx, 838 check, 839 zerolog.New(io.Discard)) 840 if err != nil { 841 clean() 842 t.Fatalf("cannot create gRPC prober: %s", err) 843 } 844 845 return prober, check, clean 846 } 847 848 func setupDNSServer(t *testing.T) (string, func()) { 849 dnsSrv, dnsAddr := startDNSServer(":0", "udp", recursiveDNSHandler) 850 851 errCh := make(chan error) 852 853 go func() { 854 dnsSrv.NotifyStartedFunc = func() { 855 close(errCh) 856 } 857 858 err := dnsSrv.ActivateAndServe() 859 if err != nil { 860 errCh <- err 861 862 close(errCh) 863 } 864 }() 865 866 if err := <-errCh; err != nil { 867 t.Fatalf("error activating DNS server: %s", err.Error()) 868 } 869 870 return dnsAddr.String(), func() { 871 err := dnsSrv.Shutdown() 872 if err != nil { 873 // this should never happen, but log it if it does 874 t.Fatalf("error shutting down DNS server: %s", err.Error()) 875 } 876 } 877 } 878 879 // startDNSServer starts a DNS server with a given handler function on a random port. 880 // Returns the Server object itself as well as the net.Addr corresponding to the server port. 881 func startDNSServer(addr, protocol string, handler func(dns.ResponseWriter, *dns.Msg)) (*dns.Server, net.Addr) { 882 h := dns.NewServeMux() 883 h.HandleFunc(".", handler) 884 885 server := &dns.Server{Net: protocol, Handler: h} 886 switch protocol { 887 case "udp", "udp4", "udp6": 888 a, err := net.ResolveUDPAddr(server.Net, server.Addr) 889 if err != nil { 890 panic(err) 891 } 892 893 l, err := net.ListenUDP(server.Net, a) 894 if err != nil { 895 panic(err) 896 } 897 898 server.Addr = l.LocalAddr().String() 899 server.PacketConn = l 900 case "tcp", "tcp4", "tcp6": 901 a, err := net.ResolveTCPAddr(server.Net, server.Addr) 902 if err != nil { 903 panic(err) 904 } 905 906 l, err := net.ListenTCP(server.Net, a) 907 if err != nil { 908 panic(err) 909 } 910 911 server.Addr = l.Addr().String() 912 server.Listener = l 913 default: 914 panic("unknown protocol") 915 } 916 917 if protocol == "tcp" { 918 return server, server.Listener.Addr() 919 } 920 921 return server, server.PacketConn.LocalAddr() 922 } 923 924 func recursiveDNSHandler(w dns.ResponseWriter, r *dns.Msg) { 925 m := new(dns.Msg) 926 m.SetReply(r) 927 928 if len(r.Question) == 0 { 929 if err := w.WriteMsg(m); err != nil { 930 panic(err) 931 } 932 933 return 934 } 935 936 var answers []string 937 938 switch r.Question[0].Qtype { 939 case dns.TypeSOA: 940 answers = []string{ 941 "example.org. 3600 IN SOA ns1.example.org. hostmaster.example.org. 2026041601 7200 3600 1209600 3600", 942 } 943 default: 944 answers = []string{ 945 "example.org. 3600 IN A 127.0.0.1", 946 "example.org. 3600 IN A 127.0.0.2", 947 } 948 } 949 950 for _, rr := range answers { 951 a, err := dns.NewRR(rr) 952 if err != nil { 953 panic(err) 954 } 955 956 m.Answer = append(m.Answer, a) 957 } 958 959 if err := w.WriteMsg(m); err != nil { 960 panic(err) 961 } 962 } 963 964 func setupTCPServer(t *testing.T) (string, func()) { 965 ln, err := net.Listen("tcp4", ":0") 966 if err != nil { 967 t.Fatalf("Error listening on socket: %s", err) 968 } 969 970 done := make(chan (struct{})) 971 972 go func() { 973 conn, err := ln.Accept() 974 if err != nil { 975 panic(fmt.Sprintf("Error accepting on socket: %s", err)) 976 } 977 978 defer func() { 979 conn.Close() 980 close(done) 981 }() 982 983 buf := make([]byte, 0, 1024) 984 _, _ = conn.Read(buf) 985 }() 986 987 return ln.Addr().String(), func() { 988 <-done 989 ln.Close() 990 } 991 } 992 993 func setupTCPServerWithSSL(t *testing.T) (string, func()) { 994 ln, err := net.Listen("tcp4", ":0") 995 if err != nil { 996 t.Fatalf("Error listening on socket: %s", err) 997 } 998 999 cert, err := tls.X509KeyPair(localhostCert, localhostKey) 1000 if err != nil { 1001 t.Fatalf("creating X509 key pair: %s", err.Error()) 1002 } 1003 1004 tlsCfg := tls.Config{ 1005 Certificates: []tls.Certificate{cert}, 1006 NextProtos: []string{"foo"}, 1007 } 1008 1009 ln = tls.NewListener(ln, &tlsCfg) 1010 1011 done := make(chan (struct{})) 1012 1013 go func() { 1014 conn, err := ln.Accept() 1015 if err != nil { 1016 panic(fmt.Sprintf("Error accepting on socket: %s", err)) 1017 } 1018 1019 defer func() { 1020 conn.Close() 1021 close(done) 1022 }() 1023 1024 buf := make([]byte, 0, 1024) 1025 _, _ = conn.Read(buf) 1026 }() 1027 1028 return ln.Addr().String(), func() { 1029 <-done 1030 ln.Close() 1031 } 1032 } 1033 1034 type gRPCSrv struct { 1035 grpchealth.HealthServer 1036 } 1037 1038 func (s *gRPCSrv) Check( 1039 context.Context, *grpchealth.HealthCheckRequest, 1040 ) (*grpchealth.HealthCheckResponse, error) { 1041 return &grpchealth.HealthCheckResponse{ 1042 Status: grpchealth.HealthCheckResponse_SERVING, 1043 }, nil 1044 } 1045 1046 func setupGRPCServer(t *testing.T) (string, func()) { 1047 lis, err := net.Listen("tcp4", ":0") 1048 if err != nil { 1049 t.Fatalf("Error listening on socket: %v", err) 1050 } 1051 1052 srv := grpc.NewServer() 1053 grpchealth.RegisterHealthServer(srv, &gRPCSrv{}) 1054 1055 go func() { 1056 _ = srv.Serve(lis) 1057 }() 1058 1059 return lis.Addr().String(), func() { 1060 srv.Stop() 1061 lis.Close() 1062 } 1063 } 1064 1065 func setupGRPCServerWithSSL(t *testing.T) (string, func()) { 1066 lis, err := net.Listen("tcp4", ":0") 1067 if err != nil { 1068 t.Fatalf("Error listening on socket: %v", err) 1069 } 1070 1071 cert, err := tls.X509KeyPair(localhostCert, localhostKey) 1072 if err != nil { 1073 t.Fatalf("Error creating X509 key pair: %v", err) 1074 } 1075 1076 tlsCfg := tls.Config{ 1077 Certificates: []tls.Certificate{cert}, 1078 NextProtos: []string{"foo"}, 1079 } 1080 1081 srv := grpc.NewServer( 1082 grpc.Creds(credentials.NewTLS(&tlsCfg)), 1083 ) 1084 grpchealth.RegisterHealthServer(srv, &gRPCSrv{}) 1085 1086 go func() { 1087 _ = srv.Serve(lis) 1088 }() 1089 1090 return lis.Addr().String(), func() { 1091 srv.Stop() 1092 lis.Close() 1093 } 1094 } 1095 1096 // TestValidateLabels validates that no probe generates more metric or log 1097 // labels than the exported constants that specify these maximums. 1098 // This test is required to be aware if any modification extends these 1099 // maximums. These maximums are useful to calculate how many check labels can 1100 // be set without exceeding the Mimir and Loki limits. 1101 func TestValidateLabels(t *testing.T) { 1102 testCases := map[string]struct { 1103 setup func(ctx context.Context, t *testing.T) (prober.Prober, model.Check, func()) 1104 }{ 1105 "ping": { 1106 setup: setupPingProbe, 1107 }, 1108 "http": { 1109 setup: setupHTTPProbe, 1110 }, 1111 "http_ssl": { 1112 setup: setupHTTPSSLProbe, 1113 }, 1114 "dns": { 1115 setup: setupDNSProbe, 1116 }, 1117 "tcp": { 1118 setup: setupTCPProbe, 1119 }, 1120 "tcp_ssl": { 1121 setup: setupTCPSSLProbe, 1122 }, 1123 "traceroute": { 1124 setup: setupTracerouteProbe, 1125 }, 1126 "scripted": { 1127 setup: setupScriptedProbe, 1128 }, 1129 "multihttp": { 1130 setup: setupMultiHTTPProbe, 1131 }, 1132 "grpc": { 1133 setup: setupGRPCProbe, 1134 }, 1135 "grpc_ssl": { 1136 setup: setupGRPCSSLProbe, 1137 }, 1138 } 1139 1140 type maxMetricLabels struct { 1141 AllMetrics int 1142 CheckInfoMetric int 1143 } 1144 1145 isCheckInfoMetric := func(labels []prompb.Label) bool { 1146 for _, l := range labels { 1147 if l.Name == "__name__" && l.Value == CheckInfoMetricName { 1148 return true 1149 } 1150 } 1151 1152 return false 1153 } 1154 1155 // maxProbeMetricLabels returns the maximum number of labels for any 1156 // timeseries from probeData, and also the maximum number of labels for the 1157 // sm_check_info metric (that should be fairly static across all checks, 1158 // only depends on alert sensitivity, but review all generated tss anyway). 1159 maxProbeMetricLabels := func(t *testing.T, tss TimeSeries) maxMetricLabels { 1160 max := 0 1161 maxCheckInfoMetric := 0 1162 1163 for _, ts := range tss { 1164 labels := ts.GetLabels() 1165 1166 nLabels := len(labels) - 1 // Discount special __name__ label 1167 if nLabels > max { 1168 max = nLabels 1169 } 1170 // Check for CheckInfoMetric 1171 if isCheckInfoMetric(labels) { 1172 if nLabels > maxCheckInfoMetric { 1173 maxCheckInfoMetric = nLabels 1174 } 1175 } 1176 } 1177 1178 return maxMetricLabels{ 1179 AllMetrics: max, 1180 CheckInfoMetric: maxCheckInfoMetric, 1181 } 1182 } 1183 1184 // maxProbeLogLabels returns the maximum number of labels for any 1185 // log stream from probeData 1186 maxProbeLogLabels := func(t *testing.T, ss Streams) int { 1187 n := 0 1188 1189 for _, s := range ss { 1190 labels, err := metricParser.ParseMetric(s.Labels) 1191 require.NoError(t, err) 1192 1193 n = max(n, labels.Len()) 1194 } 1195 1196 return n 1197 } 1198 1199 timeout := 10 * time.Second 1200 1201 ctx, cancel := context.WithTimeout(context.Background(), timeout) 1202 defer cancel() 1203 1204 var ( 1205 totalMaxMetricLabels int 1206 totalMaxCheckInfoLabels int 1207 totalMaxLogLabels int 1208 ) 1209 1210 for name, tc := range testCases { 1211 t.Run(name, func(t *testing.T) { 1212 prober, check, stop := tc.setup(ctx, t) 1213 defer stop() 1214 1215 check.AlertSensitivity = "low" // Force sm_check_info metric to include alert sensitivity label 1216 1217 s := Scraper{ 1218 checkName: "check name", 1219 target: check.Target, 1220 logger: testhelper.Logger(t), 1221 prober: prober, 1222 labelsLimiter: testLabelsLimiter{ 1223 maxMetricLabels: 100, 1224 maxLogLabels: 100, 1225 }, 1226 labellingMode: testLabellingMode{}, 1227 summaries: make(map[uint64]prometheus.Summary), 1228 histograms: make(map[uint64]prometheus.Histogram), 1229 check: check, 1230 probe: sm.Probe{ 1231 Id: 100, 1232 TenantId: 200, 1233 Name: "probe name", 1234 Latitude: -1, 1235 Longitude: -2, 1236 Region: "region", 1237 }, 1238 } 1239 1240 data, duration, err := s.collectData(context.Background(), time.Unix(int64(3141000)/1000, 0)) 1241 require.NoError(t, err) 1242 require.NotNil(t, data) 1243 require.NotZero(t, duration) 1244 1245 metricLabels := maxProbeMetricLabels(t, data.Metrics()) 1246 logLabels := maxProbeLogLabels(t, data.Streams()) 1247 1248 if metricLabels.AllMetrics > totalMaxMetricLabels { 1249 totalMaxMetricLabels = metricLabels.AllMetrics 1250 } 1251 1252 if metricLabels.CheckInfoMetric > totalMaxCheckInfoLabels { 1253 totalMaxCheckInfoLabels = metricLabels.CheckInfoMetric 1254 } 1255 1256 if logLabels > totalMaxLogLabels { 1257 totalMaxLogLabels = logLabels 1258 } 1259 }) 1260 } 1261 1262 require.Equal(t, sm.MaxAgentMetricLabels(), totalMaxMetricLabels) 1263 require.Equal(t, sm.MaxAgentCheckInfoLabels(), totalMaxCheckInfoLabels) 1264 require.Equal(t, sm.MaxAgentLogLabels(), totalMaxLogLabels) 1265 } 1266 1267 func TestMakeTimeseries(t *testing.T) { 1268 testTime := time.Now() 1269 testValue := 42.0 1270 testcases := map[string]struct { 1271 t time.Time 1272 value float64 1273 labels []prompb.Label 1274 }{ 1275 "no labels": { 1276 t: testTime, 1277 value: testValue, 1278 labels: []prompb.Label{}, 1279 }, 1280 "one label": { 1281 t: testTime, 1282 value: testValue, 1283 labels: []prompb.Label{ 1284 {Name: "name", Value: "value"}, 1285 }, 1286 }, 1287 "many labels": { 1288 t: testTime, 1289 value: testValue, 1290 labels: []prompb.Label{ 1291 {Name: "name 1", Value: "value 1"}, 1292 {Name: "name 2", Value: "value 2"}, 1293 {Name: "name 3", Value: "value 3"}, 1294 {Name: "name 4", Value: "value 4"}, 1295 }, 1296 }, 1297 } 1298 1299 for name, tc := range testcases { 1300 t.Run(name, func(t *testing.T) { 1301 actual := makeTimeseries(tc.t, tc.value, tc.labels...) 1302 require.Equal(t, len(tc.labels), len(actual.Labels)) 1303 require.Equal(t, tc.labels, actual.Labels) 1304 require.Equal(t, 1, len(actual.Samples)) 1305 require.Equal(t, tc.t.UnixNano()/1e6, actual.Samples[0].Timestamp) 1306 require.Equal(t, tc.value, actual.Samples[0].Value) 1307 }) 1308 } 1309 } 1310 1311 // TestMakeTimeseriesDeduplicates verifies that makeTimeseries drops duplicate 1312 // label names, keeping the first occurrence (Mimir rejects duplicate keys) 1313 func TestMakeTimeseriesDeduplicates(t *testing.T) { 1314 ts := makeTimeseries(time.Unix(1, 0), 1, 1315 prompb.Label{Name: prom.MetricNameLabel, Value: "m"}, 1316 prompb.Label{Name: "probe", Value: "user-probe"}, 1317 prompb.Label{Name: "job", Value: "system"}, 1318 prompb.Label{Name: "probe", Value: "system"}, // duplicate name — must be dropped 1319 prompb.Label{Name: "env", Value: "prod"}, 1320 ) 1321 1322 require.Equal(t, []prompb.Label{ 1323 {Name: prom.MetricNameLabel, Value: "m"}, 1324 {Name: "probe", Value: "user-probe"}, // first occurrence wins 1325 {Name: "job", Value: "system"}, 1326 {Name: "env", Value: "prod"}, 1327 }, ts.Labels) 1328 } 1329 1330 // TestScraperCollectDataTooManyMetricLabels verifies that the check-info label 1331 // guard rejects a scrape whose non-system label count exceeds the tenant's 1332 // metric-label budget, before any data is emitted. 1333 func TestScraperCollectDataTooManyMetricLabels(t *testing.T) { 1334 s := Scraper{ 1335 checkName: "check name", 1336 target: "test target", 1337 logger: testhelper.Logger(t), 1338 prober: testProber{}, 1339 labelsLimiter: testLabelsLimiter{maxMetricLabels: 1, maxLogLabels: 15}, 1340 labellingMode: testLabellingMode{}, 1341 summaries: make(map[uint64]prometheus.Summary), 1342 histograms: make(map[uint64]prometheus.Histogram), 1343 check: model.Check{ 1344 Check: sm.Check{ 1345 Id: 1, 1346 TenantId: 2, 1347 Frequency: 2000, 1348 Timeout: 2000, 1349 Enabled: true, 1350 Target: "target name", 1351 Job: "job name", 1352 BasicMetricsOnly: true, 1353 Created: 42, 1354 Modified: 42, 1355 Labels: []sm.Label{ 1356 {Name: "a", Value: "1"}, 1357 {Name: "b", Value: "2"}, 1358 {Name: "c", Value: "3"}, 1359 }, 1360 Settings: sm.CheckSettings{Http: &sm.HttpSettings{}}, 1361 }, 1362 }, 1363 probe: sm.Probe{Id: 100, TenantId: 200, Name: "probe name", Region: "REGION"}, 1364 } 1365 1366 _, _, err := s.collectData(context.Background(), time.Unix(3141, 0)) 1367 require.ErrorIs(t, err, errTooManyLabels) 1368 } 1369 1370 // TestScraperCollectDataFatalError verifies that the exported CollectData 1371 // surfaces a fatal collection error (one that is not errCheckFailed) with no 1372 // telemetry, matching its documented contract. 1373 func TestScraperCollectDataFatalError(t *testing.T) { 1374 s := Scraper{ 1375 checkName: "check name", 1376 target: "test target", 1377 logger: testhelper.Logger(t), 1378 prober: testProber{}, 1379 labelsLimiter: testLabelsLimiter{maxMetricLabels: 1, maxLogLabels: 15}, 1380 labellingMode: testLabellingMode{}, 1381 summaries: make(map[uint64]prometheus.Summary), 1382 histograms: make(map[uint64]prometheus.Histogram), 1383 check: model.Check{ 1384 Check: sm.Check{ 1385 Id: 1, 1386 TenantId: 2, 1387 Frequency: 2000, 1388 Timeout: 2000, 1389 Enabled: true, 1390 Target: "target name", 1391 Job: "job name", 1392 BasicMetricsOnly: true, 1393 Created: 42, 1394 Modified: 42, 1395 Labels: []sm.Label{ 1396 {Name: "a", Value: "1"}, 1397 {Name: "b", Value: "2"}, 1398 {Name: "c", Value: "3"}, 1399 }, 1400 Settings: sm.CheckSettings{Http: &sm.HttpSettings{}}, 1401 }, 1402 }, 1403 probe: sm.Probe{Id: 100, TenantId: 200, Name: "probe name", Region: "REGION"}, 1404 } 1405 1406 ts, streams, tenant, d, err := s.CollectData(context.Background(), time.Unix(3141, 0)) 1407 require.ErrorIs(t, err, errTooManyLabels) 1408 require.NotErrorIs(t, err, errCheckFailed) 1409 require.Nil(t, ts) 1410 require.Nil(t, streams) 1411 require.Zero(t, tenant) 1412 require.Zero(t, d) 1413 } 1414 1415 func TestAppendDtoToTimeseries(t *testing.T) { 1416 makeUint64Ptr := func(n uint64) *uint64 { 1417 return &n 1418 } 1419 1420 makeFloat64Ptr := func(n float64) *float64 { 1421 return &n 1422 } 1423 1424 testTime := time.Now() 1425 testTimestamp := testTime.UnixNano() / 1e6 1426 testValue := 42.0 1427 testcases := map[string]struct { 1428 t time.Time 1429 mName string 1430 sharedLabels []prompb.Label 1431 mType dto.MetricType 1432 metric *dto.Metric 1433 expected []prompb.TimeSeries 1434 }{ 1435 "counter": { 1436 t: testTime, 1437 mName: "test", 1438 sharedLabels: []prompb.Label{ 1439 {Name: "label 1", Value: "value 1"}, 1440 }, 1441 mType: dto.MetricType_COUNTER, 1442 metric: &dto.Metric{ 1443 Counter: &dto.Counter{ 1444 Value: &testValue, 1445 }, 1446 }, 1447 expected: []prompb.TimeSeries{ 1448 { 1449 Labels: []prompb.Label{ 1450 {Name: "__name__", Value: "test"}, 1451 {Name: "label 1", Value: "value 1"}, 1452 }, 1453 Samples: []prompb.Sample{ 1454 {Timestamp: testTimestamp, Value: testValue}, 1455 }, 1456 }, 1457 }, 1458 }, 1459 "gauge": { 1460 t: testTime, 1461 mName: "test", 1462 sharedLabels: []prompb.Label{ 1463 {Name: "label 1", Value: "value 1"}, 1464 }, 1465 mType: dto.MetricType_GAUGE, 1466 metric: &dto.Metric{ 1467 Gauge: &dto.Gauge{ 1468 Value: &testValue, 1469 }, 1470 }, 1471 expected: []prompb.TimeSeries{ 1472 { 1473 Labels: []prompb.Label{ 1474 {Name: "__name__", Value: "test"}, 1475 {Name: "label 1", Value: "value 1"}, 1476 }, 1477 Samples: []prompb.Sample{ 1478 {Timestamp: testTimestamp, Value: testValue}, 1479 }, 1480 }, 1481 }, 1482 }, 1483 "untyped": { 1484 t: testTime, 1485 mName: "test", 1486 sharedLabels: []prompb.Label{ 1487 {Name: "label 1", Value: "value 1"}, 1488 }, 1489 mType: dto.MetricType_UNTYPED, 1490 metric: &dto.Metric{ 1491 Untyped: &dto.Untyped{ 1492 Value: &testValue, 1493 }, 1494 }, 1495 expected: []prompb.TimeSeries{ 1496 { 1497 Labels: []prompb.Label{ 1498 {Name: "__name__", Value: "test"}, 1499 {Name: "label 1", Value: "value 1"}, 1500 }, 1501 Samples: []prompb.Sample{ 1502 {Timestamp: testTimestamp, Value: testValue}, 1503 }, 1504 }, 1505 }, 1506 }, 1507 "summary": { 1508 t: testTime, 1509 mName: "test", 1510 sharedLabels: []prompb.Label{ 1511 {Name: "label 1", Value: "value 1"}, 1512 }, 1513 mType: dto.MetricType_SUMMARY, 1514 metric: &dto.Metric{ 1515 Summary: &dto.Summary{ 1516 SampleCount: makeUint64Ptr(7), 1517 SampleSum: makeFloat64Ptr(0.25), 1518 }, 1519 }, 1520 expected: []prompb.TimeSeries{ 1521 { 1522 Labels: []prompb.Label{ 1523 {Name: "__name__", Value: "test_sum"}, 1524 {Name: "label 1", Value: "value 1"}, 1525 }, 1526 Samples: []prompb.Sample{ 1527 {Timestamp: testTimestamp, Value: 0.25}, 1528 }, 1529 }, 1530 { 1531 Labels: []prompb.Label{ 1532 {Name: "__name__", Value: "test_count"}, 1533 {Name: "label 1", Value: "value 1"}, 1534 }, 1535 Samples: []prompb.Sample{ 1536 {Timestamp: testTimestamp, Value: 7}, 1537 }, 1538 }, 1539 }, 1540 }, 1541 "histogram": { 1542 t: testTime, 1543 mName: "test", 1544 sharedLabels: []prompb.Label{ 1545 {Name: "label 1", Value: "value 1"}, 1546 }, 1547 mType: dto.MetricType_HISTOGRAM, 1548 metric: &dto.Metric{ 1549 Histogram: &dto.Histogram{ 1550 SampleCount: makeUint64Ptr(17), 1551 SampleSum: makeFloat64Ptr(120), 1552 Bucket: []*dto.Bucket{ 1553 { 1554 CumulativeCount: makeUint64Ptr(1), 1555 UpperBound: makeFloat64Ptr(0.1), 1556 }, 1557 { 1558 CumulativeCount: makeUint64Ptr(5), 1559 UpperBound: makeFloat64Ptr(1.0), 1560 }, 1561 { 1562 CumulativeCount: makeUint64Ptr(11), 1563 UpperBound: makeFloat64Ptr(10.0), 1564 }, 1565 }, 1566 }, 1567 }, 1568 expected: []prompb.TimeSeries{ 1569 { 1570 Labels: []prompb.Label{ 1571 {Name: "__name__", Value: "test_sum"}, 1572 {Name: "label 1", Value: "value 1"}, 1573 }, 1574 Samples: []prompb.Sample{ 1575 {Timestamp: testTimestamp, Value: 120}, 1576 }, 1577 }, 1578 { 1579 Labels: []prompb.Label{ 1580 {Name: "__name__", Value: "test_count"}, 1581 {Name: "label 1", Value: "value 1"}, 1582 }, 1583 Samples: []prompb.Sample{ 1584 {Timestamp: testTimestamp, Value: 17}, 1585 }, 1586 }, 1587 { 1588 Labels: []prompb.Label{ 1589 {Name: "__name__", Value: "test_bucket"}, 1590 {Name: "label 1", Value: "value 1"}, 1591 {Name: "le", Value: "0.1"}, 1592 }, 1593 Samples: []prompb.Sample{ 1594 {Timestamp: testTimestamp, Value: 1}, 1595 }, 1596 }, 1597 { 1598 Labels: []prompb.Label{ 1599 {Name: "__name__", Value: "test_bucket"}, 1600 {Name: "label 1", Value: "value 1"}, 1601 {Name: "le", Value: "1"}, 1602 }, 1603 Samples: []prompb.Sample{ 1604 {Timestamp: testTimestamp, Value: 5}, 1605 }, 1606 }, 1607 { 1608 Labels: []prompb.Label{ 1609 {Name: "__name__", Value: "test_bucket"}, 1610 {Name: "label 1", Value: "value 1"}, 1611 {Name: "le", Value: "10"}, 1612 }, 1613 Samples: []prompb.Sample{ 1614 {Timestamp: testTimestamp, Value: 11}, 1615 }, 1616 }, 1617 { 1618 Labels: []prompb.Label{ 1619 {Name: "__name__", Value: "test_bucket"}, 1620 {Name: "label 1", Value: "value 1"}, 1621 {Name: "le", Value: "+Inf"}, 1622 }, 1623 Samples: []prompb.Sample{ 1624 {Timestamp: testTimestamp, Value: 17}, 1625 }, 1626 }, 1627 }, 1628 }, 1629 } 1630 1631 for name, tc := range testcases { 1632 t.Run(name, func(t *testing.T) { 1633 actual := appendDtoToTimeseries( 1634 nil, 1635 tc.t, 1636 tc.mName, 1637 tc.sharedLabels, 1638 tc.mType, 1639 tc.metric, 1640 ) 1641 1642 require.Equal(t, tc.expected, actual) 1643 }) 1644 } 1645 } 1646 1647 type testProber struct{} 1648 1649 func (p testProber) Name() string { 1650 return "test prober" 1651 } 1652 1653 func (p testProber) Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger, _ string) (bool, float64) { 1654 counter := prometheus.NewCounter(prometheus.CounterOpts{ 1655 Name: "test_counter", 1656 }) 1657 registry.MustRegister(counter) 1658 counter.Inc() 1659 1660 return true, 1 1661 } 1662 1663 type testLabelsLimiter struct { 1664 maxMetricLabels int 1665 maxLogLabels int 1666 } 1667 1668 func (l testLabelsLimiter) MetricLabels(ctx context.Context, tenantID model.GlobalID) (int, error) { 1669 return l.maxMetricLabels, nil 1670 } 1671 1672 func (l testLabelsLimiter) LogLabels(ctx context.Context, tenantID model.GlobalID) (int, error) { 1673 return l.maxLogLabels, nil 1674 } 1675 1676 type testLabellingMode struct { 1677 mode sm.LabelMode 1678 err error 1679 } 1680 1681 func (l testLabellingMode) ForTenant(ctx context.Context, tenantID model.GlobalID) (sm.LabelMode, error) { 1682 return l.mode, l.err 1683 } 1684 1685 type testCalTenants struct { 1686 costAttributionLabels []string 1687 } 1688 1689 func (t testCalTenants) CostAttributionLabels(_ context.Context, tenantID model.GlobalID) ([]string, error) { 1690 return t.costAttributionLabels, nil 1691 } 1692 1693 //nolint:gocyclo 1694 func TestScraperCollectData(t *testing.T) { 1695 const ( 1696 checkName = "check name" 1697 checkTarget = "target name" 1698 frequency = 2000 1699 job = "job name" 1700 modifiedTs = 42 1701 probeLatitude = -1 1702 probeLongitde = -2 1703 probeName = "probe name" 1704 region = "REGION" 1705 sampleTsMs = int64(3141000) 1706 ) 1707 1708 var ( 1709 baseExpectedMetricLabels = map[string]string{ 1710 configVersionLabelName: strconv.Itoa(modifiedTs * 1_000_000_000), 1711 "instance": checkTarget, 1712 "job": job, 1713 "probe": probeName, 1714 // "source": CheckInfoSource, 1715 } 1716 baseExpectedInfoLabels = map[string]string{ 1717 "check_name": checkName, 1718 "frequency": strconv.Itoa(frequency), 1719 "geohash": geohash.Encode(probeLatitude, probeLongitde), 1720 "region": region, 1721 } 1722 baseExpectedLogLabels = map[string]string{ 1723 "check_name": checkName, 1724 "instance": checkTarget, 1725 "job": job, 1726 "probe": probeName, 1727 ProbeSuccessMetricName: "1", 1728 "region": region, 1729 "source": CheckInfoSource, 1730 } 1731 1732 // Tenants that have started the label migration (DUAL_WRITE/UNPREFIXED) 1733 // receive the agent's region label as sm_region instead of region. 1734 baseExpectedInfoLabelsSMRegion = map[string]string{ 1735 "check_name": checkName, 1736 "frequency": strconv.Itoa(frequency), 1737 "geohash": geohash.Encode(probeLatitude, probeLongitde), 1738 "sm_region": region, 1739 } 1740 baseExpectedLogLabelsSMRegion = map[string]string{ 1741 "check_name": checkName, 1742 "instance": checkTarget, 1743 "job": job, 1744 "probe": probeName, 1745 ProbeSuccessMetricName: "1", 1746 "sm_region": region, 1747 "source": CheckInfoSource, 1748 } 1749 ) 1750 1751 generateLabels := func(offset, count int, valuePrefix string) []sm.Label { 1752 var labels []sm.Label 1753 1754 for i := range count { 1755 n := strconv.Itoa(offset + i) 1756 labels = append(labels, sm.Label{ 1757 Name: "l" + n, 1758 Value: valuePrefix + n, 1759 }) 1760 } 1761 1762 return labels 1763 } 1764 1765 generateLabelSet := func(offset, count int, valuePrefix string) map[string]string { 1766 labels := make(map[string]string) 1767 1768 for i := range count { 1769 n := strconv.Itoa(offset + i) 1770 labels["label_l"+n] = valuePrefix + n 1771 } 1772 1773 return labels 1774 } 1775 1776 // generateUnprefixedLabelSet produces {"l0":"c0","l1":"c1",...} — used to describe 1777 // expected user labels on execution metrics in DUAL_WRITE and UNPREFIXED modes. 1778 generateUnprefixedLabelSet := func(offset, count int, valuePrefix string) map[string]string { 1779 labels := make(map[string]string) 1780 1781 for i := range count { 1782 n := strconv.Itoa(offset + i) 1783 labels["l"+n] = valuePrefix + n 1784 } 1785 1786 return labels 1787 } 1788 1789 type testcase struct { 1790 labelMode sm.LabelMode // zero value = PREFIXED 1791 labelModeErr error // simulates a tenant label-mode lookup failure 1792 maxMetricLabels int 1793 maxLogLabels int 1794 checkLabels []sm.Label 1795 probeLabels []sm.Label 1796 expectedMetricLabels map[string]string 1797 expectedInfoLabels map[string]string 1798 expectedLogLabels map[string]string 1799 expectedLogEntries map[string]string 1800 } 1801 1802 const ( 1803 defMaxMetricLabels = 22 1804 defMaxLogLabels = 15 1805 ) 1806 1807 testcases := map[string]testcase{ 1808 "trivial": { 1809 maxMetricLabels: defMaxMetricLabels, 1810 maxLogLabels: defMaxLogLabels, 1811 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1812 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels), 1813 expectedLogLabels: mergeMaps(baseExpectedLogLabels), 1814 expectedLogEntries: mergeMaps(baseExpectedLogLabels), 1815 }, 1816 "probe labels": { 1817 maxMetricLabels: defMaxMetricLabels, 1818 maxLogLabels: defMaxLogLabels, 1819 probeLabels: generateLabels(1, 3, "p"), 1820 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1821 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(1, 3, "p")), 1822 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(1, 3, "p")), 1823 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(1, 3, "p")), 1824 }, 1825 "check labels": { 1826 maxMetricLabels: defMaxMetricLabels, 1827 maxLogLabels: defMaxLogLabels, 1828 checkLabels: generateLabels(1, 3, "c"), 1829 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1830 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(1, 3, "c")), 1831 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(1, 3, "c")), 1832 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(1, 3, "c")), 1833 }, 1834 "check and probe labels": { 1835 maxMetricLabels: defMaxMetricLabels, 1836 maxLogLabels: defMaxLogLabels, 1837 checkLabels: generateLabels(1, 2, "c"), 1838 probeLabels: generateLabels(3, 1, "p"), 1839 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1840 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1841 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1842 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1843 }, 1844 "check and probe labels overlapping": { 1845 maxMetricLabels: defMaxMetricLabels, 1846 maxLogLabels: defMaxLogLabels, 1847 checkLabels: generateLabels(1, 2, "c"), 1848 probeLabels: generateLabels(2, 2, "p"), 1849 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1850 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1851 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1852 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(3, 1, "p"), generateLabelSet(1, 2, "c")), 1853 }, 1854 "max labels truncate": { 1855 maxMetricLabels: defMaxMetricLabels, 1856 maxLogLabels: defMaxLogLabels, 1857 checkLabels: generateLabels(0, 10, "c"), 1858 probeLabels: generateLabels(10, 3, "p"), 1859 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1860 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(0, 10, "c"), generateLabelSet(10, 3, "p")), 1861 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(0, 5, "c"), generateLabelSet(10, 3, "p")), 1862 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(0, 10, "c"), generateLabelSet(10, 3, "p")), 1863 }, 1864 "max labels override": { 1865 maxMetricLabels: 23, 1866 maxLogLabels: 23, 1867 checkLabels: generateLabels(0, 10, "c"), 1868 probeLabels: generateLabels(10, 4, "p"), 1869 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels), 1870 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabels, generateLabelSet(0, 10, "c"), generateLabelSet(10, 4, "p")), 1871 expectedLogLabels: mergeMaps(baseExpectedLogLabels, generateLabelSet(0, 10, "c"), generateLabelSet(10, 4, "p")), 1872 expectedLogEntries: mergeMaps(baseExpectedLogLabels, generateLabelSet(0, 10, "c"), generateLabelSet(10, 4, "p")), 1873 }, 1874 1875 // ── DUAL_WRITE and UNPREFIXED mode tests ────────────────────────────────── 1876 // 1877 // Key differences: 1878 // expectedMetricLabels includes un-prefixed user labels (execution metrics 1879 // gain user labels via customMetricLabels in DUAL_WRITE/UNPREFIXED). 1880 // expectedInfoLabels includes both forms in DUAL_WRITE (prefixed first in the 1881 // slice produced by buildUserLabels, then un-prefixed) and un-prefixed only 1882 // in UNPREFIXED. 1883 // expectedLogLabels follows DUAL_WRITE ordering: all prefixed forms before all 1884 // un-prefixed forms in the stream labels; overflow goes to StructuredMetadata. 1885 1886 "dual write basic": { 1887 // 3 check labels + 1 probe label; no stream-label overflow. 1888 // Verifies: execution metrics carry un-prefixed user labels; 1889 // sm_check_info carries both forms; 1890 // log stream carries both forms in correct ordering. 1891 labelMode: sm.LabelMode_LABEL_MODE_DUAL_WRITE, 1892 maxMetricLabels: defMaxMetricLabels, 1893 maxLogLabels: defMaxLogLabels, 1894 checkLabels: generateLabels(0, 3, "c"), 1895 probeLabels: generateLabels(10, 1, "p"), 1896 // Execution metrics: base + un-prefixed user labels. 1897 expectedMetricLabels: mergeMaps( 1898 baseExpectedMetricLabels, 1899 generateUnprefixedLabelSet(10, 1, "p"), 1900 generateUnprefixedLabelSet(0, 3, "c"), 1901 ), 1902 // sm_check_info: system sharedLabels (probe, config_version, instance, job) 1903 // + check-info system labels + both forms of user labels from ConstLabels. 1904 expectedInfoLabels: mergeMaps( 1905 baseExpectedMetricLabels, 1906 baseExpectedInfoLabelsSMRegion, 1907 generateLabelSet(10, 1, "p"), 1908 generateUnprefixedLabelSet(10, 1, "p"), 1909 generateLabelSet(0, 3, "c"), 1910 generateUnprefixedLabelSet(0, 3, "c"), 1911 ), 1912 // Log stream: DUAL_WRITE ordering — all prefixed first, then un-prefixed. 1913 // 4 user labels × 2 = 8 entries + 6 system labels = 14; probe_success 1914 // is appended last, totalling 15 = defMaxLogLabels. No overflow. 1915 expectedLogLabels: mergeMaps( 1916 baseExpectedLogLabelsSMRegion, 1917 generateLabelSet(10, 1, "p"), 1918 generateLabelSet(0, 3, "c"), 1919 generateUnprefixedLabelSet(10, 1, "p"), 1920 generateUnprefixedLabelSet(0, 3, "c"), 1921 ), 1922 expectedLogEntries: mergeMaps( 1923 baseExpectedLogLabelsSMRegion, 1924 generateLabelSet(10, 1, "p"), 1925 generateLabelSet(0, 3, "c"), 1926 generateUnprefixedLabelSet(10, 1, "p"), 1927 generateUnprefixedLabelSet(0, 3, "c"), 1928 ), 1929 }, 1930 1931 "unprefixed basic": { 1932 // 3 check labels + 1 probe label in UNPREFIXED mode. 1933 // Verifies: execution metrics carry un-prefixed user labels; 1934 // sm_check_info and log stream carry only un-prefixed forms. 1935 labelMode: sm.LabelMode_LABEL_MODE_UNPREFIXED, 1936 maxMetricLabels: defMaxMetricLabels, 1937 maxLogLabels: defMaxLogLabels, 1938 checkLabels: generateLabels(0, 3, "c"), 1939 probeLabels: generateLabels(10, 1, "p"), 1940 expectedMetricLabels: mergeMaps( 1941 baseExpectedMetricLabels, 1942 generateUnprefixedLabelSet(10, 1, "p"), 1943 generateUnprefixedLabelSet(0, 3, "c"), 1944 ), 1945 expectedInfoLabels: mergeMaps( 1946 baseExpectedMetricLabels, 1947 baseExpectedInfoLabelsSMRegion, 1948 generateUnprefixedLabelSet(10, 1, "p"), 1949 generateUnprefixedLabelSet(0, 3, "c"), 1950 ), 1951 expectedLogLabels: mergeMaps( 1952 baseExpectedLogLabelsSMRegion, 1953 generateUnprefixedLabelSet(10, 1, "p"), 1954 generateUnprefixedLabelSet(0, 3, "c"), 1955 ), 1956 expectedLogEntries: mergeMaps( 1957 baseExpectedLogLabelsSMRegion, 1958 generateUnprefixedLabelSet(10, 1, "p"), 1959 generateUnprefixedLabelSet(0, 3, "c"), 1960 ), 1961 }, 1962 1963 "dual write log overflow": { 1964 // 10 check labels + 3 probe labels in DUAL_WRITE mode causes log stream 1965 // overflow. DUAL_WRITE ordering means all prefixed forms come first in the 1966 // stream labels, so they are preserved when the label count exceeds the cap; 1967 // the un-prefixed forms spill into StructuredMetadata instead. 1968 // 1969 // Label budget: 6 system + 26 user (13×2) = 32 total; cap at 1970 // defMaxLogLabels-1=14 stream slots before probe_success. 1971 // Stream labels: system labels (6) + first 8 prefixed user labels. 1972 // Overflow into StructuredMetadata: remaining prefixed (5) + all 13 1973 // un-prefixed user labels. 1974 labelMode: sm.LabelMode_LABEL_MODE_DUAL_WRITE, 1975 maxMetricLabels: defMaxMetricLabels, 1976 maxLogLabels: defMaxLogLabels, 1977 checkLabels: generateLabels(0, 10, "c"), 1978 probeLabels: generateLabels(10, 3, "p"), 1979 // Execution metrics: base + all un-prefixed user labels. 1980 expectedMetricLabels: mergeMaps( 1981 baseExpectedMetricLabels, 1982 generateUnprefixedLabelSet(10, 3, "p"), 1983 generateUnprefixedLabelSet(0, 10, "c"), 1984 ), 1985 // sm_check_info: system sharedLabels + both forms of user labels from ConstLabels. 1986 expectedInfoLabels: mergeMaps( 1987 baseExpectedMetricLabels, 1988 baseExpectedInfoLabelsSMRegion, 1989 generateLabelSet(10, 3, "p"), 1990 generateUnprefixedLabelSet(10, 3, "p"), 1991 generateLabelSet(0, 10, "c"), 1992 generateUnprefixedLabelSet(0, 10, "c"), 1993 ), 1994 // Stream labels: system(6) + 8 prefixed user labels (prefixed-first 1995 // ordering means label_l10,label_l11,label_l12 + label_l0..label_l4 1996 // fill the 8 available slots before the cap). 1997 expectedLogLabels: mergeMaps( 1998 baseExpectedLogLabelsSMRegion, 1999 generateLabelSet(10, 3, "p"), // label_l10, label_l11, label_l12 2000 generateLabelSet(0, 5, "c"), // label_l0 .. label_l4 2001 ), 2002 // Log entry body includes all user labels in both forms (log entry is not 2003 // truncated, only stream labels are capped). The entries in 2004 // expectedLogEntries \ expectedLogLabels become StructuredMetadata overflow 2005 // and are validated by assertStructuredMetadata. 2006 expectedLogEntries: mergeMaps( 2007 baseExpectedLogLabelsSMRegion, 2008 generateLabelSet(10, 3, "p"), 2009 generateLabelSet(0, 10, "c"), 2010 generateUnprefixedLabelSet(10, 3, "p"), 2011 generateUnprefixedLabelSet(0, 10, "c"), 2012 ), 2013 }, 2014 2015 // In UNPREFIXED mode a user label named "job" collides with the reserved 2016 // system "job" label. buildUserLabels drops it (sm.IsSystemLabel), so the 2017 // system value is retained on every surface — execution metrics, 2018 // sm_check_info, and the log stream — rather than being overwritten. The 2019 // non-reserved "env" label is emitted normally. 2020 "unprefixed reserved label dropped": { 2021 labelMode: sm.LabelMode_LABEL_MODE_UNPREFIXED, 2022 maxMetricLabels: defMaxMetricLabels, 2023 maxLogLabels: defMaxLogLabels, 2024 checkLabels: []sm.Label{ 2025 {Name: "job", Value: "user-job"}, 2026 {Name: "env", Value: "prod"}, 2027 }, 2028 // "job" keeps its system value (from base); only "env" is added. 2029 expectedMetricLabels: mergeMaps( 2030 baseExpectedMetricLabels, 2031 map[string]string{"env": "prod"}, 2032 ), 2033 expectedInfoLabels: mergeMaps( 2034 baseExpectedMetricLabels, 2035 baseExpectedInfoLabelsSMRegion, 2036 map[string]string{"env": "prod"}, 2037 ), 2038 expectedLogLabels: mergeMaps( 2039 baseExpectedLogLabelsSMRegion, 2040 map[string]string{"env": "prod"}, 2041 ), 2042 expectedLogEntries: mergeMaps( 2043 baseExpectedLogLabelsSMRegion, 2044 map[string]string{"env": "prod"}, 2045 ), 2046 }, 2047 2048 // Renaming the agent's own region label to sm_region frees "region" for 2049 // tenant use: a user label named region must reach every surface intact, 2050 // alongside the agent's sm_region. 2051 "unprefixed user region label freed": { 2052 labelMode: sm.LabelMode_LABEL_MODE_UNPREFIXED, 2053 maxMetricLabels: defMaxMetricLabels, 2054 maxLogLabels: defMaxLogLabels, 2055 checkLabels: []sm.Label{ 2056 {Name: "region", Value: "user-region"}, 2057 }, 2058 expectedMetricLabels: mergeMaps( 2059 baseExpectedMetricLabels, 2060 map[string]string{"region": "user-region"}, 2061 ), 2062 expectedInfoLabels: mergeMaps( 2063 baseExpectedMetricLabels, 2064 baseExpectedInfoLabelsSMRegion, 2065 map[string]string{"region": "user-region"}, 2066 ), 2067 expectedLogLabels: mergeMaps( 2068 baseExpectedLogLabelsSMRegion, 2069 map[string]string{"region": "user-region"}, 2070 ), 2071 expectedLogEntries: mergeMaps( 2072 baseExpectedLogLabelsSMRegion, 2073 map[string]string{"region": "user-region"}, 2074 ), 2075 }, 2076 2077 // Same as above but in DUAL_WRITE: the riskiest mode, where the user's 2078 // region (as both region and label_region) coexists with the agent's 2079 // sm_region — three distinct keys, no duplicates, on every surface. 2080 "dual write user region label freed": { 2081 labelMode: sm.LabelMode_LABEL_MODE_DUAL_WRITE, 2082 maxMetricLabels: defMaxMetricLabels, 2083 maxLogLabels: defMaxLogLabels, 2084 checkLabels: []sm.Label{ 2085 {Name: "region", Value: "user-region"}, 2086 }, 2087 // Execution metrics carry only the un-prefixed user form. 2088 expectedMetricLabels: mergeMaps( 2089 baseExpectedMetricLabels, 2090 map[string]string{"region": "user-region"}, 2091 ), 2092 expectedInfoLabels: mergeMaps( 2093 baseExpectedMetricLabels, 2094 baseExpectedInfoLabelsSMRegion, 2095 map[string]string{"label_region": "user-region", "region": "user-region"}, 2096 ), 2097 expectedLogLabels: mergeMaps( 2098 baseExpectedLogLabelsSMRegion, 2099 map[string]string{"label_region": "user-region", "region": "user-region"}, 2100 ), 2101 expectedLogEntries: mergeMaps( 2102 baseExpectedLogLabelsSMRegion, 2103 map[string]string{"label_region": "user-region", "region": "user-region"}, 2104 ), 2105 }, 2106 2107 // This tests simulates the default label mode falling back to UNPREFIXED, but note 2108 // the scraper would actually abort if there was no tenant in cache due to 2109 // the label limits being unavailable. 2110 "label mode fetch error degrades to unprefixed": { 2111 labelMode: sm.LabelMode_LABEL_MODE_UNPREFIXED, 2112 labelModeErr: errors.New("simulated tenant label-mode fetch failure"), 2113 maxMetricLabels: defMaxMetricLabels, 2114 maxLogLabels: defMaxLogLabels, 2115 checkLabels: generateLabels(1, 3, "c"), 2116 expectedMetricLabels: mergeMaps(baseExpectedMetricLabels, generateUnprefixedLabelSet(1, 3, "c")), 2117 expectedInfoLabels: mergeMaps(baseExpectedMetricLabels, baseExpectedInfoLabelsSMRegion, generateUnprefixedLabelSet(1, 3, "c")), 2118 expectedLogLabels: mergeMaps(baseExpectedLogLabelsSMRegion, generateUnprefixedLabelSet(1, 3, "c")), 2119 expectedLogEntries: mergeMaps(baseExpectedLogLabelsSMRegion, generateUnprefixedLabelSet(1, 3, "c")), 2120 }, 2121 } 2122 2123 getMetricName := func(t *testing.T, ts prompb.TimeSeries) string { 2124 for _, l := range ts.GetLabels() { 2125 if l.GetName() != prom.MetricNameLabel { 2126 continue 2127 } 2128 2129 return l.GetValue() 2130 } 2131 2132 require.Fail(t, "metric name not found") 2133 2134 return "" 2135 } 2136 2137 validateMetrics := func(t *testing.T, ts prompb.TimeSeries, tc testcase) { 2138 require.NotNil(t, ts) 2139 2140 metricName := getMetricName(t, ts) 2141 2142 // Ensure no label name appears more than once. Duplicate label keys are 2143 // rejected by Mimir and indicate a bug in label assembly (e.g. user labels 2144 // reaching a metric via both ConstLabels and sharedLabels). 2145 seenLabelNames := make(map[string]bool, len(ts.GetLabels())) 2146 for _, l := range ts.GetLabels() { 2147 require.Falsef(t, seenLabelNames[l.GetName()], 2148 "duplicate label %q in metric %s", l.GetName(), metricName) 2149 seenLabelNames[l.GetName()] = true 2150 } 2151 2152 actualLabels := make(map[string]string) 2153 actualLabelsCount := 0 2154 actualInfoLabels := make(map[string]string) 2155 actualInfoLabelsCount := 0 2156 2157 // Verify that all the expected metric labels are present 2158 2159 for _, l := range ts.GetLabels() { 2160 switch { 2161 case l.GetName() == prom.MetricNameLabel: 2162 // ignore 2163 2164 case l.GetName() == labels.BucketLabel: 2165 // ignore 2166 2167 case metricName == CheckInfoMetricName: 2168 expectedValue, isExpected := tc.expectedInfoLabels[l.GetName()] 2169 require.Truef(t, isExpected, "metric=%s label=%s value=%s", metricName, l.GetName(), l.GetValue()) 2170 require.Equal(t, expectedValue, l.GetValue()) 2171 actualInfoLabels[l.GetName()] = l.GetValue() 2172 actualInfoLabelsCount++ 2173 2174 default: 2175 expectedValue, isExpected := tc.expectedMetricLabels[l.GetName()] 2176 require.Truef(t, isExpected, "unexpected label: metric=%s label=%s value=%s", metricName, l.GetName(), l.GetValue()) 2177 require.Equal(t, expectedValue, l.GetValue()) 2178 actualLabels[l.GetName()] = l.GetValue() 2179 actualLabelsCount++ 2180 } 2181 } 2182 2183 if metricName == CheckInfoMetricName { 2184 require.Equal(t, tc.expectedInfoLabels, actualInfoLabels) 2185 require.Equal(t, len(tc.expectedInfoLabels), actualInfoLabelsCount) 2186 } else { 2187 require.Equal(t, tc.expectedMetricLabels, actualLabels) 2188 require.Equal(t, len(tc.expectedMetricLabels), actualLabelsCount) 2189 } 2190 2191 for _, sample := range ts.GetSamples() { 2192 // This encodes the assumption that there's a single timestamp included in the 2193 // resulting metrics. 2194 require.Equal(t, sampleTsMs, sample.Timestamp) 2195 } 2196 } 2197 2198 // assertStructuredMetadata verifies that each log entry's StructuredMetadata 2199 // contains a valid execution_id UUID plus any overflow labels (those present in 2200 // expectedLogEntries but absent from expectedLogLabels — they fit in the log 2201 // entry body but were truncated from the stream label set). 2202 assertStructuredMetadata := func(t *testing.T, entry logproto.Entry, tc testcase) { 2203 t.Helper() 2204 2205 executionIDFound := false 2206 overflowFound := 0 2207 2208 for _, meta := range entry.StructuredMetadata { 2209 if meta.Name == "execution_id" { 2210 executionIDFound = true 2211 _, err := uuid.Parse(meta.Value) 2212 require.NoError(t, err, "execution_id should be a valid UUID") 2213 } else { 2214 expectedVal, inEntries := tc.expectedLogEntries[meta.Name] 2215 _, inLabels := tc.expectedLogLabels[meta.Name] 2216 require.Truef(t, inEntries && !inLabels, 2217 "unexpected structured metadata entry: %s=%s", meta.Name, meta.Value) 2218 require.Equal(t, expectedVal, meta.Value, "overflow label value mismatch: %s", meta.Name) 2219 2220 overflowFound++ 2221 } 2222 } 2223 2224 require.True(t, executionIDFound, "execution_id not found in structured metadata") 2225 2226 expectedOverflow := 0 2227 2228 for name := range tc.expectedLogEntries { 2229 if _, inLabels := tc.expectedLogLabels[name]; !inLabels { 2230 expectedOverflow++ 2231 } 2232 } 2233 2234 require.Equal(t, expectedOverflow, overflowFound, "overflow label count in structured metadata") 2235 } 2236 2237 validateStreams := func(t *testing.T, s Scraper, stream logproto.Stream, tc testcase) { 2238 sLabels, err := metricParser.ParseMetric(stream.Labels) 2239 require.NoError(t, err) 2240 2241 // Verify that all the expected log labels are present as labels in the stream labels. 2242 found := 0 2243 2244 sLabels.Range(func(label labels.Label) { 2245 expected, ok := tc.expectedLogLabels[label.Name] 2246 require.Truef(t, ok, "key=%s value=%s labels=%s", label.Name, label.Value, stream.Labels) 2247 require.Equalf(t, expected, label.Value, "key=%s", label.Name) 2248 2249 found++ 2250 }) 2251 2252 require.Equal(t, len(tc.expectedLogLabels), found, stream.Labels) 2253 2254 // Verify that all the expected log labels are present as part of the actual log entry. 2255 for _, entry := range stream.Entries { 2256 dec := logfmt.NewDecoder(strings.NewReader(entry.Line)) 2257 for dec.ScanRecord() { 2258 labelsFound := 1 // probe_success is NOT included in the log entry 2259 2260 for dec.ScanKeyval() { 2261 key := string(dec.Key()) 2262 val := string(dec.Value()) 2263 2264 switch key { 2265 case "level", "msg", "timeout_seconds", "duration_seconds", "walltime_seconds": 2266 case "target": 2267 require.Equal(t, s.target, val) 2268 case "type": 2269 require.Equal(t, s.prober.Name(), val) 2270 default: 2271 expected, found := tc.expectedLogEntries[key] 2272 require.Truef(t, found, "key=%s value=%s", key, val) 2273 require.Equalf(t, expected, val, "key=%s", key) 2274 2275 labelsFound++ 2276 } 2277 } 2278 2279 require.Equal(t, len(tc.expectedLogEntries), labelsFound) 2280 } 2281 2282 require.NoError(t, dec.Err()) 2283 2284 assertStructuredMetadata(t, entry, tc) 2285 } 2286 } 2287 2288 for name, tc := range testcases { 2289 t.Run(name, func(t *testing.T) { 2290 s := Scraper{ 2291 checkName: checkName, 2292 target: "test target", 2293 logger: testhelper.Logger(t), 2294 prober: testProber{}, 2295 labelsLimiter: testLabelsLimiter{ 2296 maxMetricLabels: tc.maxMetricLabels, 2297 maxLogLabels: tc.maxLogLabels, 2298 }, 2299 labellingMode: testLabellingMode{mode: tc.labelMode, err: tc.labelModeErr}, 2300 summaries: make(map[uint64]prometheus.Summary), 2301 histograms: make(map[uint64]prometheus.Histogram), 2302 check: model.Check{ 2303 Check: sm.Check{ 2304 Id: 1, 2305 TenantId: 2, 2306 Frequency: frequency, 2307 Timeout: frequency, 2308 Enabled: true, 2309 Target: checkTarget, 2310 Job: job, 2311 BasicMetricsOnly: true, 2312 Created: modifiedTs, 2313 Modified: modifiedTs, 2314 Labels: tc.checkLabels, 2315 // [Check.Type] panics if all settings are nil. To work around that, we add an empty, non nil 2316 // HTTP settings section. 2317 Settings: sm.CheckSettings{ 2318 Http: &sm.HttpSettings{}, 2319 }, 2320 }, 2321 }, 2322 probe: sm.Probe{ 2323 Id: 100, 2324 TenantId: 200, 2325 Name: probeName, 2326 Latitude: probeLatitude, 2327 Longitude: probeLongitde, 2328 Region: region, 2329 Labels: tc.probeLabels, 2330 }, 2331 } 2332 2333 data, duration, err := s.collectData(context.Background(), time.Unix(sampleTsMs/1000, 0)) 2334 require.NoError(t, err) 2335 require.NotNil(t, data) 2336 require.NotZero(t, duration) 2337 2338 for _, ts := range data.Metrics() { 2339 validateMetrics(t, ts, tc) 2340 } 2341 2342 for _, stream := range data.Streams() { 2343 validateStreams(t, s, stream, tc) 2344 } 2345 }) 2346 } 2347 } 2348 2349 func findPromPbLabel(t *testing.T, labels []prompb.Label, name string) string { 2350 idx := slices.IndexFunc(labels, func(v prompb.Label) bool { return v.GetName() == name }) 2351 require.GreaterOrEqualf(t, idx, int(0), "label %q must be present", name) 2352 2353 return labels[idx].GetValue() 2354 } 2355 2356 // histogramProber emits a single histogram (test_duration_seconds) with several 2357 // buckets, so the gathered metric families contain multiple *_bucket series that 2358 // differ only by their intrinsic `le` label. This is the shape that makes a 2359 // reserved-name collision destructive: unlike sm_check_info's single-valued 2360 // identity labels, `le` is per-series. 2361 type histogramProber struct{} 2362 2363 func (histogramProber) Name() string { return "histogram prober" } 2364 2365 func (histogramProber) Probe(_ context.Context, _ string, registry *prometheus.Registry, _ logger.Logger, _ string) (bool, float64) { 2366 h := prometheus.NewHistogram(prometheus.HistogramOpts{ 2367 Name: "test_duration_seconds", 2368 Buckets: []float64{0.1, 0.5, 1}, 2369 }) 2370 registry.MustRegister(h) 2371 2372 // Observations spread across buckets so every bucket series is distinct. 2373 h.Observe(0.05) 2374 h.Observe(0.3) 2375 h.Observe(0.8) 2376 2377 return true, 1 2378 } 2379 2380 // TestScraperCollectDataReservedLabelCollision pins the agent's behaviour when 2381 // a tenant carries a user label whose name collides with a *per-series 2382 // intrinsic* reserved label (`le`) while in UNPREFIXED mode. 2383 // 2384 // Per checks_extra.go, the API is supposed to make this unreachable: reserved 2385 // names are rejected at check create/update and at the LabelMode transition. 2386 // This test is the agent-side backstop for the case where such a label reaches 2387 // the scraper anyway. It asserts the safe contract: the histogram's real 2388 // bucket boundaries survive as distinct series ({0.1, 0.5, 1, +Inf}) and are 2389 // NOT collapsed onto the user's value. 2390 // 2391 // TODO(mem): Remove this note. This test FAILS against the current 2392 // implementation. customMetricLabels seeds the user `le` into 2393 // executionMetricLabels, appendDtoToTimeseries appends the real bucket `le` 2394 // last, and makeTimeseries de-dupes keeping the first occurrence, so every 2395 // bucket collapses to le=user-value. It will pass once per-series 2396 // reserved-name filtering is added to customMetricLabels. The same collapse 2397 // also corrupts the agent's own probe_all_duration_seconds histogram, so this 2398 // is not limited to custom prober metrics. 2399 func TestScraperCollectDataReservedLabelCollision(t *testing.T) { 2400 s := Scraper{ 2401 checkName: "check name", 2402 target: "test target", 2403 logger: testhelper.Logger(t), 2404 prober: histogramProber{}, 2405 labelsLimiter: testLabelsLimiter{maxMetricLabels: 22, maxLogLabels: 15}, 2406 labellingMode: testLabellingMode{mode: sm.LabelMode_LABEL_MODE_UNPREFIXED}, 2407 summaries: make(map[uint64]prometheus.Summary), 2408 histograms: make(map[uint64]prometheus.Histogram), 2409 check: model.Check{ 2410 Check: sm.Check{ 2411 Id: 1, 2412 TenantId: 2, 2413 Frequency: 2000, 2414 Timeout: 2000, 2415 Enabled: true, 2416 Target: "target name", 2417 Job: "job name", 2418 BasicMetricsOnly: false, 2419 Created: 42, 2420 Modified: 42, 2421 // User label collides with the reserved, per-series `le`. 2422 Labels: []sm.Label{{Name: "le", Value: "user-value"}}, 2423 Settings: sm.CheckSettings{Http: &sm.HttpSettings{}}, 2424 }, 2425 }, 2426 probe: sm.Probe{ 2427 Id: 100, 2428 TenantId: 200, 2429 Name: "probe name", 2430 Region: "REGION", 2431 }, 2432 } 2433 2434 data, _, err := s.collectData(context.Background(), time.Unix(3141, 0)) 2435 require.NoError(t, err) 2436 require.NotNil(t, data) 2437 2438 // Collect the `le` value from every test_duration_seconds_bucket 2439 // series. Scoped to this histogram so the count is deterministic; note 2440 // that the agent's own probe_all_duration_seconds_bucket series 2441 // collapse identically. 2442 bucketLEs := make(map[string]int) 2443 bucketSeries := 0 2444 2445 for _, ts := range data.Metrics() { 2446 name := findPromPbLabel(t, ts.GetLabels(), prom.MetricNameLabel) 2447 if name != "test_duration_seconds_bucket" { 2448 continue 2449 } 2450 2451 le := findPromPbLabel(t, ts.GetLabels(), labels.BucketLabel) 2452 2453 bucketSeries++ 2454 bucketLEs[le]++ 2455 } 2456 2457 // A 3-boundary histogram yields 4 bucket series: 0.1, 0.5, 1, +Inf. 2458 require.Equal(t, 4, bucketSeries, "expected one series per bucket boundary") 2459 2460 // The intended contract: real bucket boundaries survive, distinct and 2461 // un-clobbered by the user's `le=user-value`. 2462 require.Equal(t, 2463 map[string]int{"0.1": 1, "0.5": 1, "1": 1, "+Inf": 1}, 2464 bucketLEs, 2465 "histogram bucket `le` labels were overwritten/collapsed by the user label") 2466 } 2467 2468 // phaseProber emits probe_http_duration_seconds as a gauge with a `phase` 2469 // label, mirroring the real HTTP prober's per-phase timing breakdown. `phase` 2470 // is a reserved, per-series label (checks_extra.go) but, unlike `le` above, it 2471 // is a name a tenant might plausibly define themselves, which makes this the 2472 // more realistic collision vehicle. 2473 type phaseProber struct{} 2474 2475 func (phaseProber) Name() string { return "phase prober" } 2476 2477 func (phaseProber) Probe(_ context.Context, _ string, registry *prometheus.Registry, _ logger.Logger, _ string) (bool, float64) { 2478 g := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 2479 Name: "probe_http_duration_seconds", 2480 }, []string{"phase"}) 2481 registry.MustRegister(g) 2482 2483 // One series per HTTP phase, each with a distinct value. 2484 for i, phase := range []string{"resolve", "connect", "tls", "processing", "transfer"} { 2485 g.WithLabelValues(phase).Set(float64(i+1) * 0.01) 2486 } 2487 2488 return true, 1 2489 } 2490 2491 // TestScraperCollectDataReservedPhaseCollision is the realistic sibling of 2492 // TestScraperCollectDataReservedLabelCollision: it collides a user label with 2493 // the reserved per-series label `phase` on the multi-series 2494 // probe_http_duration_seconds gauge instead of `le` on a histogram. 2495 // 2496 // It feeds a check label phase=user-value in UNPREFIXED mode and asserts the 2497 // five per-phase series retain their distinct phase values. 2498 // 2499 // TODO(mem): remove this note. Like the `le` test above it FAILS against the 2500 // current implementation: customMetricLabels seeds the user `phase` into 2501 // executionMetricLabels ahead of each metric's own `phase` label 2502 // (appendDtoToTimeseries appends the metric's labels after sharedLabels), and 2503 // makeTimeseries' keep-first dedup drops the real phase, so all five series 2504 // collapse to phase=user-value. It will pass once per-series reserved-name 2505 // filtering is added to customMetricLabels. 2506 func TestScraperCollectDataReservedPhaseCollision(t *testing.T) { 2507 s := Scraper{ 2508 checkName: "check name", 2509 target: "test target", 2510 logger: testhelper.Logger(t), 2511 prober: phaseProber{}, 2512 labelsLimiter: testLabelsLimiter{maxMetricLabels: 22, maxLogLabels: 15}, 2513 labellingMode: testLabellingMode{mode: sm.LabelMode_LABEL_MODE_UNPREFIXED}, 2514 summaries: make(map[uint64]prometheus.Summary), 2515 histograms: make(map[uint64]prometheus.Histogram), 2516 check: model.Check{ 2517 Check: sm.Check{ 2518 Id: 1, 2519 TenantId: 2, 2520 Frequency: 2000, 2521 Timeout: 2000, 2522 Enabled: true, 2523 Target: "target name", 2524 Job: "job name", 2525 BasicMetricsOnly: false, 2526 Created: 42, 2527 Modified: 42, 2528 // User label collides with the reserved, per-series `phase`. 2529 Labels: []sm.Label{{Name: "phase", Value: "user-value"}}, 2530 Settings: sm.CheckSettings{Http: &sm.HttpSettings{}}, 2531 }, 2532 }, 2533 probe: sm.Probe{ 2534 Id: 100, 2535 TenantId: 200, 2536 Name: "probe name", 2537 Region: "REGION", 2538 }, 2539 } 2540 2541 data, _, err := s.collectData(context.Background(), time.Unix(3141, 0)) 2542 require.NoError(t, err) 2543 require.NotNil(t, data) 2544 2545 // Collect the `phase` value from every probe_http_duration_seconds series. 2546 phases := make(map[string]int) 2547 phaseSeries := 0 2548 2549 for _, ts := range data.Metrics() { 2550 name := findPromPbLabel(t, ts.GetLabels(), prom.MetricNameLabel) 2551 if name != "probe_http_duration_seconds" { 2552 continue 2553 } 2554 2555 phase := findPromPbLabel(t, ts.GetLabels(), "phase") 2556 2557 phaseSeries++ 2558 phases[phase]++ 2559 } 2560 2561 // Five phases emitted, so five distinct series. 2562 require.Equal(t, 5, phaseSeries, "expected one series per HTTP phase") 2563 2564 // The intended contract: each phase survives as its own series, un-clobbered 2565 // by the user's phase=user-value. 2566 require.Equal(t, map[string]int{ 2567 "resolve": 1, "connect": 1, "tls": 1, "processing": 1, "transfer": 1, 2568 }, phases, "per-phase series were overwritten/collapsed by the user label") 2569 } 2570 2571 // these are generated using 2572 // go run $(go env GOROOT)/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h 2573 2574 var localhostCert = []byte(`-----BEGIN CERTIFICATE----- 2575 MIICFDCCAX2gAwIBAgIRAIoIa8inD4pBali2dBpc6+wwDQYJKoZIhvcNAQELBQAw 2576 EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 2577 MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw 2578 gYkCgYEA5ii8ieKKayAtIX7BbKWB+zRDnJsFg02+cHwRy7nmX1t9MdObwNAMFIET 2579 zvKz4Yctgi3WoyMZfp6pYugHHye829DbUsm+GI6Ca3zSzADfz+zL/nIWiJje0NB1 2580 DXPASi03nTNk06O5RGlWeLqGKfyEY3xjcUrE7rKlNtLu8oK4Jm0CAwEAAaNoMGYw 2581 DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF 2582 MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA 2583 AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAyW2Yc/vnlO9PYcXw9ZRYt51hTCKCooGv 2584 RwR1OwL9Kdr/eY+wGpO+YsXxdSmRcmg467TA5d1YpssyVceKRla22NgDoZ2psFTe 2585 LqKfcZUN+jvtQMx4LnsRZcz2i2U35Biq4h0i3SOgROIOEjQJJ6I8wMw9jD5kS86Q 2586 IYZvskPli5s= 2587 -----END CERTIFICATE-----`) 2588 2589 var localhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- 2590 MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOYovIniimsgLSF+ 2591 wWylgfs0Q5ybBYNNvnB8Ecu55l9bfTHTm8DQDBSBE87ys+GHLYIt1qMjGX6eqWLo 2592 Bx8nvNvQ21LJvhiOgmt80swA38/sy/5yFoiY3tDQdQ1zwEotN50zZNOjuURpVni6 2593 hin8hGN8Y3FKxO6ypTbS7vKCuCZtAgMBAAECgYBzp1y2XOP5WL3U6wD/O1vJg0XG 2594 WA+5H0Pm+jFnEg81M6ABfbfyd5jaZNIzV7oURf0UQTxt1aFmAwxS6w1JForLZn3g 2595 PA3UVkDEZTl7C7h6kIY4PVzcki32V2YZ73e1zSCfvxIvbJ7SS697ua1sefIP5Gci 2596 HNSRzanUyOKCZ1Or8QJBAPB1A/66Baydh+2nXddaad9d5Ifjvklk60tozLYpx7Il 2597 R6NgU49Pa67MndGGykmbs094TYY8GswU2WoJVPUEYY8CQQD1CVB8++yK2jP0LkH/ 2598 s+3dag2S2IId5FwSbjiM8YtOFx2wN2HleX7Yr6ujPnKT+zy75eV0chPIP++QNKmy 2599 eIJDAkAJm/OD63Usl8MF2UljwMY4We03DP/euPy6L772jKbhVKIPQls0f+0CuESa 2600 SfOti15YD6uxcJd1jmO93A+cFwe7AkEAgLFnqHzXewWnC7PPzfA+GW+9uUYk8HYj 2601 NTrWUI/7zgOuAALWU6M/z6ZTyuTdYIMvHrBblpDjeuS5eU9vYOCR6QJAWT7VShIT 2602 sc8HRJOxUQWOI5hVUfLxVSosFDW7G5/75WlyeYwZOqOD54uNzavr8gpoTlvllUuP 2603 GcRWUmpDEz0t6w== 2604 -----END RSA TESTING KEY-----`)) 2605 2606 func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } 2607 2608 // testLogger impleents the Logger interface to pass it to the prober. 2609 type testLogger struct { 2610 w io.Writer 2611 } 2612 2613 func (l *testLogger) Log(kv ...any) error { 2614 var buf strings.Builder 2615 2616 for i, v := range kv { 2617 if i >= 2 && i%2 == 0 { 2618 buf.WriteString(", ") 2619 } 2620 2621 switch v := v.(type) { 2622 case string: 2623 buf.WriteString(v) 2624 2625 if i%2 == 0 { 2626 buf.WriteRune(':') 2627 } 2628 2629 case error: 2630 buf.WriteString(v.Error()) 2631 2632 case interface{ String() string }: 2633 buf.WriteString(v.String()) 2634 2635 default: 2636 fmt.Fprintf(&buf, "%#v", v) 2637 } 2638 } 2639 2640 fmt.Fprintf(l.w, "%s\n", buf.String()) 2641 2642 return nil 2643 } 2644 2645 func mergeMaps(mmaps ...map[string]string) map[string]string { 2646 out := make(map[string]string) 2647 for _, m := range mmaps { 2648 maps.Copy(out, m) 2649 } 2650 2651 return out 2652 } 2653 2654 func TestTruncateLabelValue(t *testing.T) { 2655 testcases := map[string]struct { 2656 length int 2657 expectedLength int 2658 }{ 2659 "zero": { 2660 length: 0, 2661 expectedLength: 0, 2662 }, 2663 "one": { 2664 length: 1, 2665 expectedLength: 1, 2666 }, 2667 "max/2": { 2668 length: maxLabelValueLength / 2, 2669 expectedLength: maxLabelValueLength / 2, 2670 }, 2671 "max-1": { 2672 length: maxLabelValueLength - 1, 2673 expectedLength: maxLabelValueLength - 1, 2674 }, 2675 "max": { 2676 length: maxLabelValueLength, 2677 expectedLength: maxLabelValueLength, 2678 }, 2679 "max+1": { 2680 length: maxLabelValueLength + 1, 2681 expectedLength: maxLabelValueLength, 2682 }, 2683 "2*max": { 2684 length: 2 * maxLabelValueLength, 2685 expectedLength: maxLabelValueLength, 2686 }, 2687 } 2688 2689 for name, tc := range testcases { 2690 t.Run(name, func(t *testing.T) { 2691 input := strings.Repeat("a", tc.length) 2692 expected := strings.Repeat("a", tc.expectedLength) 2693 actual := truncateLabelValue(input) 2694 require.Equal(t, len(expected), len(actual)) 2695 2696 if tc.expectedLength < tc.length { 2697 require.Equal(t, expected[:len(expected)-3], actual[:len(actual)-3]) 2698 require.Equal(t, "...", actual[len(actual)-3:]) 2699 } 2700 }) 2701 } 2702 } 2703 2704 type testRunner struct { 2705 metrics []byte 2706 logs []byte 2707 } 2708 2709 var _ k6runner.Runner = &testRunner{} 2710 2711 func (r *testRunner) Run(ctx context.Context, script k6runner.Script, secretStore k6runner.SecretStore, _ string) (*k6runner.RunResponse, error) { 2712 return &k6runner.RunResponse{ 2713 Metrics: r.metrics, 2714 Logs: r.logs, 2715 }, nil 2716 } 2717 2718 func (r *testRunner) WithLogger(logger *zerolog.Logger) k6runner.Runner { 2719 return r 2720 } 2721 2722 func (*testRunner) Versions(_ context.Context) <-chan []string { 2723 return nil // Blocks forever if read. 2724 } 2725 2726 type testCounter struct { 2727 count atomic.Int32 2728 } 2729 2730 func (c *testCounter) Inc() { 2731 c.count.Add(1) 2732 } 2733 2734 type testCounterVec struct { 2735 counters map[string]Incrementer 2736 t *testing.T 2737 } 2738 2739 func (c *testCounterVec) WithLabelValues(v ...string) Incrementer { 2740 require.Len(c.t, v, 1) 2741 2742 if _, found := c.counters[v[0]]; !found { 2743 c.counters[v[0]] = &testCounter{} 2744 } 2745 2746 return c.counters[v[0]] 2747 } 2748 2749 type testProberB struct { 2750 wantedFailures int32 2751 execCount int32 2752 failureCount int32 2753 } 2754 2755 func (p testProberB) Name() string { 2756 return "test prober" 2757 } 2758 2759 func (p *testProberB) Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger, _ string) (bool, float64) { 2760 p.execCount++ 2761 2762 if p.failureCount < p.wantedFailures { 2763 p.failureCount++ 2764 return false, 0 2765 } 2766 2767 return true, 1 2768 } 2769 2770 type testProbeFactory struct { 2771 builder func() prober.Prober 2772 } 2773 2774 func (f testProbeFactory) New(ctx context.Context, logger zerolog.Logger, check model.Check) (prober.Prober, string, error) { 2775 return f.builder(), check.Target, nil 2776 } 2777 2778 type testPublisher struct{} 2779 2780 func (testPublisher) Publish(pusher.Payload) { 2781 } 2782 2783 type testTelemeter struct { 2784 execCount int32 2785 ee []telemetry.Execution 2786 } 2787 2788 func (t *testTelemeter) AddExecution(e telemetry.Execution) { 2789 t.execCount++ 2790 } 2791 2792 // TestScraperRun will set up a scraper in such a way that it runs 5 times, and fails 2 out of those 5. 2793 // 2794 // This checks that the probe gets run, and that the metrics are correctly collected. 2795 func TestScraperRun(t *testing.T) { 2796 ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 2797 t.Cleanup(cancel) 2798 2799 var check model.Check 2800 2801 err := check.FromSM(sm.Check{ 2802 Id: 1, 2803 TenantId: 1000, 2804 Frequency: 100, 2805 Timeout: 1000, 2806 Enabled: true, 2807 Target: "127.0.0.1", 2808 Job: "test", 2809 Settings: sm.CheckSettings{ 2810 Ping: &sm.PingSettings{}, 2811 }, 2812 }) 2813 require.NoError(t, err) 2814 2815 var counter testCounter 2816 2817 errCounter := testCounterVec{counters: make(map[string]Incrementer), t: t} 2818 2819 testProber := &testProberB{wantedFailures: 2} 2820 testTelemeter := &testTelemeter{} 2821 2822 metrics := NewMetrics(&counter, &errCounter) 2823 2824 s, err := NewWithOpts(ctx, check, ScraperOpts{ 2825 Metrics: metrics, 2826 ProbeFactory: testProbeFactory{builder: func() prober.Prober { return testProber }}, 2827 Logger: zerolog.New(zerolog.NewTestWriter(t)), 2828 Publisher: &testPublisher{}, 2829 LabelsLimiter: testLabelsLimiter{ 2830 maxMetricLabels: 20, 2831 maxLogLabels: 15, 2832 }, 2833 LabellingMode: testLabellingMode{}, 2834 Telemeter: testTelemeter, 2835 CostAttributionLabels: testCalTenants{ 2836 costAttributionLabels: []string{"testing", "you"}, 2837 }, 2838 }) 2839 2840 require.NoError(t, err) 2841 require.NotNil(t, s) 2842 2843 s.Run(ctx) 2844 2845 require.Equal(t, testProber.execCount, counter.count.Load()) 2846 require.Len(t, errCounter.counters, 1) 2847 checkErrCounter, found := errCounter.counters["check"] 2848 require.True(t, found) 2849 require.Equal(t, testProber.failureCount, checkErrCounter.(*testCounter).count.Load()) 2850 2851 // Verify telemetry 2852 require.Equal(t, counter.count.Load(), testTelemeter.execCount) 2853 2854 for _, e := range testTelemeter.ee { 2855 require.Equal(t, 1000, e.LocalTenantID) 2856 require.Equal(t, check.Class(), e.CheckClass) 2857 require.Greater(t, 0, e.Duration) 2858 } 2859 } 2860 2861 func TestTickWithOffset(t *testing.T) { 2862 const ( 2863 WORK = 1 2864 IDLE = 2 2865 CLEANUP = 3 2866 ) 2867 2868 testcases := map[string]struct { 2869 timeout time.Duration 2870 period time.Duration 2871 offset time.Duration 2872 maxIdle time.Duration 2873 minGap time.Duration 2874 expected []int 2875 }{ 2876 "An idle worker running between regular runs": { 2877 timeout: 1050 * time.Millisecond, 2878 period: 500 * time.Millisecond, 2879 offset: 1, 2880 maxIdle: 100 * time.Millisecond, 2881 minGap: 50 * time.Millisecond, 2882 expected: []int{ 2883 WORK, // 0 2884 IDLE, // 100 2885 IDLE, // 200 2886 IDLE, // 300 2887 IDLE, // 400 2888 WORK, // 500 2889 IDLE, // 600 2890 IDLE, // 700 2891 IDLE, // 800 2892 IDLE, // 900 2893 WORK, // 1000 2894 CLEANUP, 2895 }, 2896 }, 2897 "An idle worker trying to run within gap duration of regular runs.": { 2898 timeout: 1050 * time.Millisecond, 2899 period: 500 * time.Millisecond, 2900 offset: 1, 2901 maxIdle: 100 * time.Millisecond, 2902 minGap: 150 * time.Millisecond, 2903 expected: []int{ 2904 WORK, // 0 2905 IDLE, // 200, there's no idle at 100 because it's too close to the previous run 2906 IDLE, // 300 2907 WORK, // 500, there's no idle at 400 because it's too close to the next run 2908 IDLE, // 700, there's no idle at 600 because it's too close to the previous run 2909 IDLE, // 800 2910 WORK, // 1000, there's no idle at 900 because it's too close to the next run 2911 CLEANUP, 2912 }, 2913 }, 2914 "A zero offset and a scraper that has already been cancelled.": { 2915 timeout: 0, // this asks for immediate cancellation 2916 period: 500 * time.Millisecond, 2917 offset: 0, 2918 maxIdle: 100 * time.Millisecond, 2919 minGap: 150 * time.Millisecond, 2920 expected: nil, 2921 }, 2922 "A zero offset and a short timeout.": { 2923 timeout: 25 * time.Millisecond, 2924 period: 50 * time.Millisecond, 2925 offset: 0, 2926 maxIdle: 100 * time.Millisecond, 2927 minGap: 150 * time.Millisecond, 2928 expected: []int{ 2929 WORK, // 0 2930 CLEANUP, 2931 }, 2932 }, 2933 "A zero offset and a larger timeout.": { 2934 timeout: 200 * time.Millisecond, 2935 period: 75 * time.Millisecond, 2936 offset: 0, 2937 maxIdle: 100 * time.Millisecond, 2938 minGap: 150 * time.Millisecond, 2939 expected: []int{ 2940 WORK, // 0 2941 WORK, // 75 2942 WORK, // 150 2943 CLEANUP, 2944 }, 2945 }, 2946 } 2947 2948 for name, tc := range testcases { 2949 t.Run(name, func(t *testing.T) { 2950 ctx, cancel := testhelper.Context(context.Background(), t) 2951 t.Cleanup(cancel) 2952 2953 // Append a particular marker to the results slice 2954 // depending on which function was called by 2955 // TestTickWithOffset. This allows us to test that the 2956 // correct function is being called in the correct 2957 // order. 2958 var results []int 2959 2960 work := func(context.Context, time.Time) { results = append(results, WORK) } 2961 idle := func(context.Context, time.Time) { results = append(results, IDLE) } 2962 cleanup := func(context.Context, time.Time) { results = append(results, CLEANUP) } 2963 2964 stop := make(chan struct{}) 2965 2966 if tc.timeout > 0 { 2967 go func() { 2968 time.Sleep(tc.timeout) 2969 close(stop) 2970 }() 2971 } else { 2972 // why not in the goroutine? Because we need to 2973 // make sure the channel is closed *before* 2974 // calling tickWithOffset 2975 close(stop) 2976 } 2977 2978 tickWithOffset(ctx, stop, work, idle, cleanup, tc.period, tc.offset, tc.maxIdle, tc.minGap) 2979 2980 require.Equal(t, tc.expected, results) 2981 }) 2982 } 2983 } 2984 2985 func TestGetCostAttributionLabels(t *testing.T) { 2986 t.Parallel() 2987 2988 testCases := []struct { 2989 name string 2990 tenantCALs []string 2991 checkLabels []sm.Label 2992 expectedResult []sm.CostAttributionLabel 2993 }{ 2994 { 2995 name: "no CALs for tenant returns empty slice", 2996 tenantCALs: []string{}, 2997 checkLabels: []sm.Label{{Name: "env", Value: "prod"}}, 2998 expectedResult: []sm.CostAttributionLabel{}, 2999 }, 3000 { 3001 name: "no matching labels returns all CALs with nil terminator", 3002 tenantCALs: []string{"env", "team"}, 3003 checkLabels: []sm.Label{{Name: "unrelated", Value: "value"}}, 3004 expectedResult: []sm.CostAttributionLabel{ 3005 {Name: "env", Value: telemetry.CalNilStringTerminator}, 3006 {Name: "team", Value: telemetry.CalNilStringTerminator}, 3007 }, 3008 }, 3009 { 3010 name: "some matching labels returns mix of values and nil terminators", 3011 tenantCALs: []string{"env", "team"}, 3012 checkLabels: []sm.Label{{Name: "env", Value: "prod"}}, 3013 expectedResult: []sm.CostAttributionLabel{ 3014 {Name: "env", Value: "prod"}, 3015 {Name: "team", Value: telemetry.CalNilStringTerminator}, 3016 }, 3017 }, 3018 { 3019 name: "all matching labels returns all values", 3020 tenantCALs: []string{"env", "team"}, 3021 checkLabels: []sm.Label{ 3022 {Name: "env", Value: "prod"}, 3023 {Name: "team", Value: "platform"}, 3024 }, 3025 expectedResult: []sm.CostAttributionLabel{ 3026 {Name: "env", Value: "prod"}, 3027 {Name: "team", Value: "platform"}, 3028 }, 3029 }, 3030 } 3031 3032 for _, tc := range testCases { 3033 t.Run(tc.name, func(t *testing.T) { 3034 t.Parallel() 3035 3036 calsProvider := &mockTenantCals{cals: tc.tenantCALs} 3037 3038 s := &Scraper{ 3039 cals: calsProvider, 3040 check: model.Check{Check: sm.Check{Labels: tc.checkLabels}}, 3041 } 3042 3043 result, err := s.getCostAttributionLabels(context.Background(), 1) 3044 require.NoError(t, err) 3045 require.ElementsMatch(t, tc.expectedResult, result) 3046 }) 3047 } 3048 } 3049 3050 type mockTenantCals struct { 3051 cals []string 3052 } 3053 3054 func (m *mockTenantCals) CostAttributionLabels(ctx context.Context, tenantID model.GlobalID) ([]string, error) { 3055 return m.cals, nil 3056 } 3057 3058 func TestExtractLogsK6TimeOverridesDefaultTimestamp(t *testing.T) { 3059 // Test that k6's "time" field overrides the default "ts" timestamp from the logger 3060 testCases := []struct { 3061 name string 3062 logs string 3063 sharedLabels []labelPair 3064 expected func(t *testing.T, streams Streams) 3065 }{ 3066 { 3067 name: "k6 time field overrides default ts", 3068 logs: `ts=2023-06-01T20:00:00Z time="2023-06-01T13:40:26-06:00" level=info msg="k6 message"`, 3069 sharedLabels: []labelPair{ 3070 {name: "probe", value: "test-probe"}, 3071 }, 3072 expected: func(t *testing.T, streams Streams) { 3073 require.Len(t, streams, 1) 3074 require.Len(t, streams[0].Entries, 1) 3075 entry := streams[0].Entries[0] 3076 3077 expectedTime, _ := time.Parse(time.RFC3339, "2023-06-01T13:40:26-06:00") 3078 require.Equal(t, expectedTime, entry.Timestamp, "should use k6 time field, not default ts") 3079 require.Equal(t, entry.Line, "level=info msg=\"k6 message\"\n") 3080 }, 3081 }, 3082 { 3083 name: "k6 time field before ts (order independence)", 3084 // Defensive case: time= appears before ts= in the log line. The guard on the ts= case 3085 // (if !t.IsZero() { continue }) ensures ts= does not overwrite a time= already seen. 3086 logs: `time="2023-06-01T13:40:26-06:00" ts=2023-06-01T20:00:00Z level=info msg="k6 message"`, 3087 sharedLabels: []labelPair{ 3088 {name: "probe", value: "test-probe"}, 3089 }, 3090 expected: func(t *testing.T, streams Streams) { 3091 require.Len(t, streams, 1) 3092 require.Len(t, streams[0].Entries, 1) 3093 entry := streams[0].Entries[0] 3094 3095 expectedTime, _ := time.Parse(time.RFC3339, "2023-06-01T13:40:26-06:00") 3096 require.Equal(t, expectedTime, entry.Timestamp, "should use k6 time field even when it precedes ts") 3097 require.Equal(t, entry.Line, "level=info msg=\"k6 message\"\n") 3098 }, 3099 }, 3100 { 3101 name: "BBE adapter time field with nanosecond precision is used as timestamp", 3102 logs: `ts=2023-06-01T20:00:00.500000000Z level=INFO msg="Making HTTP request" time=2023-06-01T20:00:00.600000000Z url=https://example.com host=example.com`, 3103 sharedLabels: []labelPair{ 3104 {name: "probe", value: "test-probe"}, 3105 }, 3106 expected: func(t *testing.T, streams Streams) { 3107 require.Len(t, streams, 1) 3108 require.Len(t, streams[0].Entries, 1) 3109 entry := streams[0].Entries[0] 3110 3111 // time= (600ms) should win over ts= (500ms), and must preserve nanosecond precision. 3112 expectedTime, _ := time.Parse(time.RFC3339Nano, "2023-06-01T20:00:00.600000000Z") 3113 require.Equal(t, expectedTime, entry.Timestamp, "should use nanosecond-precision time= field") 3114 // url= and host= should be in the body; time= should not. 3115 require.Contains(t, entry.Line, "url=https://example.com") 3116 require.Contains(t, entry.Line, "host=example.com") 3117 require.NotContains(t, entry.Line, "time=") 3118 }, 3119 }, 3120 { 3121 name: "ts field only (no k6 time)", 3122 logs: `ts=2023-06-01T20:00:00Z level=info msg="normal message"`, 3123 sharedLabels: []labelPair{ 3124 {name: "probe", value: "test-probe"}, 3125 }, 3126 expected: func(t *testing.T, streams Streams) { 3127 require.Len(t, streams, 1) 3128 require.Len(t, streams[0].Entries, 1) 3129 entry := streams[0].Entries[0] 3130 3131 expectedTime, _ := time.Parse(time.RFC3339Nano, "2023-06-01T20:00:00Z") 3132 require.Equal(t, expectedTime, entry.Timestamp) 3133 require.Equal(t, entry.Line, "level=info msg=\"normal message\"\n") 3134 }, 3135 }, 3136 } 3137 3138 for _, tc := range testCases { 3139 t.Run(tc.name, func(t *testing.T) { 3140 t.Parallel() 3141 3142 s := Scraper{ 3143 logger: testhelper.Logger(t), 3144 } 3145 3146 streams := s.extractLogs(time.Now(), []byte(tc.logs), tc.sharedLabels, nil) 3147 tc.expected(t, streams) 3148 }) 3149 } 3150 } 3151 3152 func TestHTTPCheckLogTimestampsAreNonDecreasing(t *testing.T) { 3153 ctx := context.Background() 3154 3155 p, check, cleanup := setupHTTPProbe(ctx, t) 3156 defer cleanup() 3157 3158 s := Scraper{ 3159 checkName: check.Settings.Http.String(), 3160 target: check.Target, 3161 logger: testhelper.Logger(t), 3162 prober: p, 3163 labelsLimiter: testLabelsLimiter{ 3164 maxMetricLabels: 20, 3165 maxLogLabels: 15, 3166 }, 3167 labellingMode: testLabellingMode{}, 3168 summaries: make(map[uint64]prometheus.Summary), 3169 histograms: make(map[uint64]prometheus.Histogram), 3170 check: check, 3171 probe: sm.Probe{ 3172 Name: "test-probe", 3173 Region: "test-region", 3174 }, 3175 } 3176 3177 // A scheduled scrape may carry an older ticker value for metric stamping, 3178 // but its wrapper logs must retain their original wall-clock behavior. The 3179 // unscheduled collector has separate coverage for logical timestamps. 3180 wallStart := time.Now() 3181 data, _, err := s.collectData(ctx, time.Date(2020, 3, 1, 12, 0, 0, 0, time.UTC)) 3182 require.NoError(t, err) 3183 require.NotNil(t, data) 3184 3185 streams := data.Streams() 3186 require.NotEmpty(t, streams, "expected at least one log stream") 3187 3188 for _, stream := range streams { 3189 entries := stream.Entries 3190 require.NotEmpty(t, entries, "expected log entries in stream") 3191 3192 // All 7 expected messages must be present. 3193 msgs := make([]string, 0, len(entries)) 3194 for _, e := range entries { 3195 if strings.Contains(e.Line, `msg="Beginning check"`) || strings.Contains(e.Line, `msg="Check succeeded"`) { 3196 require.False(t, e.Timestamp.Before(wallStart), "scheduled wrapper log used the logical metric timestamp: %s", e.Timestamp) 3197 } 3198 3199 dec := logfmt.NewDecoder(strings.NewReader(e.Line)) 3200 for dec.ScanRecord() { 3201 for dec.ScanKeyval() { 3202 if string(dec.Key()) == "msg" { 3203 msgs = append(msgs, string(dec.Value())) 3204 } 3205 } 3206 } 3207 } 3208 3209 expectedMsgs := []string{ 3210 "Beginning check", 3211 "Resolving target address", 3212 "Resolved target address", 3213 "Making HTTP request", 3214 "Received HTTP response", 3215 "Response timings for roundtrip", 3216 "Check succeeded", 3217 } 3218 for _, expected := range expectedMsgs { 3219 require.Contains(t, msgs, expected, "missing log entry: %q", expected) 3220 } 3221 3222 // Entry timestamps must be non-decreasing (Loki stream ordering requirement). 3223 for i := 1; i < len(entries); i++ { 3224 require.False(t, 3225 entries[i].Timestamp.Before(entries[i-1].Timestamp), 3226 "entry[%d] timestamp %v is before entry[%d] timestamp %v (out-of-order)\nentry[%d] msg: %s\nentry[%d] msg: %s", 3227 i, entries[i].Timestamp, 3228 i-1, entries[i-1].Timestamp, 3229 i-1, entries[i-1].Line, 3230 i, entries[i].Line, 3231 ) 3232 } 3233 } 3234 }