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

     1  package adhoc
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"time"
    10  
    11  	"github.com/google/uuid"
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	prompb "github.com/prometheus/prometheus/prompb"
    14  	"github.com/rs/zerolog"
    15  	"google.golang.org/grpc"
    16  	"google.golang.org/grpc/codes"
    17  	"google.golang.org/grpc/connectivity"
    18  	"google.golang.org/grpc/status"
    19  
    20  	logproto "github.com/grafana/loki/pkg/push"
    21  	"github.com/grafana/synthetic-monitoring-agent/internal/feature"
    22  	"github.com/grafana/synthetic-monitoring-agent/internal/k6runner"
    23  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    24  	"github.com/grafana/synthetic-monitoring-agent/internal/prober"
    25  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    26  	"github.com/grafana/synthetic-monitoring-agent/internal/secrets"
    27  	"github.com/grafana/synthetic-monitoring-agent/internal/version"
    28  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    29  )
    30  
    31  // Handler is in charge of retrieving ad-hoc checks from the
    32  // synthetic-monitoring-api, running them and forwarding the results to
    33  // the publisher.
    34  type Handler struct {
    35  	api                          apiInfo
    36  	logger                       zerolog.Logger
    37  	features                     feature.Collection
    38  	backoff                      Backoffer
    39  	probe                        *sm.Probe
    40  	metrics                      metrics
    41  	publisher                    pusher.Publisher
    42  	tenantCh                     chan<- sm.Tenant
    43  	runnerFactory                func(context.Context, *sm.AdHocRequest) (*runner, error)
    44  	grpcAdhocChecksClientFactory func(conn ClientConn) (sm.AdHocChecksClient, error)
    45  	proberFactory                prober.ProberFactory
    46  	supportsProtocolSecrets      bool
    47  }
    48  
    49  // Error represents errors returned from this package.
    50  type Error string
    51  
    52  func (e Error) Error() string { return string(e) }
    53  
    54  const (
    55  	errNotAuthorized       = Error("probe not authorized")
    56  	errTransportClosing    = Error("transport closing")
    57  	errProbeUnregistered   = Error("probe no longer registered")
    58  	errIncompatibleApi     = Error("API does not support required features")
    59  	errInvalidAdHocRequest = Error("invalid ad-hoc request")
    60  	errIdleTimeout         = Error("connection idle timeout")
    61  
    62  	k6AdhocGraceTime = 20 * time.Second
    63  )
    64  
    65  // Removed HTTP status code parsing; we treat all codes.Unavailable as idle timeout.
    66  
    67  type runner struct {
    68  	logger  zerolog.Logger
    69  	prober  prober.Prober
    70  	id      string
    71  	target  string
    72  	probe   string
    73  	timeout time.Duration
    74  }
    75  
    76  // ClientConn represents the GRPC client connection that can be used to
    77  // make RPC calls to the Synthetic Monitoring API.
    78  type ClientConn interface {
    79  	grpc.ClientConnInterface
    80  	GetState() connectivity.State
    81  }
    82  
    83  type apiInfo struct {
    84  	conn ClientConn
    85  }
    86  
    87  type metrics struct {
    88  	opsCounter *prometheus.CounterVec
    89  }
    90  
    91  // Backoffer defines an interface to provide backoff durations.
    92  //
    93  // The implementation of this interface SHOULD NOT perform the actual
    94  // sleep, but rather return the duration to sleep.
    95  type Backoffer interface {
    96  	// Reset causes the backoff provider to go back its initial
    97  	// state, before any calls to Duration() were made.
    98  	Reset()
    99  	// Duration returns the duration to sleep.
   100  	Duration() time.Duration
   101  }
   102  
   103  type constantBackoff time.Duration
   104  
   105  func (constantBackoff) Reset() {}
   106  
   107  func (b constantBackoff) Duration() time.Duration { return time.Duration(b) }
   108  
   109  // HandlerOpts is used to pass configuration options to the Handler.
   110  type HandlerOpts struct {
   111  	Conn                    ClientConn
   112  	Logger                  zerolog.Logger
   113  	Backoff                 Backoffer
   114  	Publisher               pusher.Publisher
   115  	TenantCh                chan<- sm.Tenant
   116  	PromRegisterer          prometheus.Registerer
   117  	Features                feature.Collection
   118  	K6Runner                k6runner.Runner
   119  	SecretProvider          secrets.SecretProvider
   120  	SupportsProtocolSecrets bool
   121  
   122  	// these two fields exists so that tests can pass alternate
   123  	// implementations, they are unexported so that clients of this
   124  	// package always use the default ones.
   125  	runnerFactory                func(context.Context, *sm.AdHocRequest) (*runner, error)
   126  	grpcAdhocChecksClientFactory func(conn ClientConn) (sm.AdHocChecksClient, error)
   127  }
   128  
   129  // NewHandler creates a new Handler using the specified options.
   130  func NewHandler(opts HandlerOpts) (*Handler, error) {
   131  	opsCounter := prometheus.NewCounterVec(
   132  		prometheus.CounterOpts{
   133  			Namespace: "sm",
   134  			Subsystem: "adhoc",
   135  			Name:      "ops_total",
   136  			Help:      "Total number of adhoc operations",
   137  		},
   138  		[]string{"type"},
   139  	)
   140  
   141  	if err := opts.PromRegisterer.Register(opsCounter); err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	h := &Handler{
   146  		logger:                       opts.Logger,
   147  		features:                     opts.Features,
   148  		backoff:                      opts.Backoff,
   149  		publisher:                    opts.Publisher,
   150  		tenantCh:                     opts.TenantCh,
   151  		runnerFactory:                opts.runnerFactory,
   152  		grpcAdhocChecksClientFactory: opts.grpcAdhocChecksClientFactory,
   153  		proberFactory:                prober.NewProberFactory(opts.K6Runner, 0, opts.Features, opts.SecretProvider),
   154  		supportsProtocolSecrets:      opts.SupportsProtocolSecrets,
   155  		api: apiInfo{
   156  			conn: opts.Conn,
   157  		},
   158  		metrics: metrics{
   159  			opsCounter: opsCounter,
   160  		},
   161  	}
   162  
   163  	if opts.runnerFactory == nil {
   164  		h.runnerFactory = h.defaultRunnerFactory
   165  	}
   166  
   167  	if opts.grpcAdhocChecksClientFactory == nil {
   168  		h.grpcAdhocChecksClientFactory = defaultGrpcAdhocChecksClientFactory
   169  	}
   170  
   171  	if opts.Backoff == nil {
   172  		h.backoff = constantBackoff(60 * time.Second)
   173  	}
   174  
   175  	return h, nil
   176  }
   177  
   178  // Run starts the handler.
   179  func (h *Handler) Run(ctx context.Context) error {
   180  	for {
   181  		err := h.loop(ctx)
   182  		switch {
   183  		case err == nil:
   184  			return nil
   185  
   186  		case errors.Is(err, errNotAuthorized):
   187  			// our token is invalid, bail out?
   188  			h.logger.Error().
   189  				Err(err).
   190  				Str("connection_state", h.api.conn.GetState().String()).
   191  				Msg("cannot connect, bailing out")
   192  
   193  			return err
   194  
   195  		case errors.Is(err, errIncompatibleApi):
   196  			// API server doesn't support required features.
   197  			h.logger.Error().
   198  				Err(err).
   199  				Str("connection_state", h.api.conn.GetState().String()).
   200  				Msg("cannot connect, bailing out")
   201  
   202  			return err
   203  
   204  		case errors.Is(err, errTransportClosing):
   205  			// the other end went away? Allow GRPC to reconnect.
   206  			h.logger.Warn().
   207  				Err(err).
   208  				Str("connection_state", h.api.conn.GetState().String()).
   209  				Msg("the other end closed the connection, trying to reconnect")
   210  
   211  			// After disconnecting, reset the backoff
   212  			// counter to start afresh.
   213  			h.backoff.Reset()
   214  
   215  			continue
   216  
   217  		case errors.Is(err, errIdleTimeout):
   218  			// connection idle timeout - this is normal behavior when adhoc checks aren't used frequently
   219  			h.logger.Info().
   220  				Str("connection_state", h.api.conn.GetState().String()).
   221  				Msg("connection idle timeout, reconnecting")
   222  
   223  			// After idle timeout, reset the backoff counter to start afresh.
   224  			h.backoff.Reset()
   225  
   226  			// Sleep to allow context cancellation to be checked
   227  			if err := sleepCtx(ctx, h.backoff.Duration()); err != nil {
   228  				return err
   229  			}
   230  
   231  			continue
   232  
   233  		case errors.Is(err, context.Canceled):
   234  			// context was cancelled, clean up
   235  			h.logger.Error().
   236  				Err(err).
   237  				Str("connection_state", h.api.conn.GetState().String()).
   238  				Msg("context cancelled, closing updater")
   239  
   240  			return nil
   241  
   242  		case errors.Is(err, errProbeUnregistered):
   243  			// Probe unregistered itself from the API, wait
   244  			// until attempting to reconnect again.
   245  			h.logger.Warn().
   246  				Str("connection_state", h.api.conn.GetState().String()).
   247  				Msg("unregistered probe in API, sleeping for 1 minute...")
   248  
   249  			if err := sleepCtx(ctx, 1*time.Minute); err != nil {
   250  				return err
   251  			}
   252  
   253  			// The probe is going to reconnect, reset the
   254  			// backoff counter to start afresh.
   255  			h.backoff.Reset()
   256  
   257  		default:
   258  			h.logger.Warn().
   259  				Err(err).
   260  				Str("connection_state", h.api.conn.GetState().String()).
   261  				Msg("handling check changes")
   262  
   263  			// TODO(mem): this might be a transient error (e.g. bad connection). We probably need to
   264  			// fine-tune GRPPC's backoff parameters. We might also need to keep count of the reconnects, and
   265  			// give up if they hit some threshold?
   266  			if err := sleepCtx(ctx, h.backoff.Duration()); err != nil {
   267  				return err
   268  			}
   269  		}
   270  	}
   271  }
   272  
   273  func (h *Handler) loop(ctx context.Context) error {
   274  	h.logger.Info().Msg("fetching ad-hoc checks from synthetic-monitoring-api")
   275  
   276  	client, err := h.grpcAdhocChecksClientFactory(h.api.conn)
   277  	if err != nil {
   278  		return fmt.Errorf("cannot create adhoc checks client: %w", err)
   279  	}
   280  
   281  	grpcErrorHandler := func(action string, err error) error {
   282  		status, ok := status.FromError(err)
   283  
   284  		switch {
   285  		case !ok:
   286  			h.logger.Error().Err(err).Bool("ok", ok).Str("action", action).Msg("unknown error")
   287  			return fmt.Errorf("%s: %w", action, err)
   288  
   289  		case status.Code() == codes.Canceled:
   290  			// either we were told to shut down
   291  			return context.Canceled
   292  
   293  		case status.Message() == "transport is closing":
   294  			// the other end is shutting down
   295  			return errTransportClosing
   296  
   297  		case status.Code() == codes.Unavailable:
   298  			// Treat all Unavailable as idle timeout; allow reconnect loop.
   299  			return errIdleTimeout
   300  
   301  		case status.Code() == codes.PermissionDenied:
   302  			h.logger.Error().Err(err).Bool("ok", ok).Str("action", action).Uint32("code", uint32(status.Code())).Msg(status.Message())
   303  			return errNotAuthorized
   304  
   305  		case status.Code() == codes.Unimplemented:
   306  			h.logger.Error().Err(err).Bool("ok", ok).Str("action", action).Uint32("code", uint32(status.Code())).Msg(status.Message())
   307  			return errIncompatibleApi
   308  
   309  		default:
   310  			h.logger.Error().Err(err).Bool("ok", ok).Str("action", action).Uint32("code", uint32(status.Code())).Msg(status.Message())
   311  			return status.Err()
   312  		}
   313  	}
   314  
   315  	result, err := client.RegisterProbe(
   316  		ctx,
   317  		&sm.ProbeInfo{
   318  			Version:                 version.Short(),
   319  			Commit:                  version.Commit(),
   320  			Buildstamp:              version.Buildstamp(),
   321  			SupportsProtocolSecrets: h.supportsProtocolSecrets,
   322  		},
   323  	)
   324  	if err != nil {
   325  		return grpcErrorHandler(
   326  			"registering ad-hoc probe with synthetic-monitoring-api",
   327  			err)
   328  	}
   329  
   330  	switch result.Status.Code {
   331  	case sm.StatusCode_OK:
   332  		// continue
   333  
   334  	case sm.StatusCode_NOT_AUTHORIZED:
   335  		return errNotAuthorized
   336  
   337  	default:
   338  		return fmt.Errorf(
   339  			"registering ad-hoc probe with synthetic-monitoring-api, response: %s",
   340  			result.Status.Message)
   341  	}
   342  
   343  	h.probe = &result.Probe
   344  
   345  	h.logger.Info().
   346  		Int64("probe id", h.probe.Id).
   347  		Str("probe name", h.probe.Name).
   348  		Msg("registered ad-hoc probe with synthetic-monitoring-api")
   349  
   350  	requests, err := client.GetAdHocChecks(ctx, &sm.Void{})
   351  	if err != nil {
   352  		return grpcErrorHandler("requesting ad-hoc checks from synthetic-monitoring-api", err)
   353  	}
   354  
   355  	return h.processAdHocChecks(ctx, requests)
   356  }
   357  
   358  func (h *Handler) processAdHocChecks(ctx context.Context, client sm.AdHocChecks_GetAdHocChecksClient) error {
   359  	for {
   360  		select {
   361  		case <-client.Context().Done():
   362  			return nil
   363  
   364  		case <-ctx.Done():
   365  			return ctx.Err()
   366  
   367  		default:
   368  			switch msg, err := client.Recv(); err {
   369  			case nil:
   370  				if err := h.handleAdHocCheck(ctx, msg); err != nil {
   371  					h.logger.Error().Err(err).Interface("request", msg).Msg("handling ad-hoc check")
   372  					continue
   373  				}
   374  
   375  			case io.EOF:
   376  				h.logger.Warn().Err(err).Msg("no more messages?")
   377  				// XXX(mem): what happened here? The
   378  				// other end told us there are no more
   379  				// changes. Stop? Is it restarting?
   380  				return nil
   381  
   382  			default:
   383  				h.logger.Error().Err(err).Msg("receiving ad-hoc check")
   384  				return err
   385  			}
   386  		}
   387  	}
   388  }
   389  
   390  func (h *Handler) handleAdHocCheck(ctx context.Context, ahReq *sm.AdHocRequest) error {
   391  	h.logger.Debug().Interface("request", ahReq).Msg("got ad-hoc check request")
   392  
   393  	h.metrics.opsCounter.WithLabelValues(ahReq.AdHocCheck.Type().String()).Inc()
   394  
   395  	runner, err := h.runnerFactory(ctx, ahReq)
   396  	if err != nil {
   397  		// TODO(mem): if the runner factory returns an error, we should
   398  		// create a result that reflects that and publish it, so that
   399  		// the frontend is able to communicate to the user that
   400  		// something went wrong.
   401  		return err
   402  	}
   403  
   404  	go runner.Run(ctx, model.GlobalID(ahReq.AdHocCheck.TenantId), h.publisher)
   405  
   406  	// If there's a tenant in the request, this should be forwarded
   407  	// to the changes handler.
   408  	if ahReq.Tenant != nil {
   409  		h.tenantCh <- *ahReq.Tenant
   410  	}
   411  
   412  	return nil
   413  }
   414  
   415  func defaultGrpcAdhocChecksClientFactory(conn ClientConn) (sm.AdHocChecksClient, error) {
   416  	cc, ok := conn.(*grpc.ClientConn)
   417  	if !ok {
   418  		return nil, fmt.Errorf("unexpected type of connection: %T", conn)
   419  	}
   420  
   421  	return sm.NewAdHocChecksClient(cc), nil
   422  }
   423  
   424  func (h *Handler) defaultRunnerFactory(ctx context.Context, req *sm.AdHocRequest) (*runner, error) {
   425  	// This should never happen. If we hit this, it's a bug in the code
   426  	// that is handling the request, or the API sent us an invalid request.
   427  	if req == nil || req.AdHocCheck.TenantId == 0 {
   428  		return nil, errInvalidAdHocRequest
   429  	}
   430  
   431  	check := model.Check{
   432  		Check: sm.Check{
   433  			TenantId: req.AdHocCheck.TenantId,
   434  			Target:   req.AdHocCheck.Target,
   435  			Timeout:  req.AdHocCheck.Timeout,
   436  			Settings: req.AdHocCheck.Settings,
   437  			Channels: req.AdHocCheck.Channels,
   438  
   439  			// All the following fields are not used for ad-hoc checks.
   440  			Id:               0, // ad-hoc checks don't have an ID in this sense.
   441  			Job:              "",
   442  			Frequency:        0,
   443  			Offset:           0,
   444  			Enabled:          true,
   445  			Labels:           nil,
   446  			Probes:           nil,
   447  			BasicMetricsOnly: false,
   448  			AlertSensitivity: "",
   449  			Created:          0,
   450  			Modified:         0,
   451  		},
   452  	}
   453  
   454  	p, target, err := h.proberFactory.New(ctx, h.logger, check)
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  
   459  	// Ad-hoc checks that rely on k6 runners need some grace time to give the runner time to communicate a timeout back
   460  	// to us, instead of both us and the runner timing out at the same time and missing the runner's response. We define
   461  	// that grace time to be 20 seconds, to be consistent with the grace time used for k6, non ad-hoc checks.
   462  	timeout := time.Duration(check.Timeout) * time.Millisecond
   463  	switch check.Type() {
   464  	case sm.CheckTypeMultiHttp, sm.CheckTypeScripted, sm.CheckTypeBrowser:
   465  		timeout += k6AdhocGraceTime
   466  	}
   467  
   468  	return &runner{
   469  		logger:  h.logger,
   470  		prober:  p,
   471  		id:      req.AdHocCheck.Id,
   472  		target:  target,
   473  		probe:   h.probe.Name,
   474  		timeout: timeout,
   475  	}, nil
   476  }
   477  
   478  // sleepCtx is like time.Sleep, but it pays attention to the
   479  // cancellation of the provided context.
   480  func sleepCtx(ctx context.Context, d time.Duration) error {
   481  	var err error
   482  
   483  	timer := time.NewTimer(d)
   484  
   485  	select {
   486  	case <-ctx.Done():
   487  		err = ctx.Err()
   488  
   489  		if !timer.Stop() {
   490  			<-timer.C
   491  		}
   492  
   493  	case <-timer.C:
   494  	}
   495  
   496  	return err
   497  }
   498  
   499  // jsonLogger implements the log.Logger interface.
   500  type jsonLogger struct {
   501  	entries []map[string]string
   502  }
   503  
   504  // Log takes key-value pairs and logs them.
   505  func (l *jsonLogger) Log(keyvals ...any) error {
   506  	m := make(map[string]string)
   507  
   508  	if len(keyvals)%2 != 0 {
   509  		return fmt.Errorf("expected even number of keyvals, got %d", len(keyvals))
   510  	}
   511  
   512  	for i := 0; i < len(keyvals); i += 2 {
   513  		k := fmt.Sprintf("%v", keyvals[i])
   514  		v := fmt.Sprintf("%v", keyvals[i+1])
   515  		m[k] = v
   516  	}
   517  
   518  	l.entries = append(l.entries, m)
   519  
   520  	return nil
   521  }
   522  
   523  // Run runs the specified prober once and captures the results using
   524  // jsonLogger.
   525  func (r *runner) Run(ctx context.Context, tenantId model.GlobalID, publisher pusher.Publisher) {
   526  	r.logger.Info().Msg("running ad-hoc check")
   527  
   528  	var (
   529  		logger   = &jsonLogger{}
   530  		registry = prometheus.NewRegistry()
   531  		// This is the execution ID for the check run.
   532  		executionID = uuid.New().String()
   533  	)
   534  
   535  	// TODO(mem): decide what to do with these metrics.
   536  	successGauge := prometheus.NewGauge(prometheus.GaugeOpts{
   537  		Name: "probe_success",
   538  		Help: "whether the check was successful",
   539  	})
   540  
   541  	registry.MustRegister(successGauge)
   542  
   543  	durationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
   544  		Name: "probe_duration_seconds",
   545  		Help: "duration of the check in seconds",
   546  	})
   547  
   548  	registry.MustRegister(durationGauge)
   549  
   550  	start := time.Now()
   551  
   552  	rCtx, cancel := context.WithTimeout(ctx, r.timeout)
   553  	defer cancel()
   554  
   555  	success, duration := r.prober.Probe(rCtx, r.target, registry, logger, executionID)
   556  
   557  	if success {
   558  		successGauge.Set(1)
   559  	} else {
   560  		successGauge.Set(0)
   561  	}
   562  
   563  	if duration != 0 {
   564  		durationGauge.Set(duration)
   565  	} else {
   566  		durationGauge.Set(float64(time.Since(start).Microseconds()) / 1e6)
   567  	}
   568  
   569  	mfs, err := registry.Gather()
   570  
   571  	buf := &bytes.Buffer{}
   572  	targetLogger := zerolog.New(buf)
   573  
   574  	targetLogger.Warn().
   575  		AnErr("error", err).
   576  		Str("id", r.id).
   577  		Str("target", r.target).
   578  		Str("probe", r.probe).
   579  		Str("check_name", r.prober.Name()).
   580  		Interface("logs", logger.entries).
   581  		Interface("timeseries", mfs).
   582  		Msg("ad-hoc check done")
   583  
   584  	r.logger.Debug().
   585  		Str("id", r.id).
   586  		Str("target", r.target).
   587  		Str("probe", r.probe).
   588  		Str("check_name", r.prober.Name()).
   589  		Msg("ad-hoc check done")
   590  
   591  	publisher.Publish(adhocData{
   592  		tenantId: tenantId,
   593  		streams: Streams{
   594  			{
   595  				Labels: fmt.Sprintf(`{probe=%q,source="synthetic-monitoring",type="adhoc"}`, r.probe),
   596  				Entries: []logproto.Entry{
   597  					{
   598  						Timestamp: start,
   599  						Line:      buf.String(),
   600  						StructuredMetadata: logproto.LabelsAdapter{
   601  							{Name: "execution_id", Value: executionID},
   602  						},
   603  					},
   604  				},
   605  			},
   606  		},
   607  	})
   608  
   609  	r.logger.Debug().
   610  		Str("id", r.id).
   611  		Str("target", r.target).
   612  		Str("probe", r.probe).
   613  		Str("check_name", r.prober.Name()).
   614  		Msg("ad-hoc result sent to publisher")
   615  }
   616  
   617  type (
   618  	TimeSeries = []prompb.TimeSeries
   619  	Streams    = []logproto.Stream
   620  )
   621  
   622  type adhocData struct {
   623  	tenantId model.GlobalID
   624  	streams  Streams
   625  }
   626  
   627  func (d adhocData) Metrics() TimeSeries {
   628  	return nil
   629  }
   630  
   631  func (d adhocData) Streams() Streams {
   632  	return d.streams
   633  }
   634  
   635  func (d adhocData) Tenant() model.GlobalID {
   636  	return d.tenantId
   637  }