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

     1  package checks
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/signal"
    10  	"strconv"
    11  	"sync"
    12  	"sync/atomic"
    13  	"syscall"
    14  	"time"
    15  
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/prometheus/prompb"
    18  	"github.com/rs/zerolog"
    19  	"golang.org/x/sync/errgroup"
    20  	"google.golang.org/grpc"
    21  	"google.golang.org/grpc/codes"
    22  	"google.golang.org/grpc/status"
    23  
    24  	logproto "github.com/grafana/loki/pkg/push"
    25  
    26  	"github.com/grafana/synthetic-monitoring-agent/internal/cals"
    27  	"github.com/grafana/synthetic-monitoring-agent/internal/error_types"
    28  	"github.com/grafana/synthetic-monitoring-agent/internal/feature"
    29  	"github.com/grafana/synthetic-monitoring-agent/internal/k6runner"
    30  	"github.com/grafana/synthetic-monitoring-agent/internal/labelmode"
    31  	"github.com/grafana/synthetic-monitoring-agent/internal/limits"
    32  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    33  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    34  	"github.com/grafana/synthetic-monitoring-agent/internal/scraper"
    35  	"github.com/grafana/synthetic-monitoring-agent/internal/secrets"
    36  	"github.com/grafana/synthetic-monitoring-agent/internal/telemetry"
    37  	"github.com/grafana/synthetic-monitoring-agent/internal/usage"
    38  	"github.com/grafana/synthetic-monitoring-agent/internal/version"
    39  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    40  )
    41  
    42  type Error = error_types.BasicError
    43  
    44  type TransientError = error_types.TransientError
    45  
    46  type FatalError = error_types.FatalError
    47  
    48  const (
    49  	errCapabilityK6Missing = FatalError("k6 is required for scripted check support - configure k6 or edit probe capabilities in the SM app")
    50  	errIncompatibleApi     = FatalError("API does not support required features")
    51  	errNotAuthorized       = FatalError("probe not authorized")
    52  	errProbeUnregistered   = TransientError("probe no longer registered")
    53  	errTransportClosing    = TransientError("transport closing")
    54  )
    55  
    56  const metricNamespace = "sm_agent"
    57  
    58  // Backoffer defines an interface to provide backoff durations.
    59  //
    60  // The implementation of this interface SHOULD NOT perform the actual
    61  // sleep, but rather return the duration to sleep.
    62  type Backoffer interface {
    63  	// Reset causes the backoff provider to go back its initial
    64  	// state, before any calls to Duration() were made.
    65  	Reset()
    66  	// Duration returns the duration to sleep.
    67  	Duration() time.Duration
    68  }
    69  
    70  // Updater represents a probe along with the collection of scrapers
    71  // running on that probe and it manages the configuration for
    72  // blackbox-exporter that corresponds to the collection of scrapers.
    73  type Updater struct {
    74  	api                     apiInfo
    75  	logger                  zerolog.Logger
    76  	features                feature.Collection
    77  	backoff                 Backoffer
    78  	publisher               pusher.Publisher
    79  	tenantCh                chan<- sm.Tenant
    80  	probeCh                 chan<- *sm.Probe
    81  	probeTenantOnce         sync.Once
    82  	IsConnected             func(bool)
    83  	probe                   *sm.Probe
    84  	scrapersMutex           sync.Mutex
    85  	scrapers                map[model.GlobalID]*scraper.Scraper
    86  	metrics                 metrics
    87  	k6Runner                k6runner.Runner
    88  	scraperFactory          scraper.Factory
    89  	tenantLimits            *limits.TenantLimits
    90  	tenantSecrets           secrets.SecretProvider
    91  	telemeter               *telemetry.Telemeter
    92  	usageReporter           usage.Reporter
    93  	tenantCals              *cals.CostAttributionLabels
    94  	tenantLabellingMode     *labelmode.LabelMode
    95  	supportsProtocolSecrets bool
    96  }
    97  
    98  type apiInfo struct {
    99  	conn *grpc.ClientConn
   100  }
   101  
   102  type metrics struct {
   103  	changeErrorsCounter *prometheus.CounterVec
   104  	changesCounter      *prometheus.CounterVec
   105  	connectionStatus    prometheus.Gauge
   106  	probeInfo           *prometheus.GaugeVec
   107  	runningScrapers     *prometheus.GaugeVec
   108  	scrapeErrorCounter  *prometheus.CounterVec
   109  	scrapesCounter      *prometheus.CounterVec
   110  }
   111  
   112  type (
   113  	TimeSeries = []prompb.TimeSeries
   114  	Streams    = []logproto.Stream
   115  )
   116  
   117  type UpdaterOptions struct {
   118  	Conn                    *grpc.ClientConn
   119  	Logger                  zerolog.Logger
   120  	Backoff                 Backoffer
   121  	Publisher               pusher.Publisher
   122  	TenantCh                chan<- sm.Tenant
   123  	ProbeCh                 chan<- *sm.Probe
   124  	IsConnected             func(bool)
   125  	PromRegisterer          prometheus.Registerer
   126  	Features                feature.Collection
   127  	K6Runner                k6runner.Runner
   128  	ScraperFactory          scraper.Factory
   129  	TenantLimits            *limits.TenantLimits
   130  	SecretProvider          secrets.SecretProvider
   131  	Telemeter               *telemetry.Telemeter
   132  	UsageReporter           usage.Reporter
   133  	CostAttributionLabels   *cals.CostAttributionLabels
   134  	LabellingMode           *labelmode.LabelMode
   135  	SupportsProtocolSecrets bool
   136  }
   137  
   138  func NewUpdater(opts UpdaterOptions) (*Updater, error) {
   139  	changesCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
   140  		Namespace: metricNamespace,
   141  		Subsystem: "updater",
   142  		Name:      "changes_total",
   143  		Help:      "Total number of changes processed.",
   144  	}, []string{
   145  		"type",
   146  	})
   147  
   148  	if err := opts.PromRegisterer.Register(changesCounter); err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	changeErrorsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
   153  		Namespace: metricNamespace,
   154  		Subsystem: "updater",
   155  		Name:      "change_errors_total",
   156  		Help:      "Total number of errors during change processing.",
   157  	}, []string{
   158  		"type",
   159  	})
   160  
   161  	if err := opts.PromRegisterer.Register(changeErrorsCounter); err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	runningScrapers := prometheus.NewGaugeVec(prometheus.GaugeOpts{
   166  		Namespace: metricNamespace,
   167  		Subsystem: "updater",
   168  		Name:      "scrapers_total",
   169  		Help:      "Total number of running scrapers.",
   170  	}, []string{
   171  		"type",
   172  	})
   173  
   174  	if err := opts.PromRegisterer.Register(runningScrapers); err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	scrapesCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
   179  		Namespace: metricNamespace,
   180  		Subsystem: "scraper",
   181  		Name:      "operations_total",
   182  		Help:      "Total number of scrape operations performed by type.",
   183  	}, []string{
   184  		"type",
   185  		"tenantId",
   186  		"regionId",
   187  	})
   188  
   189  	if err := opts.PromRegisterer.Register(scrapesCounter); err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	scrapeErrorCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
   194  		Namespace: metricNamespace,
   195  		Subsystem: "scraper",
   196  		Name:      "errors_total",
   197  		Help:      "Total number of scraper errors by type and status.",
   198  	}, []string{
   199  		"type",
   200  		"source",
   201  		"tenantId",
   202  		"regionId",
   203  	})
   204  
   205  	if err := opts.PromRegisterer.Register(scrapeErrorCounter); err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	connectionStatusGauge := prometheus.NewGauge(prometheus.GaugeOpts{
   210  		Namespace: metricNamespace,
   211  		Subsystem: "api_connection",
   212  		Name:      "status",
   213  		Help:      "API connection status.",
   214  	})
   215  
   216  	if err := opts.PromRegisterer.Register(connectionStatusGauge); err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	connectionStatusGauge.Set(0)
   221  
   222  	probeInfoGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
   223  		Namespace: metricNamespace,
   224  		Name:      "info",
   225  		Help:      "Agent information.",
   226  	}, []string{
   227  		"id",
   228  		"name",
   229  		"version",
   230  		"commit",
   231  		"buildstamp",
   232  	})
   233  
   234  	if err := opts.PromRegisterer.Register(probeInfoGauge); err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	scraperFactory := scraper.New
   239  	if opts.ScraperFactory != nil {
   240  		scraperFactory = opts.ScraperFactory
   241  	}
   242  
   243  	return &Updater{
   244  		api: apiInfo{
   245  			conn: opts.Conn,
   246  		},
   247  		logger:                  opts.Logger,
   248  		features:                opts.Features,
   249  		backoff:                 opts.Backoff,
   250  		publisher:               opts.Publisher,
   251  		tenantCh:                opts.TenantCh,
   252  		probeCh:                 opts.ProbeCh,
   253  		IsConnected:             opts.IsConnected,
   254  		scrapers:                make(map[model.GlobalID]*scraper.Scraper),
   255  		k6Runner:                opts.K6Runner,
   256  		scraperFactory:          scraperFactory,
   257  		tenantLimits:            opts.TenantLimits,
   258  		tenantSecrets:           opts.SecretProvider,
   259  		telemeter:               opts.Telemeter,
   260  		supportsProtocolSecrets: opts.SupportsProtocolSecrets,
   261  		metrics: metrics{
   262  			changeErrorsCounter: changeErrorsCounter,
   263  			changesCounter:      changesCounter,
   264  			connectionStatus:    connectionStatusGauge,
   265  			probeInfo:           probeInfoGauge,
   266  			runningScrapers:     runningScrapers,
   267  			scrapeErrorCounter:  scrapeErrorCounter,
   268  			scrapesCounter:      scrapesCounter,
   269  		},
   270  		usageReporter:       opts.UsageReporter,
   271  		tenantCals:          opts.CostAttributionLabels,
   272  		tenantLabellingMode: opts.LabellingMode,
   273  	}, nil
   274  }
   275  
   276  func (c *Updater) Run(ctx context.Context) error {
   277  	c.backoff.Reset()
   278  
   279  	for {
   280  		wasConnected, err := c.loop(ctx)
   281  
   282  		logger := c.logger.With().Str("connection_state", c.api.conn.GetState().String()).Logger()
   283  
   284  		logger.Info().Err(err).Bool("was_connected", wasConnected).Msg("broke out of loop")
   285  
   286  		done, err := handleError(ctx, logger, c.backoff, wasConnected, err)
   287  
   288  		if done {
   289  			return err
   290  		}
   291  	}
   292  }
   293  
   294  // handleError takes care of handling errors that occur during the execution of
   295  // the updater loop. It returns a boolean indicating whether the updater should
   296  // stop and an error that should be returned to the caller of this function.
   297  // That error might be nil.
   298  //
   299  // TODO(mem): `wasConnected` is passed to preserve the previous behavior of the
   300  // updater. It might be possible to remove it and handle this in the caller.
   301  func handleError(ctx context.Context, logger zerolog.Logger, backoff Backoffer, wasConnected bool, err error) (bool, error) {
   302  	var (
   303  		transientErr TransientError
   304  		fatalError   FatalError
   305  	)
   306  
   307  	// Add logging to see what error we're dealing with
   308  	logger.Debug().Err(err).Str("error_type", fmt.Sprintf("%T", err)).Msg("handling error in handleError")
   309  
   310  	switch {
   311  	case err == nil:
   312  		return true, nil
   313  
   314  	case errors.As(err, &transientErr):
   315  		logger.Warn().Err(err).Msg("transient error, trying to reconnect")
   316  
   317  		if err := sleepCtx(ctx, backoff.Duration()); err != nil {
   318  			return true, err
   319  		}
   320  
   321  		if wasConnected {
   322  			backoff.Reset()
   323  		}
   324  
   325  		return false, nil
   326  
   327  	case errors.As(err, &fatalError):
   328  		logger.Error().Err(err).Msg("fatal error, bailing out")
   329  		return true, err
   330  
   331  	case errors.Is(err, context.Canceled):
   332  		// context was cancelled, clean up
   333  		logger.Error().
   334  			Err(err).
   335  			Msg("context cancelled, closing updater")
   336  
   337  		return true, nil
   338  
   339  	default:
   340  		logger.Warn().Err(err).Str("error_type", fmt.Sprintf("%T", err)).Msg("unhandled error type, treating as transient")
   341  
   342  		// TODO(mem): this might be a transient error (e.g. bad connection). We probably need to
   343  		// fine-tune GRPPC's backoff parameters. We might also need to keep count of the reconnects, and
   344  		// give up if they hit some threshold?
   345  		if err := sleepCtx(ctx, backoff.Duration()); err != nil {
   346  			return true, err
   347  		}
   348  	}
   349  
   350  	return false, nil
   351  }
   352  
   353  //goland:noinspection GoBoolExpressions
   354  func (c *Updater) loop(ctx context.Context) (bool, error) {
   355  	connected := false
   356  
   357  	c.logger.Info().Msg("fetching check configuration from synthetic-monitoring-api")
   358  
   359  	client := sm.NewChecksClient(c.api.conn)
   360  
   361  	grpcErrorHandler := func(action string, err error) error {
   362  		status, ok := status.FromError(err)
   363  		c.logger.Error().Err(err).Str("action", action).Uint32("code", uint32(status.Code())).Msg(status.Message())
   364  
   365  		switch {
   366  		case !ok:
   367  			return fmt.Errorf("%s: %w", action, err)
   368  
   369  		case status.Code() == codes.Canceled:
   370  			// either we were told to shut down
   371  			return context.Canceled
   372  
   373  		case status.Message() == "transport is closing":
   374  			// the other end is shutting down
   375  			return errTransportClosing
   376  
   377  		case status.Code() == codes.Unavailable:
   378  			// Network errors, connection resets, etc. are transient
   379  			return TransientError(fmt.Sprintf("%s: %s", action, status.Message()))
   380  
   381  		case status.Code() == codes.PermissionDenied:
   382  			return errNotAuthorized
   383  
   384  		case status.Code() == codes.Unimplemented:
   385  			return errIncompatibleApi
   386  
   387  		default:
   388  			return status.Err()
   389  		}
   390  	}
   391  
   392  	result, err := client.RegisterProbe(ctx, &sm.ProbeInfo{
   393  		Version:                 version.Short(),
   394  		Commit:                  version.Commit(),
   395  		Buildstamp:              version.Buildstamp(),
   396  		SupportsProtocolSecrets: c.supportsProtocolSecrets,
   397  	})
   398  	if err != nil {
   399  		return connected, grpcErrorHandler("registering probe with synthetic-monitoring-api", err)
   400  	}
   401  
   402  	switch result.Status.Code {
   403  	case sm.StatusCode_OK:
   404  		// continue
   405  
   406  	case sm.StatusCode_NOT_AUTHORIZED:
   407  		return connected, errNotAuthorized
   408  
   409  	default:
   410  		return connected, fmt.Errorf("registering probe with synthetic-monitoring-api, response: %s", result.Status.Message)
   411  	}
   412  
   413  	if err := c.validateProbeCapabilities(result.Probe.Capabilities); err != nil {
   414  		return connected, err
   415  	}
   416  
   417  	c.probe = &result.Probe
   418  
   419  	c.notifyProbeTenant()
   420  
   421  	logger := c.logger.With().Int64("probe_id", c.probe.Id).Logger()
   422  
   423  	logger.Info().Str("probe_name", c.probe.Name).Msg("registered probe with synthetic-monitoring-api")
   424  
   425  	c.metrics.connectionStatus.Set(1)
   426  	defer c.metrics.connectionStatus.Set(0)
   427  
   428  	// true indicates that probe is connected to API
   429  	connected = true
   430  
   431  	c.IsConnected(true)
   432  	defer c.IsConnected(false)
   433  
   434  	// this is constant throughout the life of the probe, but since
   435  	// we don't know the probe's id or name until this point, set it
   436  	// here.
   437  	c.metrics.probeInfo.Reset()
   438  	c.metrics.probeInfo.With(map[string]string{
   439  		"id":         strconv.FormatInt(c.probe.Id, 10),
   440  		"name":       c.probe.Name,
   441  		"version":    version.Short(),
   442  		"commit":     version.Commit(),
   443  		"buildstamp": version.Buildstamp(),
   444  	}).Set(1)
   445  
   446  	// groupCtx is used to coordinate shutting down all the
   447  	// goroutines started here.
   448  	g, groupCtx := errgroup.WithContext(ctx)
   449  
   450  	// We get _another_ context from the signal handler that we can
   451  	// use tell the GRPC client that we need to break out. We have
   452  	// multiple ways of cancelling the context (another signal
   453  	// elsewhere in the system communicated through the parent
   454  	// context; cancelling the child context because we are
   455  	// returning from this function; cancelling the new context
   456  	// because the signal fired), so we need an additional way of
   457  	// telling them apart.
   458  	sigCtx, signalFired := installSignalHandler(groupCtx)
   459  
   460  	errorHandler := func(err error, action string, signalFired *int32) error {
   461  		switch {
   462  		case err == nil:
   463  			return nil
   464  
   465  		case atomic.LoadInt32(signalFired) == 1:
   466  			return errProbeUnregistered
   467  
   468  		default:
   469  			return grpcErrorHandler(action, err)
   470  		}
   471  	}
   472  
   473  	knownChecks := sm.ProbeState{
   474  		Checks: make([]sm.EntityRef, 0, len(c.scrapers)),
   475  	}
   476  
   477  	for cID, scraper := range c.scrapers {
   478  		knownChecks.Checks = append(knownChecks.Checks, sm.EntityRef{
   479  			Id:           int64(cID),
   480  			LastModified: scraper.LastModified(),
   481  		})
   482  	}
   483  
   484  	cc, err := client.GetChanges(sigCtx, &knownChecks)
   485  	if err != nil {
   486  		return connected, errorHandler(err, "requesting changes from synthetic-monitoring-api", signalFired)
   487  	}
   488  
   489  	// Run a ping to the GRPC server. Bail out if there's an error here.
   490  	g.Go(func() error {
   491  		err := ping(sigCtx, client)
   492  		logger.Warn().Err(err).Msg("health check ping stopped")
   493  
   494  		return err
   495  	})
   496  
   497  	g.Go(func() error {
   498  		// processChanges uses the context in its first argument to
   499  		// create scrapers. This means that cancelling that context
   500  		// cancels all the running scrapers. That's why we are passing
   501  		// the _original_ context, not sigCtx, so that scrapers are
   502  		// _not_ stopped if the signal is trapped. We want scrapers to
   503  		// continue running in case the agent is _not_ killed.
   504  		err := c.processChanges(ctx, cc)
   505  		logger.Warn().Err(err).Msg("processing changes stopped")
   506  
   507  		return err
   508  	})
   509  
   510  	go func() {
   511  		// Do this in a goroutine to avoid blocking the rest of the process.
   512  		//
   513  		// This error is not fatal, so simply log it instead of returning it.
   514  		// That would cause the rest of the check-handling to stop.
   515  		//
   516  		// Add a 30 second timeout to guarantee that this goroutine will finish.
   517  		// The internal timeout in the usage reporter should take care of that.
   518  		ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
   519  		defer cancel()
   520  
   521  		err := c.usageReporter.ReportProbe(ctx, result.Probe, c.features)
   522  		if err != nil {
   523  			c.logger.Warn().Err(err).Msg("reporting usage failed")
   524  		}
   525  	}()
   526  
   527  	err = g.Wait()
   528  
   529  	return connected, errorHandler(err, "getting changes from synthetic-monitoring-api", signalFired)
   530  }
   531  
   532  func (c *Updater) validateProbeCapabilities(capabilities *sm.Probe_Capabilities) error {
   533  	// k6 is required by default unless it has been explicitly disabled from
   534  	// the API side by forbidding scripted and browser checks execution.
   535  	requireK6 := capabilities == nil ||
   536  		!capabilities.DisableScriptedChecks ||
   537  		!capabilities.DisableBrowserChecks
   538  
   539  	if requireK6 && c.k6Runner == nil {
   540  		return errCapabilityK6Missing
   541  	}
   542  
   543  	return nil
   544  }
   545  
   546  // notifyProbeTenant will notify the probes tenantID to the probeTenantCh at most once and then close the channel.
   547  // This is used by metamonitoring to infer which tenant the publisher should send data to.
   548  func (c *Updater) notifyProbeTenant() {
   549  	c.probeTenantOnce.Do(func() {
   550  		if c.probeCh != nil {
   551  			c.probeCh <- c.probe
   552  
   553  			close(c.probeCh)
   554  		}
   555  	})
   556  }
   557  
   558  // ping will use the provided client to send a health signal to the GRPC
   559  // server. Any error is returned, the caller should take the necessary
   560  // steps to correct the problem.
   561  func ping(ctx context.Context, client sm.ChecksClient) error {
   562  	var (
   563  		req  sm.PingRequest
   564  		opts = []grpc.CallOption{
   565  			grpc.WaitForReady(false),
   566  		}
   567  	)
   568  
   569  	// Send one ping to try to figure out if the API understands
   570  	// what we are trying to do.
   571  	_, err := client.Ping(ctx, &req, opts...)
   572  	if err != nil {
   573  		status, ok := status.FromError(err)
   574  
   575  		switch {
   576  		case !ok:
   577  			return fmt.Errorf("sending ping: %w", err)
   578  
   579  		case status.Code() == codes.Unimplemented:
   580  			// The API does not support this. Return without error.
   581  			return nil
   582  
   583  		default:
   584  			return status.Err()
   585  		}
   586  	}
   587  
   588  	req.Sequence++
   589  
   590  	ticker := time.NewTicker(sm.HealthCheckInterval)
   591  	defer ticker.Stop()
   592  
   593  	for {
   594  		select {
   595  		case <-ctx.Done():
   596  			return ctx.Err()
   597  
   598  		case <-ticker.C:
   599  			_, err := client.Ping(ctx, &req, opts...)
   600  			if err != nil {
   601  				return err
   602  			}
   603  
   604  			req.Sequence++
   605  		}
   606  	}
   607  }
   608  
   609  // installSignalHandler installs a signal handler for SIGUSR1.
   610  //
   611  // The returned context's Done channel is closed if the signal is
   612  // delivered. To make it simpler to determine if the signal was
   613  // delivered, a value of 1 is written to the location pointed to by the
   614  // returned int32 pointer.
   615  //
   616  // If the provided context's Done channel is closed before the signal is
   617  // delivered, the signal handler is removed and the returned context's
   618  // Done channel is closed, too. It's the callers responsibility to
   619  // cancel the provided context if it's no longer interested in the
   620  // signal.
   621  func installSignalHandler(ctx context.Context) (context.Context, *int32) {
   622  	sigCtx, cancel := context.WithCancel(ctx)
   623  
   624  	fired := new(int32)
   625  
   626  	sigCh := make(chan os.Signal, 1)
   627  	signal.Notify(sigCh, syscall.SIGUSR1)
   628  
   629  	go func() {
   630  		select {
   631  		case <-sigCh:
   632  			atomic.StoreInt32(fired, 1)
   633  			cancel()
   634  		case <-ctx.Done():
   635  		}
   636  
   637  		signal.Stop(sigCh)
   638  	}()
   639  
   640  	return sigCtx, fired
   641  }
   642  
   643  func (c *Updater) processChanges(ctx context.Context, cc sm.Checks_GetChangesClient) error {
   644  	firstBatch := true
   645  
   646  	for {
   647  		select {
   648  		case <-cc.Context().Done():
   649  			return cc.Context().Err()
   650  
   651  		case <-ctx.Done():
   652  			return ctx.Err()
   653  
   654  		default:
   655  			switch msg, err := cc.Recv(); err {
   656  			case nil:
   657  				c.handleChangeBatch(ctx, msg, firstBatch)
   658  				firstBatch = false
   659  
   660  			case io.EOF:
   661  				c.logger.Warn().Err(err).Msg("no more messages?")
   662  				// XXX(mem): what happened here? The
   663  				// other end told us there are no more
   664  				// changes. Stop? Is it restarting?
   665  				return nil
   666  
   667  			default:
   668  				return err
   669  			}
   670  		}
   671  	}
   672  }
   673  
   674  func (c *Updater) handleCheckAdd(ctx context.Context, check model.Check) error {
   675  	c.metrics.changesCounter.WithLabelValues("add").Inc()
   676  
   677  	if err := check.Validate(); err != nil {
   678  		return fmt.Errorf("invalid check: %w", err)
   679  	}
   680  
   681  	c.scrapersMutex.Lock()
   682  	defer c.scrapersMutex.Unlock()
   683  
   684  	if running, found := c.scrapers[check.GlobalID()]; found {
   685  		// we can get here if the API sent us a check add twice:
   686  		// once during the initial connection and another right
   687  		// after that. The window for that is small, but it
   688  		// exists.
   689  		return fmt.Errorf("check with id %d already exists (version %s)", check.GlobalID(), running.ConfigVersion())
   690  	}
   691  
   692  	return c.addAndStartScraperWithLock(ctx, check)
   693  }
   694  
   695  func (c *Updater) handleCheckUpdate(ctx context.Context, check model.Check) error {
   696  	c.metrics.changesCounter.WithLabelValues("update").Inc()
   697  
   698  	if err := check.Validate(); err != nil {
   699  		return fmt.Errorf("invalid check: %w", err)
   700  	}
   701  
   702  	c.scrapersMutex.Lock()
   703  	defer c.scrapersMutex.Unlock()
   704  
   705  	return c.handleCheckUpdateWithLock(ctx, check)
   706  }
   707  
   708  // handleCheckUpdateWithLock is the bottom half of handleCheckUpdate. It
   709  // MUST be called with the scrapersMutex lock held.
   710  func (c *Updater) handleCheckUpdateWithLock(ctx context.Context, check model.Check) error {
   711  	cid := check.GlobalID()
   712  
   713  	scraper, found := c.scrapers[cid]
   714  	if !found {
   715  		c.logger.Warn().Int64("check_id", check.Id).Int("region_id", check.RegionId).Msg("update request for an unknown check")
   716  		return c.addAndStartScraperWithLock(ctx, check)
   717  	}
   718  
   719  	// this is the lazy way to update the scraper: tear everything
   720  	// down, start it again.
   721  
   722  	scraper.Stop()
   723  	checkType := scraper.CheckType().String()
   724  
   725  	delete(c.scrapers, cid)
   726  
   727  	c.metrics.runningScrapers.WithLabelValues(checkType).Dec()
   728  
   729  	return c.addAndStartScraperWithLock(ctx, check)
   730  }
   731  
   732  func (c *Updater) handleCheckDelete(ctx context.Context, check model.Check) error {
   733  	c.metrics.changesCounter.WithLabelValues("delete").Inc()
   734  
   735  	cid := check.GlobalID()
   736  
   737  	c.scrapersMutex.Lock()
   738  	defer c.scrapersMutex.Unlock()
   739  
   740  	scraper, found := c.scrapers[cid]
   741  	if !found {
   742  		c.logger.Warn().Int64("check_id", check.Id).Int("region_id", check.RegionId).Msg("delete request for an unknown check")
   743  		return errors.New("check not found")
   744  	}
   745  
   746  	scraper.Stop()
   747  	checkType := scraper.CheckType().String()
   748  
   749  	delete(c.scrapers, cid)
   750  
   751  	c.metrics.runningScrapers.WithLabelValues(checkType).Dec()
   752  
   753  	return nil
   754  }
   755  
   756  // handleFirstBatch takes a list of changes and adds them to the running set
   757  // and stops any scrapers that shouldn't be running.
   758  //
   759  // When handling this, we don't know which scrapers the server thinks we are
   760  // no longer running and which scrapers we are running. If we got
   761  // disconnected, the server will send a bunch of ADD operations, and no DELETE
   762  // or UPDATE ones. After we reconnect, we still have a bunch of running
   763  // scrapers that the server might think we are NOT running, so build a list of
   764  // checks the server sent our way and compare it with the list of checks we
   765  // actually have (from the running scrapers). Remove anything that the server
   766  // didn't send, because that means it didn't know we have those (they got
   767  // deleted during the reconnect, and the server didn't send them).
   768  //
   769  // We have to do this exactly once per reconnect. It's up to the calling code
   770  // to ensure this.
   771  func (c *Updater) handleFirstBatch(ctx context.Context, changes *sm.Changes) {
   772  	newChecks := make(map[model.GlobalID]struct{})
   773  
   774  	c.scrapersMutex.Lock()
   775  	defer c.scrapersMutex.Unlock()
   776  
   777  	// add checks from the provided list
   778  	for _, checkChange := range changes.Checks {
   779  		c.logger.Debug().Interface("check change", checkChange).Msg("got check change")
   780  
   781  		switch checkChange.Operation {
   782  		case sm.CheckOperation_CHECK_ADD:
   783  			var check model.Check
   784  			if err := check.FromSM(checkChange.Check); err != nil {
   785  				c.logger.Error().Err(err).Interface("check_change", checkChange).Msg("dropping check during add operation")
   786  				continue
   787  			}
   788  
   789  			if err := c.handleInitialChangeAddWithLock(ctx, check); err != nil {
   790  				c.metrics.changeErrorsCounter.WithLabelValues("add").Inc()
   791  				c.logger.Error().Err(err).Int64("check_id", check.Id).Int("region_id", check.RegionId).
   792  					Msg("adding check failed, dropping check")
   793  
   794  				continue
   795  			}
   796  
   797  			// add this to the list of checks we have seen during
   798  			// this operation
   799  			newChecks[check.GlobalID()] = struct{}{}
   800  
   801  		default:
   802  			// we should never hit this because the first time we
   803  			// connect the server will only send adds.
   804  			c.logger.Warn().
   805  				Interface("check", checkChange.Check).
   806  				Str("operation", checkChange.Operation.String()).
   807  				Msg("unexpected operation, dropping check change")
   808  
   809  			continue
   810  		}
   811  	}
   812  
   813  	// remove all the running scrapers that weren't sent with the first batch
   814  	for id, scraper := range c.scrapers {
   815  		if _, found := newChecks[id]; found {
   816  			continue
   817  		}
   818  
   819  		cid, rid := model.GetLocalAndRegionIDs(id)
   820  		c.logger.Debug().
   821  			Int64("check_id", cid).
   822  			Int("region_id", rid).
   823  			Msg("stopping scraper during first batch handling")
   824  
   825  		checkType := scraper.CheckType().String()
   826  		scraper.Stop()
   827  
   828  		delete(c.scrapers, id)
   829  
   830  		c.metrics.runningScrapers.WithLabelValues(checkType).Dec()
   831  	}
   832  }
   833  
   834  // handleCheckUpdateWithLock the specified check to the running checks.
   835  //
   836  // It deals with the case where this check is the product of a reconnection
   837  // and changes the operation to an update if necessary.
   838  //
   839  // This function MUST be called with the scrapers mutex held.
   840  func (c *Updater) handleInitialChangeAddWithLock(ctx context.Context, check model.Check) error {
   841  	if running, found := c.scrapers[check.GlobalID()]; found {
   842  		oldVersion := running.ConfigVersion()
   843  		newVersion := check.ConfigVersion()
   844  
   845  		if oldVersion == newVersion {
   846  			// we already have this, skip
   847  			//
   848  			// XXX(mem): beware, the probe might have changed
   849  			return nil
   850  		}
   851  
   852  		// transform this request into an update
   853  		c.logger.Debug().Str("old_check_version", oldVersion).Str("new_check_version", newVersion).Msg("transforming add into update")
   854  
   855  		return c.handleCheckUpdateWithLock(ctx, check)
   856  	}
   857  
   858  	c.metrics.changesCounter.WithLabelValues("add").Inc()
   859  
   860  	if err := check.Validate(); err != nil {
   861  		return err
   862  	}
   863  
   864  	if err := c.addAndStartScraperWithLock(ctx, check); err != nil {
   865  		c.metrics.changeErrorsCounter.WithLabelValues("add").Inc()
   866  		return err
   867  	}
   868  
   869  	return nil
   870  }
   871  
   872  func (c *Updater) handleChangeBatch(ctx context.Context, changes *sm.Changes, firstBatch bool) {
   873  	if firstBatch && !changes.IsDeltaFirstBatch {
   874  		c.handleFirstBatch(ctx, changes)
   875  		return
   876  	}
   877  
   878  	for _, tenant := range changes.Tenants {
   879  		c.tenantCh <- tenant
   880  	}
   881  
   882  	for _, checkChange := range changes.Checks {
   883  		c.logger.Debug().Interface("check change", checkChange).Msg("got check change")
   884  
   885  		var check model.Check
   886  		if err := check.FromSM(checkChange.Check); err != nil {
   887  			c.logger.Error().Err(err).Interface("check_change", checkChange).Msg("dropping check change")
   888  			continue
   889  		}
   890  
   891  		switch checkChange.Operation {
   892  		case sm.CheckOperation_CHECK_ADD:
   893  			if err := c.handleCheckAdd(ctx, check); err != nil {
   894  				c.metrics.changeErrorsCounter.WithLabelValues("add").Inc()
   895  				c.logger.Error().Err(err).Msg("handling check add")
   896  			}
   897  
   898  		case sm.CheckOperation_CHECK_UPDATE:
   899  			if err := c.handleCheckUpdate(ctx, check); err != nil {
   900  				c.metrics.changeErrorsCounter.WithLabelValues("update").Inc()
   901  				c.logger.Error().Err(err).Msg("handling check update")
   902  			}
   903  
   904  		case sm.CheckOperation_CHECK_DELETE:
   905  			if err := c.handleCheckDelete(ctx, check); err != nil {
   906  				c.metrics.changeErrorsCounter.WithLabelValues("delete").Inc()
   907  				c.logger.Error().Err(err).Msg("handling check delete")
   908  			}
   909  		}
   910  	}
   911  }
   912  
   913  // addAndStartScraperWithLock creates a new scraper, adds it to the list of
   914  // scrapers managed by this updater and starts running it.
   915  //
   916  // This MUST be called with the scrapersMutex held.
   917  func (c *Updater) addAndStartScraperWithLock(ctx context.Context, check model.Check) error {
   918  	// This is a good place to filter out checks by feature flags.
   919  	//
   920  	// If we need to accept checks based on whether a feature flag
   921  	// is enabled or not, we can "accept" the check from the point
   922  	// of view of the API, and skip running it here, e.g.
   923  	switch check.Type() {
   924  	case sm.CheckTypeScripted:
   925  		if !c.features.IsSet(feature.K6) {
   926  			return nil
   927  		}
   928  
   929  	case sm.CheckTypeMultiHttp:
   930  		// This is correct, MultiHttp is a K6 check. We probably want
   931  		// to abstrct this by adding a function to the settings that
   932  		// returns whether the check requires k6 or not.
   933  		if !c.features.IsSet(feature.K6) {
   934  			return nil
   935  		}
   936  
   937  	default:
   938  	}
   939  
   940  	checkType := check.Type().String()
   941  
   942  	tidStr := strconv.FormatInt(check.TenantId, 10)
   943  	ridStr := strconv.Itoa(check.RegionId)
   944  
   945  	scrapeCounter, err := c.metrics.scrapesCounter.GetMetricWith(prometheus.Labels{
   946  		"type":     checkType,
   947  		"tenantId": tidStr,
   948  		"regionId": ridStr,
   949  	})
   950  	if err != nil {
   951  		return err
   952  	}
   953  
   954  	scrapeErrorCounter, err := c.metrics.scrapeErrorCounter.CurryWith(prometheus.Labels{
   955  		"type":     checkType,
   956  		"tenantId": tidStr,
   957  		"regionId": ridStr,
   958  	})
   959  	if err != nil {
   960  		return err
   961  	}
   962  
   963  	metrics := scraper.NewMetrics(
   964  		scrapeCounter,
   965  		scraper.NewIncrementerFromCounterVec(scrapeErrorCounter),
   966  	)
   967  
   968  	scraper, err := c.scraperFactory(
   969  		ctx, check, c.publisher, *c.probe,
   970  		c.features,
   971  		c.logger,
   972  		metrics,
   973  		c.k6Runner,
   974  		c.tenantLimits, c.telemeter, c.tenantSecrets, c.tenantCals, c.tenantLabellingMode,
   975  	)
   976  	if err != nil {
   977  		return fmt.Errorf("cannot create new scraper: %w", err)
   978  	}
   979  
   980  	c.scrapers[check.GlobalID()] = scraper
   981  
   982  	go scraper.Run(ctx)
   983  
   984  	c.metrics.runningScrapers.WithLabelValues(checkType).Inc()
   985  
   986  	return nil
   987  }
   988  
   989  // sleepCtx is like time.Sleep, but it pays attention to the
   990  // cancellation of the provided context.
   991  func sleepCtx(ctx context.Context, d time.Duration) error {
   992  	var err error
   993  
   994  	timer := time.NewTimer(d)
   995  
   996  	select {
   997  	case <-ctx.Done():
   998  		err = ctx.Err()
   999  
  1000  		if !timer.Stop() {
  1001  			<-timer.C
  1002  		}
  1003  
  1004  	case <-timer.C:
  1005  	}
  1006  
  1007  	return err
  1008  }