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  }