github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/scraper/scraper.go (about) 1 package scraper 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "math" 9 "math/rand" 10 "slices" 11 "strconv" 12 "strings" 13 "time" 14 15 kitlog "github.com/go-kit/kit/log" //nolint:staticcheck // TODO(mem): replace in BBE 16 "github.com/go-kit/kit/log/level" //nolint:staticcheck // TODO(mem): replace in BBE 17 "github.com/go-logfmt/logfmt" 18 "github.com/google/uuid" 19 "github.com/mmcloughlin/geohash" 20 "github.com/prometheus/client_golang/prometheus" 21 dto "github.com/prometheus/client_model/go" 22 prom "github.com/prometheus/common/model" 23 "github.com/prometheus/prometheus/prompb" 24 "github.com/rs/zerolog" 25 26 logproto "github.com/grafana/loki/pkg/push" 27 "github.com/grafana/synthetic-monitoring-agent/internal/feature" 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/pusher" 32 "github.com/grafana/synthetic-monitoring-agent/internal/secrets" 33 "github.com/grafana/synthetic-monitoring-agent/internal/telemetry" 34 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 35 ) 36 37 const ( 38 ProbeSuccessMetricName = "probe_success" 39 CheckInfoMetricName = "sm_check_info" 40 CheckInfoSource = "synthetic-monitoring-agent" 41 maxLabelValueLength = 2048 // this is the default value in Prometheus 42 maxPublishInterval = 2 * time.Minute 43 minPublishGap = 10 * time.Second 44 ) 45 46 const ( 47 probeLabelName = "probe" 48 regionLabelName = "region" 49 smRegionLabelName = "sm_region" 50 unsetRegionValue = "unset" 51 configVersionLabelName = "config_version" 52 instanceLabelName = "instance" 53 checkNameLabelName = "check_name" 54 ) 55 56 // mimirMaxLabelsIngest protects us and Mimir from hitting an error due to 57 // ingest limit; is short of the true ingest limit to retain room for growth. 58 const mimirMaxLabelsIngest = 40 59 60 var ( 61 staleNaN uint64 = 0x7ff0000000000002 62 staleMarker float64 = math.Float64frombits(staleNaN) 63 ) 64 65 type Metrics interface { 66 AddScrape() 67 AddCheckError() 68 AddCollectorError() 69 } 70 71 type LabelsLimiter interface { 72 MetricLabels(ctx context.Context, tenantID model.GlobalID) (int, error) 73 LogLabels(ctx context.Context, tenantID model.GlobalID) (int, error) 74 } 75 76 type TenantCals interface { 77 CostAttributionLabels(ctx context.Context, tenantID model.GlobalID) ([]string, error) 78 } 79 80 type TenantLabelMode interface { 81 ForTenant(ctx context.Context, tenantID model.GlobalID) (sm.LabelMode, error) 82 } 83 84 type Telemeter interface { 85 AddExecution(e telemetry.Execution) 86 } 87 88 type Scraper struct { 89 publisher pusher.Publisher 90 cancel context.CancelFunc 91 checkName string 92 target string 93 logger zerolog.Logger 94 check model.Check 95 probe sm.Probe 96 prober prober.Prober 97 labelsLimiter LabelsLimiter 98 labellingMode TenantLabelMode 99 stop chan struct{} 100 metrics Metrics 101 summaries map[uint64]prometheus.Summary 102 histograms map[uint64]prometheus.Histogram 103 telemeter Telemeter 104 cals TenantCals 105 } 106 107 type Factory func( 108 ctx context.Context, check model.Check, publisher pusher.Publisher, probe sm.Probe, 109 features feature.Collection, 110 logger zerolog.Logger, 111 metrics Metrics, 112 k6runner k6runner.Runner, 113 labelsLimiter LabelsLimiter, 114 telemeter *telemetry.Telemeter, 115 secretStore secrets.SecretProvider, 116 cals TenantCals, 117 labellingMode TenantLabelMode, 118 ) (*Scraper, error) 119 120 type ( 121 TimeSeries = []prompb.TimeSeries 122 Streams = []logproto.Stream 123 ) 124 125 type probeData struct { 126 tenantId model.GlobalID 127 ts TimeSeries 128 streams Streams 129 } 130 131 func (d *probeData) Metrics() TimeSeries { 132 return d.ts 133 } 134 135 func (d *probeData) Streams() Streams { 136 return d.streams 137 } 138 139 func (d *probeData) Tenant() model.GlobalID { 140 return d.tenantId 141 } 142 143 func New( 144 ctx context.Context, check model.Check, publisher pusher.Publisher, probe sm.Probe, 145 features feature.Collection, 146 logger zerolog.Logger, 147 metrics Metrics, 148 k6runner k6runner.Runner, 149 labelsLimiter LabelsLimiter, 150 telemeter *telemetry.Telemeter, 151 secretStore secrets.SecretProvider, 152 cals TenantCals, 153 labellingMode TenantLabelMode, 154 ) (*Scraper, error) { 155 return NewWithOpts(ctx, check, ScraperOpts{ 156 Probe: probe, 157 Publisher: publisher, 158 Logger: logger, 159 Metrics: metrics, 160 ProbeFactory: prober.NewProberFactory(k6runner, probe.Id, features, secretStore), 161 LabelsLimiter: labelsLimiter, 162 Telemeter: telemeter, 163 CostAttributionLabels: cals, 164 LabellingMode: labellingMode, 165 }) 166 } 167 168 var _ Factory = New 169 170 type ScraperOpts struct { 171 Probe sm.Probe 172 Publisher pusher.Publisher 173 Logger zerolog.Logger 174 Metrics Metrics 175 ProbeFactory prober.ProberFactory 176 LabelsLimiter LabelsLimiter 177 LabellingMode TenantLabelMode 178 Telemeter Telemeter 179 CostAttributionLabels TenantCals 180 } 181 182 func NewWithOpts(ctx context.Context, check model.Check, opts ScraperOpts) (*Scraper, error) { 183 checkName := check.Type().String() 184 185 logger := opts.Logger.With(). 186 Int("region_id", check.RegionId). 187 Int64("tenantId", check.TenantId). 188 Int64("check_id", check.Id). 189 Str(probeLabelName, opts.Probe.Name). 190 Str("target", check.Target). 191 Str("job", check.Job). 192 Str("check", checkName). 193 Logger() 194 195 sctx, cancel := context.WithCancel(ctx) 196 197 smProber, target, err := opts.ProbeFactory.New(sctx, logger, check) 198 if err != nil { 199 cancel() 200 return nil, err 201 } 202 203 return &Scraper{ 204 publisher: opts.Publisher, 205 cancel: cancel, 206 checkName: checkName, 207 target: target, 208 logger: logger, 209 check: check, 210 probe: opts.Probe, 211 prober: smProber, 212 labelsLimiter: opts.LabelsLimiter, 213 labellingMode: opts.LabellingMode, 214 stop: make(chan struct{}), 215 metrics: opts.Metrics, 216 summaries: make(map[uint64]prometheus.Summary), 217 histograms: make(map[uint64]prometheus.Histogram), 218 telemeter: opts.Telemeter, 219 cals: opts.CostAttributionLabels, 220 }, nil 221 } 222 223 var ( 224 errCheckFailed = errors.New("probe failed") 225 errUnsupportedMetric = errors.New("unsupported metric type") 226 errTooManyLabels = errors.New("invalid configuration, too many labels") 227 ) 228 229 type checkStateMachine struct { 230 passes int 231 failures int 232 threshold int 233 } 234 235 func (sm *checkStateMachine) fail(cb func()) { 236 wasFailing := sm.isFailing() 237 sm.passes = 0 238 sm.failures++ 239 isFailing := sm.isFailing() 240 241 if isFailing != wasFailing { 242 cb() 243 } 244 } 245 246 func (sm *checkStateMachine) pass(cb func()) { 247 wasPassing := sm.isPassing() 248 sm.passes++ 249 sm.failures = 0 250 isPassing := sm.isPassing() 251 252 if isPassing != wasPassing { 253 cb() 254 } 255 } 256 257 func (sm checkStateMachine) isPassing() bool { 258 return sm.passes > sm.threshold 259 } 260 261 func (sm checkStateMachine) isFailing() bool { 262 return sm.failures > sm.threshold 263 } 264 265 func (s *Scraper) Run(ctx context.Context) { 266 s.logger.Info().Msg("starting scraper") 267 268 // TODO(mem): keep count of the number of successive errors and 269 // collect logs if threshold is reached. 270 271 var ( 272 frequency = ms(s.check.Frequency) 273 offset = ms(s.check.Offset) 274 ) 275 276 if offset == 0 { 277 offset = randDuration(min(frequency, maxPublishInterval)) 278 } 279 280 scrapeHandler := scrapeHandler{scraper: s} 281 282 tickWithOffset( 283 ctx, 284 s.stop, 285 scrapeHandler.scrape, scrapeHandler.republish, scrapeHandler.cleanup, 286 frequency, offset, 287 maxPublishInterval, minPublishGap, 288 ) 289 290 s.cancel() 291 292 s.logger.Info().Msg("scraper stopped") 293 } 294 295 type scrapeHandler struct { 296 scraper *Scraper 297 payload *probeData // needed to keep the most recently published payload for republication and clean up 298 sm checkStateMachine 299 } 300 301 func (h *scrapeHandler) scrape(ctx context.Context, t time.Time) { 302 h.scraper.metrics.AddScrape() 303 304 var ( 305 err error 306 duration time.Duration 307 ) 308 309 h.payload, duration, err = h.scraper.collectData(ctx, t) 310 311 switch { 312 case errors.Is(err, errCheckFailed): 313 h.scraper.metrics.AddCheckError() 314 h.sm.fail(func() { 315 h.scraper.logger.Info().Err(err).Msg("check entered FAIL state") 316 }) 317 318 case err != nil: 319 h.scraper.metrics.AddCollectorError() 320 h.scraper.logger.Error().Err(err).Msg("error collecting data") 321 322 return 323 324 default: 325 h.sm.pass(func() { 326 h.scraper.logger.Info().Msg("check entered PASS state") 327 }) 328 } 329 330 costAttributionLabels, err := h.scraper.getCostAttributionLabels(ctx, h.payload.tenantId) 331 if err != nil { 332 // If cals can't be found, do not block 333 h.scraper.logger.Error(). 334 Int64("tenantId", int64(h.payload.tenantId)). 335 Msg("Could not load cals") 336 } 337 338 h.scraper.logger.Debug(). 339 Int64("tenantId", int64(h.payload.tenantId)). 340 Int("costAttributionLabelsCount", len(costAttributionLabels)). 341 Any("CostAttributionLabels", costAttributionLabels). 342 Msg("tenant telemetry") 343 344 // If we are dropping the data in case of errors, we should not count that execution. 345 h.scraper.telemeter.AddExecution(telemetry.Execution{ 346 LocalTenantID: h.scraper.check.TenantId, 347 RegionID: int32(h.scraper.check.RegionId), 348 CheckClass: h.scraper.check.Class(), 349 CostAttributionLabels: costAttributionLabels, 350 Duration: duration, 351 }) 352 353 if h.payload != nil { 354 h.scraper.publisher.Publish(h.payload) 355 } 356 } 357 358 func (h *scrapeHandler) republish(ctx context.Context, t time.Time) { 359 if h.payload == nil { 360 return 361 } 362 363 h.payload.streams = nil // do not republish logs 364 365 // Update the timestamps of all collected samples to now. 366 now := t.UnixMilli() 367 368 for i := range h.payload.ts { 369 ts := &h.payload.ts[i] 370 for j := range ts.Samples { 371 ts.Samples[j].Timestamp = now 372 } 373 } 374 375 h.scraper.publisher.Publish(h.payload) 376 } 377 378 func (h *scrapeHandler) cleanup(ctx context.Context, t time.Time) { 379 if h.payload == nil { 380 return 381 } 382 383 staleSample := prompb.Sample{ 384 Timestamp: t.UnixNano()/1e6 + 1, // ms 385 Value: staleMarker, 386 } 387 388 for i := range h.payload.ts { 389 ts := &h.payload.ts[i] 390 for j := range ts.Samples { 391 ts.Samples[j] = staleSample 392 } 393 } 394 395 h.payload.streams = nil 396 397 h.scraper.publisher.Publish(h.payload) 398 399 h.payload = nil 400 } 401 402 func ms(n int64) time.Duration { 403 return time.Duration(n) * time.Millisecond 404 } 405 406 func randDuration(d time.Duration) time.Duration { 407 return time.Duration(rand.Int63n(int64(d))) 408 } 409 410 func (s *Scraper) Stop() { 411 s.logger.Info().Msg("stopping scraper") 412 close(s.stop) 413 } 414 415 func (s Scraper) CheckType() sm.CheckType { 416 return s.check.Type() 417 } 418 419 func (s Scraper) ConfigVersion() string { 420 return s.check.ConfigVersion() 421 } 422 423 func (s Scraper) LastModified() float64 { 424 return s.check.Modified 425 } 426 427 func tickWithOffset( 428 ctx context.Context, 429 stop <-chan struct{}, 430 work, idle, cleanup func(context.Context, time.Time), 431 period, offset, maxIdle, minGap time.Duration, 432 ) { 433 // wait for up to offset duration, paying attention to cancellation signals. 434 offsetTimer := time.NewTimer(offset) 435 436 var lastTick time.Time 437 438 drainOffsetTimer := func() { 439 if !offsetTimer.Stop() { 440 <-offsetTimer.C 441 } 442 } 443 444 // Check context and stop channels first, to avoid runnig the work 445 // function if we have been cancelled already. This can happen with a 0 446 // offset, and the Go runtime choosing to send the offset tick before 447 // other two channels. 448 select { 449 case <-ctx.Done(): 450 drainOffsetTimer() 451 return 452 453 case <-stop: 454 drainOffsetTimer() 455 // we haven't done anything yet, no clean up 456 return 457 458 default: 459 } 460 461 select { 462 case <-ctx.Done(): 463 drainOffsetTimer() 464 return 465 466 case <-stop: 467 drainOffsetTimer() 468 // we haven't done anything yet, no clean up 469 return 470 471 case t := <-offsetTimer.C: 472 lastTick = t 473 work(ctx, t) 474 } 475 476 // create a ticker that won't fire, and replace it with a running 477 // ticker in case we need it. 478 inactivityTicker := &time.Ticker{} 479 480 if period > maxIdle { 481 inactivityTicker = time.NewTicker(maxIdle) 482 } 483 484 workTicker := time.NewTicker(period) 485 486 for { 487 select { 488 case <-ctx.Done(): 489 workTicker.Stop() 490 inactivityTicker.Stop() 491 // no clean up if the context is cancelled. 492 return 493 494 case <-stop: 495 workTicker.Stop() 496 inactivityTicker.Stop() 497 // if we are here, we already pushed something 498 // at least once, lastTick cannot be zero, but 499 // just in case... 500 if !lastTick.IsZero() { 501 cleanup(ctx, lastTick) 502 } 503 504 return 505 506 case t := <-inactivityTicker.C: 507 // if the amount of time since the last run is greater 508 // than minGap and the amount of time left until the 509 // next run is larger than minGap, then run the idle 510 // function. 511 // 512 // p----I----I----Ip---I----I----I-p 513 // ^ ^ ^ 514 // | | +-- don't run here because it's too close to the next run 515 // | +-- run here 516 // +-- don't run here because it's too close to the last run 517 if t.Sub(lastTick) >= minGap && lastTick.Add(period).Sub(t) >= minGap { 518 idle(ctx, t) 519 } 520 521 case t := <-workTicker.C: 522 lastTick = t 523 work(ctx, t) 524 } 525 } 526 } 527 528 // CollectData runs the configured prober once at time t and returns transformed 529 // metrics and logs without publishing. A failed probe returns populated 530 // metrics and logs alongside a non-nil error; fatal collection errors return 531 // no data. 532 func (s *Scraper) CollectData(ctx context.Context, t time.Time) (TimeSeries, Streams, model.GlobalID, time.Duration, error) { 533 pd, d, err := s.collectDataWith(ctx, t, logicalScrapeClock(t)) 534 if err != nil && !errors.Is(err, errCheckFailed) { 535 return nil, nil, 0, 0, err 536 } 537 538 return pd.Metrics(), pd.Streams(), pd.Tenant(), d, err 539 } 540 541 func (s Scraper) collectData(ctx context.Context, t time.Time) (*probeData, time.Duration, error) { 542 return s.collectDataWith(ctx, t, scheduledScrapeClock(t)) 543 } 544 545 func (s Scraper) collectDataWith(ctx context.Context, t time.Time, clock scrapeClock) (*probeData, time.Duration, error) { 546 target := s.target 547 548 labelMode, err := s.labellingMode.ForTenant(ctx, s.check.GlobalTenantID()) 549 if err != nil { 550 // ForTenant falls back to UNPREFIXED on transient errors so that a temporary tenant 551 // cache miss does not abort the scrape. 552 s.logger.Warn().Err(err).Msg("could not retrieve tenant label mode, falling back to UNPREFIXED") 553 } 554 555 // TODO(mem): this is constant for the scraper, move this 556 // outside this function? 557 // sysMetricLabels are the common labels present on ALL metrics 558 // emitted by the system. 559 sysMetricLabels := []labelPair{ 560 {name: probeLabelName, value: s.probe.Name}, 561 {name: configVersionLabelName, value: s.check.ConfigVersion()}, 562 {name: instanceLabelName, value: s.check.Target}, 563 {name: "job", value: s.check.Job}, 564 // {name: "source", value: CheckInfoSource}, // identify metrics that belong to synthetic-monitoring-agent 565 } 566 567 var ( 568 // These are the labels defined by the user. 569 userLabels = buildUserLabels(s.probe.Labels, s.check.Labels, labelMode) 570 // These labels are applied to the sm_check_info metric. 571 checkInfoLabels = s.buildCheckInfoLabels(userLabels, sysMetricLabels, labelMode) 572 // This is the execution ID for the check run. 573 executionID = uuid.New().String() 574 ) 575 576 maxMetricLabels, err := s.labelsLimiter.MetricLabels(ctx, s.check.GlobalTenantID()) 577 if err != nil { 578 return nil, 0, fmt.Errorf("retrieving tenant metric labels limit: %w", err) 579 } 580 581 maxLogLabels, err := s.labelsLimiter.LogLabels(ctx, s.check.GlobalTenantID()) 582 if err != nil { 583 return nil, 0, fmt.Errorf("retrieving tenant log labels limit: %w", err) 584 } 585 586 // In DUAL_WRITE mode, buildUserLabels emits both prefixed and un-prefixed 587 // forms, so the user portion can be up to 2× the user label entries. 588 // Cap at mimirMaxLabelsIngest to avoid false-firing while still guarding 589 // against genuinely invalid sizes. 590 checkInfoLimit := min(maxMetricLabels*2, mimirMaxLabelsIngest) 591 if labelMode != sm.LabelMode_LABEL_MODE_DUAL_WRITE { 592 checkInfoLimit = maxMetricLabels 593 } 594 595 if len(checkInfoLabels) > checkInfoLimit { 596 // This should never happen. 597 return nil, 0, fmt.Errorf("%w: %d", errTooManyLabels, len(checkInfoLabels)) 598 } 599 600 logLabels := []labelPair{ 601 {name: probeLabelName, value: s.probe.Name}, 602 regionLabelForMode(labelMode, s.probe.Region), 603 {name: instanceLabelName, value: s.check.Target}, 604 {name: "job", value: s.check.Job}, 605 {name: checkNameLabelName, value: s.checkName}, 606 {name: "source", value: CheckInfoSource}, // identify log lines that belong to synthetic-monitoring-agent 607 } 608 logLabels = mergeLogLabels(logLabels, userLabels) 609 610 // set up logger to capture check logs 611 logs := bytes.Buffer{} 612 bl := kitlog.NewLogfmtLogger(&logs) 613 614 // set up logger to capture all the labels as part of the log entry 615 loggerLabels := make([]any, 0, 2*(2+len(logLabels))) 616 617 loggerLabels = append(loggerLabels, "ts", clock.wrapperLogTimestamp(), "target", target) 618 for _, l := range logLabels { 619 loggerLabels = append(loggerLabels, l.name, l.value) 620 } 621 622 sl := kitlog.With(bl, loggerLabels...) 623 624 var timeout time.Duration 625 626 switch s.CheckType() { 627 case sm.CheckTypeMultiHttp, sm.CheckTypeScripted, sm.CheckTypeBrowser: 628 // k6-backed checks have a more sophisticated handling of timeouts, where the k6 runner receives the check 629 // timeout from the specification and may implement a retrying mechanism. For this reason, we set the prober 630 // timeout to the check frequency instead, which is the hard limit at which no further retries are possible as 631 // the next execution is due. 632 timeout = time.Duration(s.check.Frequency) * time.Millisecond 633 default: 634 timeout = time.Duration(s.check.Timeout) * time.Millisecond 635 } 636 637 wallStart := time.Now() 638 639 success, mfs, err := getProbeMetrics( 640 ctx, 641 s.prober, 642 target, 643 timeout, 644 checkInfoLabels, 645 s.summaries, s.histograms, 646 sl, 647 s.check.BasicMetricsOnly, 648 executionID, 649 clock, 650 ) 651 if err != nil { 652 return nil, 0, err 653 } 654 655 duration, found := patchDuration(mfs) 656 if !found { 657 duration = clock.fallbackDuration(wallStart) 658 } 659 660 // At this point we know the check has been executed, regardless of 661 // whether it succeeded or not. 662 663 // In DUAL_WRITE and UNPREFIXED modes, un-prefixed user labels are appended to 664 // execution metrics (probe_success, probe_duration_seconds, probe_http_*, etc.). 665 // user-defined label values are retained over system-defined values. 666 executionMetricLabels := appendUniqueLabels(customMetricLabels(s.probe.Labels, s.check.Labels, labelMode), sysMetricLabels) 667 668 ts := s.extractTimeseries(t, mfs, executionMetricLabels) 669 670 successValue := "1" 671 672 if !success { 673 err = errCheckFailed 674 successValue = "0" 675 } 676 677 // Split logLabels into stream labels and structured metadata overflow. 678 // maxLogLabels - 1 reserves one slot for probe_success which is always appended. 679 var ( 680 streamLogLabels []labelPair 681 overflowMetadata []logproto.LabelAdapter 682 ) 683 684 maxUserLogLabels := maxLogLabels - 1 685 if len(logLabels) > maxUserLogLabels { 686 streamLogLabels = logLabels[:maxUserLogLabels] 687 for _, lp := range logLabels[maxUserLogLabels:] { 688 overflowMetadata = append(overflowMetadata, logproto.LabelAdapter{Name: lp.name, Value: lp.value}) 689 } 690 } else { 691 streamLogLabels = logLabels 692 } 693 694 streamLogLabels = append(streamLogLabels, labelPair{name: ProbeSuccessMetricName, value: successValue}) // identify log lines that are failures 695 696 // full set of structured metadata consists of any overflow labels and the execution_id 697 overflowMetadata = append(overflowMetadata, logproto.LabelAdapter{Name: "execution_id", Value: executionID}) 698 structuredMetadata := overflowMetadata 699 700 // streams need to have all the labels applied to them because loki does not support joins 701 streams := s.extractLogs(t, logs.Bytes(), streamLogLabels, structuredMetadata) 702 703 return &probeData{ts: ts, streams: streams, tenantId: s.check.GlobalTenantID()}, duration, err 704 } 705 706 // getCostAttributionLabels looks for the cost attribution labels for a specific tenant and 707 // generates a []sm.CostAttributionLabel where the Name's are the cal, and the value 708 // is derived from the checks label with the corresponding name. 709 func (s *Scraper) getCostAttributionLabels(ctx context.Context, tenantID model.GlobalID) ([]sm.CostAttributionLabel, error) { 710 cals, err := s.cals.CostAttributionLabels(ctx, tenantID) 711 if err != nil { 712 return nil, err 713 } 714 715 vals := make([]sm.CostAttributionLabel, 0, len(cals)) 716 for _, cal := range cals { 717 // Make the assumption there are no labels that match the CAL and set the default value to __MISSING__ 718 value := telemetry.CalNilStringTerminator 719 720 idx := slices.IndexFunc(s.check.Labels, func(l sm.Label) bool { 721 return l.Name == cal 722 }) 723 if idx >= 0 { 724 value = s.check.Labels[idx].Value 725 } 726 727 vals = append(vals, sm.CostAttributionLabel{ 728 Name: cal, 729 Value: value, 730 }) 731 } 732 733 return vals, nil 734 } 735 736 // patchDuration will modify the probe_duration_seconds metric to match the 737 // probe_script_duration_seconds metric. This is necessary because the 738 // probe_duration_seconds metric includes the time it takes waiting to run the 739 // script plus the time it takes to actually run the script. 740 // 741 // TODO(mem): find a better way to handle this. 742 func patchDuration(mfs []*dto.MetricFamily) (time.Duration, bool) { 743 var duration, scriptDuration *dto.Gauge 744 745 getGauge := func(mf *dto.MetricFamily) *dto.Gauge { 746 for _, m := range mf.GetMetric() { 747 if g := m.GetGauge(); g != nil { 748 return g 749 } 750 } 751 752 return nil 753 } 754 755 for _, mf := range mfs { 756 switch mf.GetName() { 757 case "probe_duration_seconds": 758 duration = getGauge(mf) 759 760 case "probe_script_duration_seconds": 761 scriptDuration = getGauge(mf) 762 } 763 764 if duration != nil && scriptDuration != nil { 765 *duration.Value = *scriptDuration.Value 766 break 767 } 768 } 769 770 if duration != nil { 771 return time.Duration(*duration.Value * float64(time.Second)), true 772 } 773 774 return 0, false 775 } 776 777 func getProbeMetrics( 778 ctx context.Context, 779 prober prober.Prober, 780 target string, 781 timeout time.Duration, 782 checkInfoLabels map[string]string, 783 summaries map[uint64]prometheus.Summary, 784 histograms map[uint64]prometheus.Histogram, 785 logger kitlog.Logger, 786 basicMetricsOnly bool, 787 executionID string, 788 clock scrapeClock, 789 ) (bool, []*dto.MetricFamily, error) { 790 registry := prometheus.NewRegistry() 791 792 success := runProber(ctx, prober, target, timeout, registry, checkInfoLabels, logger, executionID, clock) 793 794 mfs, err := registry.Gather() 795 if err != nil { 796 return success, nil, fmt.Errorf(`extracting data from blackbox-exporter: %w`, err) 797 } 798 799 registry = prometheus.NewRegistry() 800 801 if err := getDerivedMetrics(mfs, summaries, histograms, registry, basicMetricsOnly); err != nil { 802 return success, nil, fmt.Errorf(`getting derived metrics: %w`, err) 803 } 804 805 dmfs, err := registry.Gather() 806 if err != nil { 807 return success, nil, fmt.Errorf(`extracting derived metrics: %w`, err) 808 } 809 810 mfs = append(mfs, dmfs...) 811 812 return success, mfs, nil 813 } 814 815 func runProber( 816 ctx context.Context, 817 prober prober.Prober, 818 target string, 819 timeout time.Duration, 820 registry *prometheus.Registry, 821 checkInfoLabels map[string]string, 822 logger kitlog.Logger, 823 executionID string, 824 clock scrapeClock, 825 ) bool { 826 wallStart := time.Now() 827 828 // The begin log needs no explicit time: the wrapper `ts` valuer already 829 // stamps it at the stamper's start time (logical event time for the 830 // collector, wall-clock now for the scheduled scraper). 831 _ = level.Info(logger).Log("msg", "Beginning check", "type", prober.Name(), "timeout_seconds", timeout.Seconds()) 832 833 checkCtx, cancel := context.WithTimeout(ctx, timeout) 834 defer cancel() 835 836 success, duration := prober.Probe(checkCtx, target, registry, logger, executionID) 837 838 wallDuration := time.Since(wallStart).Seconds() 839 if duration == 0 { 840 // If the prober did not provide their own duration, fallback to the wall time the scraper took to run. 841 duration = wallDuration 842 } 843 844 probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 845 Name: ProbeSuccessMetricName, 846 Help: "Displays whether or not the probe was a success", 847 }) 848 probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 849 Name: "probe_duration_seconds", 850 Help: "Returns how long the probe took to complete in seconds", 851 }) 852 smCheckInfo := prometheus.NewGauge(prometheus.GaugeOpts{ 853 Name: CheckInfoMetricName, 854 Help: "Provides information about a single check configuration", 855 ConstLabels: checkInfoLabels, 856 }) 857 858 registry.MustRegister(probeSuccessGauge) 859 registry.MustRegister(smCheckInfo) 860 registry.MustRegister(probeDurationGauge) 861 862 probeDurationGauge.Set(duration) 863 864 if success { 865 probeSuccessGauge.Set(1) 866 867 fields := append([]any{ 868 "msg", "Check succeeded", 869 "duration_seconds", duration, 870 "walltime_seconds", wallDuration, 871 }, clock.endLogFields(duration)...) 872 873 _ = level.Info(logger).Log(fields...) 874 } else { 875 probeSuccessGauge.Set(0) 876 877 fields := append([]any{ 878 "msg", "Check failed", 879 "duration_seconds", duration, 880 "walltime_seconds", wallDuration, 881 }, clock.endLogFields(duration)...) 882 883 _ = level.Error(logger).Log(fields...) 884 } 885 886 smCheckInfo.Set(1) 887 888 return success 889 } 890 891 func getDerivedMetrics(mfs []*dto.MetricFamily, summaries map[uint64]prometheus.Summary, histograms map[uint64]prometheus.Histogram, registry *prometheus.Registry, basicMetricsOnly bool) error { 892 for _, mf := range mfs { 893 switch { 894 case mf.GetType() == dto.MetricType_GAUGE && mf.GetName() == ProbeSuccessMetricName: 895 derivedMetricName := "probe_all_success" 896 897 for _, metric := range mf.GetMetric() { 898 _, err := updateSummaryFromMetric(derivedMetricName, mf.GetHelp(), metric, summaries, registry) 899 if err != nil { 900 return err 901 } 902 } 903 904 case mf.GetType() == dto.MetricType_GAUGE: 905 metricName := mf.GetName() 906 907 // we need to keep probe_all_duration_seconds 908 // because we use it to build a more reliable 909 // way of computing "uptime". 910 if metricName != "probe_duration_seconds" && basicMetricsOnly { 911 continue 912 } 913 914 suffixes := []string{"_duration_seconds", "_time_seconds"} 915 916 for _, suffix := range suffixes { 917 if strings.HasSuffix(metricName, suffix) { 918 derivedMetricName := strings.TrimSuffix(metricName, suffix) + "_all" + suffix 919 920 for _, metric := range mf.GetMetric() { 921 _, err := updateHistogramFromMetric(derivedMetricName, mf.GetHelp(), metric, histograms, registry) 922 if err != nil { 923 return err 924 } 925 } 926 927 break 928 } 929 } 930 } 931 } 932 933 return nil 934 } 935 936 // extractLogs parses logfmt-encoded log bytes and returns Loki streams using sharedLabels 937 // as stream labels. Any labels in structuredMetadata are attached to each log entry as 938 // Loki structured metadata. 939 func (s Scraper) extractLogs(t time.Time, logs []byte, sharedLabels []labelPair, structuredMetadata []logproto.LabelAdapter) Streams { 940 var line strings.Builder 941 942 dec := logfmt.NewDecoder(bytes.NewReader(logs)) 943 944 labels := make([]labelPair, 0, len(sharedLabels)) 945 946 var entries []logproto.Entry 947 948 RECORD: 949 for dec.ScanRecord() { 950 var t time.Time 951 952 line.Reset() 953 954 enc := logfmt.NewEncoder(&line) 955 956 labels = labels[:0] 957 labels = append(labels, sharedLabels...) 958 959 for dec.ScanKeyval() { 960 value := dec.Value() 961 962 switch key := dec.Key(); string(key) { 963 case "ts": 964 if !t.IsZero() { 965 continue // already set by the 'time' field 966 } 967 968 var err error 969 970 t, err = time.Parse(time.RFC3339Nano, string(value)) 971 if err != nil { 972 // We should never hit this as the timestamp string in the log should be valid. 973 // Without a timestamp we cannot do anything. And we cannot use something like 974 // time.Now() because that would mess up other entries 975 s.logger.Warn().Err(err).Bytes("value", value).Msg("invalid timestamp scanning logs") 976 continue RECORD 977 } 978 979 // k6 sets a 'time' field on its logs using RFC3339; if present, report it instead of the default timestamp. 980 // The slogHandler adapter (used for BBE probers) also emits a 'time' field using RFC3339Nano. 981 // RFC3339Nano is a strict superset of RFC3339; use it to parse either 'time' value.` 982 case "time": 983 var err error 984 985 t, err = time.Parse(time.RFC3339Nano, string(value)) 986 if err != nil { 987 s.logger.Warn().Err(err).Bytes("value", value).Msg("invalid timestamp scanning logs") 988 continue RECORD 989 } 990 991 default: 992 if err := enc.EncodeKeyval(key, value); err != nil { 993 // We should never hit this because all the entries are valid. 994 s.logger.Warn().Err(err).Bytes("key", key).Bytes("value", value).Msg("invalid entry scanning logs") 995 continue RECORD 996 } 997 } 998 } 999 1000 if err := enc.EndRecord(); err != nil { 1001 s.logger.Warn().Err(err).Msg("encoding logs") 1002 } 1003 1004 entries = append(entries, logproto.Entry{ 1005 Timestamp: t, 1006 Line: line.String(), 1007 StructuredMetadata: structuredMetadata, 1008 }) 1009 } 1010 1011 if err := dec.Err(); err != nil { 1012 s.logger.Error().Err(err).Msg("decoding logs") 1013 } 1014 1015 return Streams{ 1016 logproto.Stream{ 1017 Labels: fmtLabels(labels), 1018 Entries: entries, 1019 }, 1020 } 1021 } 1022 1023 func (s Scraper) extractTimeseries(t time.Time, metrics []*dto.MetricFamily, executionLabels []labelPair) TimeSeries { 1024 return extractTimeseries(t, metrics, executionLabels, s.summaries, s.histograms, s.logger) 1025 } 1026 1027 // extractTimeseries converts gathered Prometheus metrics into remote-write time series. 1028 // 1029 // executionLabels: system labels + un-prefixed user labels; applied to all check execution 1030 // metrics (probe_success, etc.). 1031 func extractTimeseries(t time.Time, metrics []*dto.MetricFamily, executionLabels []labelPair, summaries map[uint64]prometheus.Summary, histograms map[uint64]prometheus.Histogram, logger zerolog.Logger) TimeSeries { 1032 toPrompb := func(pairs []labelPair) []prompb.Label { 1033 out := make([]prompb.Label, 0, len(pairs)) 1034 for _, label := range pairs { 1035 out = append(out, prompb.Label{Name: label.name, Value: truncateLabelValue(label.value)}) 1036 } 1037 1038 return out 1039 } 1040 1041 executionPrompb := toPrompb(executionLabels) 1042 1043 var ts []prompb.TimeSeries 1044 1045 for _, mf := range metrics { 1046 mName := mf.GetName() 1047 mType := mf.GetType() 1048 1049 shared := executionPrompb 1050 if mName == CheckInfoMetricName { 1051 // sm_check_info is already constructed with the user labels 1052 // added to it under the correct LabeLMode paradigm. 1053 shared = nil 1054 } 1055 1056 for _, m := range mf.GetMetric() { 1057 ts = appendDtoToTimeseries(ts, t, mName, shared, mType, m) 1058 } 1059 } 1060 1061 return ts 1062 } 1063 1064 func (s Scraper) buildCheckInfoLabels(userLabels []labelPair, commonLabels []labelPair, mode sm.LabelMode) map[string]string { 1065 baseLabels := []labelPair{ 1066 {name: checkNameLabelName, value: s.checkName}, 1067 regionLabelForMode(mode, s.probe.Region), 1068 {name: "frequency", value: strconv.FormatInt(s.check.Frequency, 10)}, 1069 {name: "geohash", value: geohash.Encode(float64(s.probe.Latitude), float64(s.probe.Longitude))}, 1070 } 1071 baseLabels = append(baseLabels, commonLabels...) 1072 1073 if s.check.AlertSensitivity != "" && s.check.AlertSensitivity != "none" { 1074 baseLabels = append(baseLabels, labelPair{name: "alert_sensitivity", value: s.check.AlertSensitivity}) 1075 } 1076 1077 checkInfoLabels := appendUniqueLabels(userLabels, baseLabels) 1078 1079 labelMap := make(map[string]string, len(checkInfoLabels)) 1080 for _, l := range checkInfoLabels { 1081 labelMap[l.name] = l.value 1082 } 1083 1084 return labelMap 1085 } 1086 1087 // buildUserLabels builds the user-defined label pairs from probe and check labels 1088 // according to the tenant's LabelMode. 1089 // 1090 // In DUAL_WRITE and UNPREFIXED modes, un-prefixed user labels whose names are 1091 // reserved system labels (sm.IsSystemLabel) are dropped. This asserts the API's 1092 // guarantee that a migrated tenant carries no such label, and prevents a stray 1093 // reserved name from overwriting an intrinsic per-series metric label such as 1094 // `le` or `phase`. Prefixed forms (`label_*`) are never dropped, since no reserved 1095 // name begins with "label_", and PREFIXED mode is left untouched. 1096 // 1097 // In DUAL_WRITE mode, all prefixed forms are emitted first, followed by all 1098 // surviving un-prefixed forms: [label_foo, label_bar, foo, bar]. This ordering 1099 // ensures that in the event of log stream label overflow, the prefixed forms — 1100 // which existing policies depend on — are preserved as stream labels, while the 1101 // un-prefixed forms spill into StructuredMetadata. 1102 func buildUserLabels(probeLabels, checkLabels []sm.Label, mode sm.LabelMode) []labelPair { 1103 // Probe labels first, then any non-colliding check labels. On a name 1104 // collision the check value overwrites in place, so check values win while 1105 // probe ordering is preserved. 1106 userLabels := mergeUserLabels(probeLabels, checkLabels) 1107 1108 switch mode { 1109 case sm.LabelMode_LABEL_MODE_DUAL_WRITE: 1110 // All prefixed forms first, then all un-prefixed forms. 1111 labels := make([]labelPair, 0, 2*len(userLabels)) 1112 for _, e := range userLabels { 1113 labels = append(labels, labelPair{name: "label_" + e.name, value: e.value}) 1114 } 1115 1116 for _, e := range userLabels { 1117 // reserved labels should be dropped from the user's custom set 1118 if sm.IsSystemLabel(e.name) { 1119 continue 1120 } 1121 1122 labels = append(labels, labelPair{name: e.name, value: e.value}) 1123 } 1124 1125 return labels 1126 case sm.LabelMode_LABEL_MODE_UNPREFIXED: 1127 filtered := make([]labelPair, 0, len(userLabels)) 1128 for _, e := range userLabels { 1129 // reserved labels should be dropped from the user's custom set 1130 if sm.IsSystemLabel(e.name) { 1131 continue 1132 } 1133 1134 filtered = append(filtered, e) 1135 } 1136 1137 return filtered 1138 default: // LABEL_MODE_PREFIXED - prefix custom label names with label_ 1139 for i := range userLabels { 1140 userLabels[i].name = "label_" + userLabels[i].name 1141 } 1142 1143 return userLabels 1144 } 1145 } 1146 1147 // customMetricLabels returns the un-prefixed user-defined labels to be added to 1148 // check execution metrics (probe_success, probe_duration_seconds, probe_http_*, etc.) 1149 // in DUAL_WRITE and UNPREFIXED modes. 1150 // 1151 // In PREFIXED mode this returns nil — execution metrics carry no custom labels. 1152 func customMetricLabels(probeLabels, checkLabels []sm.Label, mode sm.LabelMode) []labelPair { 1153 if mode == sm.LabelMode_LABEL_MODE_PREFIXED { 1154 return nil 1155 } 1156 1157 return buildUserLabels(probeLabels, checkLabels, sm.LabelMode_LABEL_MODE_UNPREFIXED) 1158 } 1159 1160 // regionLabelForMode returns the label under which the agent emits the 1161 // probe's region. Tenants that have started the label migration (any mode 1162 // other than PREFIXED) receive it as sm_region, which is part of the reserved 1163 // set, freeing "region" for use as one of their own labels. Legacy PREFIXED 1164 // tenants keep the historical "region" name. 1165 // 1166 // sm_region always carries a non-empty value — "unset" when the probe has no 1167 // region — because its presence is how the API's alert expressions recognize 1168 // a migrated series: an empty label value is equivalent to an absent label in 1169 // PromQL, which would classify the series as legacy and route its alerts with 1170 // the wrong exclusion list. The historical region label keeps its value as-is 1171 // (absent when the probe has no region), preserving legacy series identity. 1172 func regionLabelForMode(mode sm.LabelMode, probeRegion string) labelPair { 1173 if mode == sm.LabelMode_LABEL_MODE_PREFIXED { 1174 return labelPair{name: regionLabelName, value: probeRegion} 1175 } 1176 1177 if probeRegion == "" { 1178 probeRegion = unsetRegionValue 1179 } 1180 1181 return labelPair{name: smRegionLabelName, value: probeRegion} 1182 } 1183 1184 func makeTimeseries(t time.Time, value float64, labels ...prompb.Label) prompb.TimeSeries { 1185 var ts prompb.TimeSeries 1186 1187 // duplicate labels on timeseries are rejected by Mimir. 1188 // this is also a common point to enforce conflict resolution 1189 // for LabelMode - user-defined labels should appear first in the list 1190 // of labels associated with each timeseries so that de-duplication favours retaining them 1191 // over system-defined values. 1192 ts.Labels = make([]prompb.Label, 0, len(labels)) 1193 1194 for _, l := range labels { 1195 dup := false 1196 1197 for i := range ts.Labels { 1198 if ts.Labels[i].Name == l.Name { 1199 dup = true 1200 break 1201 } 1202 } 1203 1204 if !dup { 1205 ts.Labels = append(ts.Labels, l) 1206 } 1207 } 1208 1209 ts.Samples = []prompb.Sample{ 1210 {Timestamp: t.UnixNano() / 1e6, Value: value}, 1211 } 1212 1213 return ts 1214 } 1215 1216 // mergeUserLabels combines probe- and check-level labels into a single ordered 1217 // slice. Probe labels come first (in order), followed by any check labels whose 1218 // names do not appear among the probe labels. When a check label's name collides 1219 // with a probe label, the check value overwrites the probe value in place, 1220 // preserving the label's original position. 1221 func mergeUserLabels(probeLabels, checkLabels []sm.Label) []labelPair { 1222 labels := make([]labelPair, 0, len(probeLabels)+len(checkLabels)) 1223 idx := make(map[string]int, len(probeLabels)) 1224 1225 for _, l := range probeLabels { 1226 idx[l.Name] = len(labels) 1227 labels = append(labels, labelPair{name: l.Name, value: l.Value}) 1228 } 1229 1230 for _, l := range checkLabels { 1231 if where, found := idx[l.Name]; found { 1232 labels[where].value = l.Value 1233 continue 1234 } 1235 1236 idx[l.Name] = len(labels) 1237 labels = append(labels, labelPair{name: l.Name, value: l.Value}) 1238 } 1239 1240 return labels 1241 } 1242 1243 // mergeLogLabels merges user-defined labels into the system log labels. On a 1244 // name collision the user value wins in place, preserving the ordering of the 1245 // system labels; user labels whose names do not collide are appended after them. 1246 // 1247 // Ordering matters: the system labels keep their leading positions so they are 1248 // retained as stream labels when the label set exceeds the stream cap and the 1249 // tail spills into structured metadata. User values still win on collision, 1250 // which protects a tenant whose existing label name later becomes reserved. 1251 func mergeLogLabels(systemLabels, userLabels []labelPair) []labelPair { 1252 merged := make([]labelPair, len(systemLabels), len(systemLabels)+len(userLabels)) 1253 copy(merged, systemLabels) 1254 1255 idx := make(map[string]int, len(systemLabels)) 1256 for i, l := range merged { 1257 idx[l.name] = i 1258 } 1259 1260 for _, l := range userLabels { 1261 if where, found := idx[l.name]; found { 1262 merged[where].value = l.value 1263 continue 1264 } 1265 1266 idx[l.name] = len(merged) 1267 merged = append(merged, l) 1268 } 1269 1270 return merged 1271 } 1272 1273 // appendUniqueLabels returns a slice of key-value labels which 1274 // are unique by key. Duplicates are discarded, meaning values 1275 // from prior slices are favoured over latter ones. 1276 func appendUniqueLabels(slice1, slice2 []labelPair) []labelPair { 1277 if slice1 == nil && slice2 == nil { 1278 return nil 1279 } 1280 1281 result := make([]labelPair, 0, len(slice1)+len(slice2)) 1282 seen := make(map[string]bool) 1283 1284 addUnique := func(slice []labelPair) { 1285 for _, val := range slice { 1286 if !seen[val.name] { 1287 seen[val.name] = true 1288 result = append(result, val) 1289 } 1290 } 1291 } 1292 1293 addUnique(slice1) 1294 addUnique(slice2) 1295 1296 return slices.Clip(result) 1297 } 1298 1299 func appendDtoToTimeseries(ts []prompb.TimeSeries, t time.Time, mName string, sharedLabels []prompb.Label, mType dto.MetricType, metric *dto.Metric) []prompb.TimeSeries { 1300 ml := metric.GetLabel() 1301 1302 labels := make([]prompb.Label, 0, 1+len(sharedLabels)+len(ml)) 1303 labels = append(labels, prompb.Label{Name: prom.MetricNameLabel, Value: mName}) 1304 1305 labels = append(labels, sharedLabels...) 1306 for _, l := range ml { 1307 labels = append(labels, prompb.Label{Name: *(l.Name), Value: truncateLabelValue(*(l.Value))}) 1308 } 1309 1310 switch mType { 1311 case dto.MetricType_COUNTER: 1312 if v := metric.GetCounter(); v != nil && v.Value != nil { 1313 ts = append(ts, makeTimeseries(t, *v.Value, labels...)) 1314 } 1315 1316 case dto.MetricType_GAUGE: 1317 if v := metric.GetGauge(); v != nil && v.Value != nil { 1318 ts = append(ts, makeTimeseries(t, *v.Value, labels...)) 1319 } 1320 1321 case dto.MetricType_UNTYPED: 1322 if v := metric.GetUntyped(); v != nil && v.Value != nil { 1323 ts = append(ts, makeTimeseries(t, *v.Value, labels...)) 1324 } 1325 1326 case dto.MetricType_SUMMARY: 1327 if s := metric.GetSummary(); s != nil { 1328 sLabels := make([]prompb.Label, len(labels)) 1329 copy(sLabels, labels) 1330 1331 sLabels[0] = prompb.Label{Name: prom.MetricNameLabel, Value: mName + "_sum"} 1332 ts = append(ts, makeTimeseries(t, s.GetSampleSum(), sLabels...)) 1333 1334 sLabels[0] = prompb.Label{Name: prom.MetricNameLabel, Value: mName + "_count"} 1335 ts = append(ts, makeTimeseries(t, float64(s.GetSampleCount()), sLabels...)) 1336 1337 sLabels = make([]prompb.Label, len(labels)+1) 1338 copy(sLabels, labels) 1339 1340 for _, v := range s.GetQuantile() { 1341 sLabels[len(sLabels)-1] = prompb.Label{ 1342 Name: "quantile", 1343 Value: strconv.FormatFloat(v.GetQuantile(), 'G', -1, 64), 1344 } 1345 ts = append(ts, makeTimeseries(t, v.GetValue(), sLabels...)) 1346 } 1347 } 1348 1349 case dto.MetricType_HISTOGRAM: 1350 if h := metric.GetHistogram(); h != nil { 1351 if b := h.GetBucket(); b != nil { 1352 hLabels := make([]prompb.Label, len(labels)) 1353 copy(hLabels, labels) 1354 1355 hLabels[0] = prompb.Label{Name: prom.MetricNameLabel, Value: mName + "_sum"} 1356 ts = append(ts, makeTimeseries(t, h.GetSampleSum(), hLabels...)) 1357 1358 hLabels[0] = prompb.Label{Name: prom.MetricNameLabel, Value: mName + "_count"} 1359 ts = append(ts, makeTimeseries(t, float64(h.GetSampleCount()), hLabels...)) 1360 1361 hLabels = make([]prompb.Label, len(labels)+1) 1362 copy(hLabels, labels) 1363 1364 hLabels[0] = prompb.Label{Name: prom.MetricNameLabel, Value: mName + "_bucket"} 1365 for _, v := range b { 1366 hLabels[len(hLabels)-1] = prompb.Label{ 1367 Name: "le", 1368 Value: strconv.FormatFloat(v.GetUpperBound(), 'G', -1, 64), 1369 } 1370 ts = append(ts, makeTimeseries(t, float64(v.GetCumulativeCount()), hLabels...)) 1371 } 1372 1373 // Add the +Inf bucket, which corresponds to the sample count. 1374 hLabels[len(hLabels)-1] = prompb.Label{ 1375 Name: "le", 1376 Value: "+Inf", 1377 } 1378 ts = append(ts, makeTimeseries(t, float64(h.GetSampleCount()), hLabels...)) 1379 } 1380 } 1381 } 1382 1383 return ts 1384 } 1385 1386 type labelPair struct { 1387 name string 1388 value string 1389 } 1390 1391 func fmtLabels(labels []labelPair) string { 1392 if len(labels) == 0 { 1393 return "" 1394 } 1395 1396 var s strings.Builder 1397 1398 // these calls do not produce errors, the errors are required to 1399 // satisfy interfaces 1400 _, _ = s.WriteRune('{') 1401 1402 for i, pair := range labels { 1403 if i > 0 { 1404 _, _ = s.WriteRune(',') 1405 } 1406 1407 _, _ = s.WriteString(pair.name) 1408 _, _ = s.WriteRune('=') 1409 _, _ = s.WriteRune('"') 1410 _, _ = s.WriteString(pair.value) 1411 _, _ = s.WriteRune('"') 1412 } 1413 1414 _, _ = s.WriteRune('}') 1415 1416 return s.String() 1417 } 1418 1419 func updateSummaryFromMetric(mName, help string, m *dto.Metric, summaries map[uint64]prometheus.Summary, registry *prometheus.Registry) (prometheus.Summary, error) { 1420 var value float64 1421 1422 switch { 1423 case m.GetCounter() != nil: 1424 value = m.GetCounter().GetValue() 1425 1426 case m.GetGauge() != nil: 1427 value = m.GetGauge().GetValue() 1428 1429 default: 1430 return nil, errUnsupportedMetric 1431 } 1432 1433 mHash := hashMetricNameAndLabels(mName, m.GetLabel()) 1434 1435 summary, found := summaries[mHash] 1436 if !found { 1437 summary = prometheus.NewSummary(prometheus.SummaryOpts{ 1438 Name: mName, 1439 Help: help + " (summary)", 1440 ConstLabels: getLabels(m), 1441 }) 1442 1443 summaries[mHash] = summary 1444 } 1445 1446 if err := registry.Register(summary); err != nil { 1447 return nil, err 1448 } 1449 1450 summary.Observe(value) 1451 1452 return summary, nil 1453 } 1454 1455 func updateHistogramFromMetric(mName, help string, m *dto.Metric, histograms map[uint64]prometheus.Histogram, registry *prometheus.Registry) (prometheus.Histogram, error) { 1456 var value float64 1457 1458 switch { 1459 case m.GetCounter() != nil: 1460 value = m.GetCounter().GetValue() 1461 1462 case m.GetGauge() != nil: 1463 value = m.GetGauge().GetValue() 1464 1465 default: 1466 return nil, errUnsupportedMetric 1467 } 1468 1469 mHash := hashMetricNameAndLabels(mName, m.GetLabel()) 1470 1471 histogram, found := histograms[mHash] 1472 if !found { 1473 histogram = prometheus.NewHistogram(prometheus.HistogramOpts{ 1474 Name: mName, 1475 Help: help + " (histogram)", 1476 ConstLabels: getLabels(m), 1477 Buckets: prometheus.DefBuckets, 1478 }) 1479 1480 histograms[mHash] = histogram 1481 } 1482 1483 if err := registry.Register(histogram); err != nil { 1484 return nil, err 1485 } 1486 1487 histogram.Observe(value) 1488 1489 return histogram, nil 1490 } 1491 1492 func hashMetricNameAndLabels(name string, dtoLabels []*dto.LabelPair) uint64 { 1493 ls := prom.LabelSet{ 1494 prom.MetricNameLabel: prom.LabelValue(name), 1495 } 1496 1497 for _, label := range dtoLabels { 1498 ls[prom.LabelName(label.GetName())] = prom.LabelValue(label.GetValue()) 1499 } 1500 1501 return uint64(ls.Fingerprint()) 1502 } 1503 1504 func getLabels(m *dto.Metric) map[string]string { 1505 if len(m.GetLabel()) == 0 { 1506 return nil 1507 } 1508 1509 labels := make(map[string]string) 1510 1511 for _, label := range m.GetLabel() { 1512 labels[label.GetName()] = label.GetValue() 1513 } 1514 1515 return labels 1516 } 1517 1518 func truncateLabelValue(str string) string { 1519 if len(str) > maxLabelValueLength { 1520 b := []byte(str[:maxLabelValueLength]) 1521 for i := maxLabelValueLength - 3; i < maxLabelValueLength; i++ { 1522 b[i] = '.' 1523 } 1524 1525 str = string(b) 1526 } 1527 1528 return str 1529 }