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 }