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

     1  package prom
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"crypto/sha256"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"fmt"
    11  	"io"
    12  	"maps"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/gogo/protobuf/proto"
    21  	"github.com/golang/snappy"
    22  	"github.com/mwitkow/go-conntrack"
    23  	"github.com/pkg/errors"
    24  	"github.com/prometheus/prometheus/prompb"
    25  )
    26  
    27  const maxErrMsgLen = 256
    28  
    29  var (
    30  	maxRetries = 10
    31  	minBackoff = 30 * time.Millisecond
    32  	maxBackoff = 250 * time.Millisecond
    33  )
    34  
    35  type recoverableError struct {
    36  	error
    37  }
    38  
    39  func NewRecoverableError(cause error) error {
    40  	return recoverableError{cause}
    41  }
    42  
    43  type HttpError struct {
    44  	StatusCode int
    45  	Status     string
    46  	Err        error
    47  }
    48  
    49  func (e *HttpError) Unwrap() error {
    50  	return e.Err
    51  }
    52  
    53  func (e *HttpError) Error() string {
    54  	return e.Err.Error()
    55  }
    56  
    57  func GetHttpStatusCode(err error) (int, bool) {
    58  	for ; err != nil; err = errors.Unwrap(err) {
    59  		err2, ok := err.(*HttpError)
    60  		if ok {
    61  			return err2.StatusCode, true
    62  		}
    63  	}
    64  
    65  	return 0, false
    66  }
    67  
    68  type PrometheusClient interface {
    69  	StoreBytes(ctx context.Context, req []byte) error
    70  	StoreStream(ctx context.Context, req io.Reader) error
    71  	CountRetries(retries float64)
    72  }
    73  
    74  func SendBytesWithBackoff(ctx context.Context, client PrometheusClient, req []byte) error {
    75  	return withBackoff(ctx, client, func(ctx context.Context, client PrometheusClient) error {
    76  		return client.StoreBytes(ctx, req)
    77  	})
    78  }
    79  
    80  func withBackoff(ctx context.Context, client PrometheusClient, store func(ctx context.Context, prometheusClient PrometheusClient) error) error {
    81  	clampBackoff := func(a time.Duration) time.Duration {
    82  		if a > maxBackoff {
    83  			return maxBackoff
    84  		}
    85  
    86  		return a
    87  	}
    88  
    89  	var err error
    90  
    91  	backoff := minBackoff
    92  
    93  	retries := maxRetries
    94  
    95  	defer func() {
    96  		// Performing the retries performed calculation inside a go-routine to evaluate at defer-time
    97  		client.CountRetries(float64(maxRetries - retries))
    98  	}()
    99  
   100  	for retries > 0 {
   101  		select {
   102  		case <-ctx.Done():
   103  			return ctx.Err()
   104  		default:
   105  		}
   106  
   107  		err = store(ctx, client)
   108  		if err == nil {
   109  			return nil
   110  		}
   111  
   112  		if _, ok := err.(recoverableError); !ok {
   113  			return err
   114  		}
   115  
   116  		retries--
   117  
   118  		if retries > 0 {
   119  			time.Sleep(backoff)
   120  			backoff = clampBackoff(2 * backoff)
   121  		}
   122  	}
   123  
   124  	return err
   125  }
   126  
   127  // sendSamples to the remote storage with backoff for recoverable errors.
   128  func SendSamplesWithBackoff(ctx context.Context, client *Client, samples []prompb.TimeSeries, buf *[]byte) error {
   129  	req, _, err := buildTimeSeriesWriteRequest(samples, *buf)
   130  	*buf = req
   131  
   132  	if err != nil {
   133  		// Failing to build the write request is non-recoverable, since it will
   134  		// only error if marshaling the proto to bytes fails.
   135  		return err
   136  	}
   137  
   138  	return SendBytesWithBackoff(ctx, client, req)
   139  }
   140  
   141  func buildTimeSeriesWriteRequest(samples []prompb.TimeSeries, buf []byte) ([]byte, int64, error) {
   142  	var highest int64
   143  	for _, ts := range samples {
   144  		// At the moment we only ever append a TimeSeries with a single sample in it.
   145  		if ts.Samples[0].Timestamp > highest {
   146  			highest = ts.Samples[0].Timestamp
   147  		}
   148  	}
   149  
   150  	req := &prompb.WriteRequest{
   151  		Timeseries: samples,
   152  	}
   153  
   154  	data, err := proto.Marshal(req)
   155  	if err != nil {
   156  		return nil, highest, err
   157  	}
   158  
   159  	// snappy uses len() to see if it needs to allocate a new slice. Make the
   160  	// buffer as long as possible.
   161  	if buf != nil {
   162  		buf = buf[0:cap(buf)]
   163  	}
   164  
   165  	compressed := snappy.Encode(buf, data)
   166  
   167  	return compressed, highest, nil
   168  }
   169  
   170  type closeIdler interface {
   171  	CloseIdleConnections()
   172  }
   173  
   174  // BasicAuth contains basic HTTP authentication credentials.
   175  type BasicAuth struct {
   176  	Username string `yaml:"username"`
   177  	Password string `yaml:"password,omitempty"`
   178  }
   179  
   180  // TLSConfig configures the options for TLS connections.
   181  type TLSConfig struct {
   182  	// The CA cert to use for the targets.
   183  	CAFile string `yaml:"ca_file,omitempty"`
   184  	// The client cert file for the targets.
   185  	CertFile string `yaml:"cert_file,omitempty"`
   186  	// The client key file for the targets.
   187  	KeyFile string `yaml:"key_file,omitempty"`
   188  	// Used to verify the hostname for the targets.
   189  	ServerName string `yaml:"server_name,omitempty"`
   190  	// Disable target certificate validation.
   191  	InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
   192  }
   193  
   194  // HTTPClientConfig configures an HTTP client.
   195  type HTTPClientConfig struct {
   196  	// The HTTP basic authentication credentials for the targets.
   197  	BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"`
   198  	// TLSConfig to use to connect to the targets.
   199  	TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
   200  }
   201  
   202  // ClientConfig configures a Client.
   203  type ClientConfig struct {
   204  	URL              *url.URL
   205  	Timeout          time.Duration
   206  	HTTPClientConfig HTTPClientConfig
   207  	UserAgent        string
   208  	Headers          map[string]string
   209  }
   210  
   211  type counterMetricFunc func(float64)
   212  
   213  type Client struct {
   214  	remoteName       string
   215  	url              *url.URL
   216  	client           *http.Client
   217  	timeout          time.Duration
   218  	headers          map[string]string
   219  	countRetriesFunc counterMetricFunc
   220  }
   221  
   222  // NewClient creates a new Client.
   223  func NewClient(remoteName string, conf *ClientConfig, retriesCounter counterMetricFunc) (*Client, error) {
   224  	httpClient, err := NewClientFromConfig(conf.HTTPClientConfig, "remote_storage", false)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  
   229  	headers := map[string]string{
   230  		"User-Agent": conf.UserAgent,
   231  	}
   232  
   233  	// Copy other headers
   234  	maps.Copy(headers, conf.Headers)
   235  
   236  	return &Client{
   237  		remoteName:       remoteName,
   238  		url:              conf.URL,
   239  		client:           httpClient,
   240  		timeout:          conf.Timeout,
   241  		headers:          headers,
   242  		countRetriesFunc: retriesCounter,
   243  	}, nil
   244  }
   245  
   246  // NewClient returns a http.Client using the specified http.RoundTripper.
   247  func newClient(rt http.RoundTripper) *http.Client {
   248  	return &http.Client{Transport: rt}
   249  }
   250  
   251  // NewClientFromConfig returns a new HTTP client configured for the
   252  // given config.HTTPClientConfig. The name is used as go-conntrack metric label.
   253  func NewClientFromConfig(cfg HTTPClientConfig, name string, disableKeepAlives bool) (*http.Client, error) {
   254  	rt, err := newRoundTripperFromConfig(cfg, name, disableKeepAlives)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	return newClient(rt), nil
   260  }
   261  
   262  // newRoundTripperFromConfig returns a new HTTP RoundTripper configured for the
   263  // given config.HTTPClientConfig. The name is used as go-conntrack metric label.
   264  func newRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAlives bool) (http.RoundTripper, error) {
   265  	newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) {
   266  		// The only timeout we care about is the configured scrape timeout.
   267  		// It is applied on request. So we leave out any timings here.
   268  		var rt http.RoundTripper = &http.Transport{
   269  			MaxIdleConns:        20000,
   270  			MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801
   271  			DisableKeepAlives:   disableKeepAlives,
   272  			TLSClientConfig:     tlsConfig,
   273  			DisableCompression:  true,
   274  			// 5 minutes is typically above the maximum sane scrape interval. So we can
   275  			// use keepalive for all configurations.
   276  			IdleConnTimeout:       5 * time.Minute,
   277  			TLSHandshakeTimeout:   10 * time.Second,
   278  			ExpectContinueTimeout: 1 * time.Second,
   279  			Proxy:                 http.ProxyFromEnvironment,
   280  			DialContext: conntrack.NewDialContextFunc(
   281  				conntrack.DialWithTracing(),
   282  				conntrack.DialWithName(name),
   283  			),
   284  		}
   285  
   286  		if cfg.BasicAuth != nil {
   287  			rt = newBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, rt)
   288  		}
   289  		// Return a new configured RoundTripper.
   290  		return rt, nil
   291  	}
   292  
   293  	tlsConfig, err := NewTLSConfig(&cfg.TLSConfig)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	if len(cfg.TLSConfig.CAFile) == 0 {
   299  		// No need for a RoundTripper that reloads the CA file automatically.
   300  		return newRT(tlsConfig)
   301  	}
   302  
   303  	return newTLSRoundTripper(tlsConfig, cfg.TLSConfig.CAFile, newRT)
   304  }
   305  
   306  // NewTLSConfig creates a new tls.Config from the given TLSConfig.
   307  func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) {
   308  	// TODO(mem): figure out if there's a reasonable way to deal
   309  	// with this.
   310  	//
   311  	// #nosec
   312  	tlsConfig := &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}
   313  
   314  	// If a CA cert is provided then let's read it in so we can validate the
   315  	// scrape target's certificate properly.
   316  	if len(cfg.CAFile) > 0 {
   317  		b, err := readCAFile(cfg.CAFile)
   318  		if err != nil {
   319  			return nil, err
   320  		}
   321  
   322  		if !updateRootCA(tlsConfig, b) {
   323  			return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile)
   324  		}
   325  	}
   326  
   327  	if len(cfg.ServerName) > 0 {
   328  		tlsConfig.ServerName = cfg.ServerName
   329  	}
   330  	// If a client cert & key is provided then configure TLS config accordingly.
   331  	switch {
   332  	case len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0:
   333  		return nil, fmt.Errorf("client cert file %q specified without client key file", cfg.CertFile)
   334  	case len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0:
   335  		return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile)
   336  	case len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0:
   337  		// Verify that client cert and key are valid.
   338  		if _, err := cfg.getClientCertificate(nil); err != nil {
   339  			return nil, err
   340  		}
   341  
   342  		tlsConfig.GetClientCertificate = cfg.getClientCertificate
   343  	}
   344  
   345  	return tlsConfig, nil
   346  }
   347  
   348  // getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate.
   349  func (c *TLSConfig) getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
   350  	cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
   351  	if err != nil {
   352  		return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", c.CertFile, c.KeyFile, err)
   353  	}
   354  
   355  	return &cert, nil
   356  }
   357  
   358  type basicAuthRoundTripper struct {
   359  	username string
   360  	password string
   361  	rt       http.RoundTripper
   362  }
   363  
   364  // newBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has
   365  // already been set.
   366  func newBasicAuthRoundTripper(username string, password string, rt http.RoundTripper) http.RoundTripper {
   367  	return &basicAuthRoundTripper{username, password, rt}
   368  }
   369  
   370  func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   371  	if len(req.Header.Get("Authorization")) != 0 {
   372  		return rt.rt.RoundTrip(req)
   373  	}
   374  
   375  	req = cloneRequest(req)
   376  	req.SetBasicAuth(rt.username, strings.TrimSpace(rt.password))
   377  
   378  	return rt.rt.RoundTrip(req)
   379  }
   380  
   381  // CountRetries registers the number of retries the client did internally, in a generic metric. The main purpose is to
   382  // wrap in a simple way the underlying Prometheus metric.
   383  func (c *Client) CountRetries(retries float64) {
   384  	c.countRetriesFunc(retries)
   385  }
   386  
   387  // StoreBytes sends a batch of samples to the HTTP endpoint, the request is the proto marshalled
   388  // and encoded bytes from codec.go.
   389  func (c *Client) StoreBytes(ctx context.Context, req []byte) error {
   390  	return c.StoreStream(ctx, bytes.NewReader(req))
   391  }
   392  
   393  // StoreStream sends a batch of samples to the HTTP endpoint, the request is the proto marshalled
   394  // and encoded bytes from codec.go.
   395  func (c *Client) StoreStream(ctx context.Context, req io.Reader) error {
   396  	// Setup the new request...
   397  	httpReq, err := http.NewRequest("POST", c.url.String(), req)
   398  	if err != nil {
   399  		// Errors from NewRequest are from unparsable URLs, so are not
   400  		// recoverable.
   401  		return err
   402  	}
   403  
   404  	httpReq.Header.Add("Content-Encoding", "snappy")
   405  	httpReq.Header.Set("Content-Type", "application/x-protobuf")
   406  
   407  	for k, v := range c.headers {
   408  		httpReq.Header.Set(k, v)
   409  	}
   410  
   411  	// ... and add a context with timeout as late as possible to give it as
   412  	// much chance to finish as possible.
   413  	ctx, cancel := context.WithTimeout(ctx, c.timeout)
   414  	defer cancel()
   415  
   416  	httpResp, err := c.client.Do(httpReq.WithContext(ctx))
   417  	if err != nil {
   418  		// Errors from client.Do are from (for example) network errors, so are
   419  		// recoverable.
   420  		return NewRecoverableError(err)
   421  	}
   422  
   423  	defer func() {
   424  		// we are draining the body, so it's OK to ignore the
   425  		// return value from io.Copy.
   426  		_, _ = io.Copy(io.Discard, httpResp.Body)
   427  
   428  		// If there are close errors we cannot do anything about
   429  		// them.
   430  		_ = httpResp.Body.Close()
   431  	}()
   432  
   433  	if httpResp.StatusCode/100 != 2 {
   434  		scanner := bufio.NewScanner(io.LimitReader(httpResp.Body, maxErrMsgLen))
   435  
   436  		line := ""
   437  		if scanner.Scan() {
   438  			line = scanner.Text()
   439  		}
   440  
   441  		err = &HttpError{
   442  			Status:     httpResp.Status,
   443  			StatusCode: httpResp.StatusCode,
   444  			Err:        errors.Errorf("server returned HTTP status %s: %s", httpResp.Status, line),
   445  		}
   446  	}
   447  
   448  	if httpResp.StatusCode/100 == 5 {
   449  		return NewRecoverableError(err)
   450  	}
   451  
   452  	return err
   453  }
   454  
   455  // tlsRoundTripper is a RoundTripper that updates automatically its TLS
   456  // configuration whenever the content of the CA file changes.
   457  type tlsRoundTripper struct {
   458  	caFile string
   459  	// newRT returns a new RoundTripper.
   460  	newRT func(*tls.Config) (http.RoundTripper, error)
   461  
   462  	mtx        sync.RWMutex
   463  	rt         http.RoundTripper
   464  	hashCAFile []byte
   465  	tlsConfig  *tls.Config
   466  }
   467  
   468  func newTLSRoundTripper(
   469  	cfg *tls.Config,
   470  	caFile string,
   471  	newRT func(*tls.Config) (http.RoundTripper, error),
   472  ) (http.RoundTripper, error) {
   473  	t := &tlsRoundTripper{
   474  		caFile:    caFile,
   475  		newRT:     newRT,
   476  		tlsConfig: cfg,
   477  	}
   478  
   479  	rt, err := t.newRT(t.tlsConfig)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  
   484  	t.rt = rt
   485  
   486  	_, t.hashCAFile, err = t.getCAWithHash()
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	return t, nil
   492  }
   493  
   494  func (t *tlsRoundTripper) getCAWithHash() ([]byte, []byte, error) {
   495  	b, err := readCAFile(t.caFile)
   496  	if err != nil {
   497  		return nil, nil, err
   498  	}
   499  
   500  	h := sha256.Sum256(b)
   501  
   502  	return b, h[:], nil
   503  }
   504  
   505  // RoundTrip implements the http.RoundTrip interface.
   506  func (t *tlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   507  	b, h, err := t.getCAWithHash()
   508  	if err != nil {
   509  		return nil, err
   510  	}
   511  
   512  	t.mtx.RLock()
   513  	equal := bytes.Equal(h, t.hashCAFile)
   514  	rt := t.rt
   515  	t.mtx.RUnlock()
   516  
   517  	if equal {
   518  		// The CA cert hasn't changed, use the existing RoundTripper.
   519  		return rt.RoundTrip(req)
   520  	}
   521  
   522  	// Create a new RoundTripper.
   523  	tlsConfig := t.tlsConfig.Clone()
   524  	if !updateRootCA(tlsConfig, b) {
   525  		return nil, fmt.Errorf("unable to use specified CA cert %s", t.caFile)
   526  	}
   527  
   528  	rt, err = t.newRT(tlsConfig)
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  
   533  	t.CloseIdleConnections()
   534  
   535  	t.mtx.Lock()
   536  	t.rt = rt
   537  	t.hashCAFile = h
   538  	t.mtx.Unlock()
   539  
   540  	return rt.RoundTrip(req)
   541  }
   542  
   543  func (t *tlsRoundTripper) CloseIdleConnections() {
   544  	t.mtx.RLock()
   545  	defer t.mtx.RUnlock()
   546  
   547  	if ci, ok := t.rt.(closeIdler); ok {
   548  		ci.CloseIdleConnections()
   549  	}
   550  }
   551  
   552  // readCAFile reads the CA cert file from disk.
   553  func readCAFile(f string) ([]byte, error) {
   554  	// gosec complains that 'data' will contain a file.
   555  	//
   556  	// #nosec
   557  	data, err := os.ReadFile(f)
   558  	if err != nil {
   559  		return nil, fmt.Errorf("unable to load specified CA cert %s: %s", f, err)
   560  	}
   561  
   562  	return data, nil
   563  }
   564  
   565  // updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs.
   566  func updateRootCA(cfg *tls.Config, b []byte) bool {
   567  	caCertPool := x509.NewCertPool()
   568  	if !caCertPool.AppendCertsFromPEM(b) {
   569  		return false
   570  	}
   571  
   572  	cfg.RootCAs = caCertPool
   573  
   574  	return true
   575  }
   576  
   577  // cloneRequest returns a clone of the provided *http.Request.
   578  // The clone is a shallow copy of the struct and its Header map.
   579  func cloneRequest(r *http.Request) *http.Request {
   580  	// Shallow copy of the struct.
   581  	r2 := new(http.Request)
   582  	*r2 = *r
   583  	// Deep copy of the Header.
   584  	r2.Header = make(http.Header)
   585  	maps.Copy(r2.Header, r.Header)
   586  
   587  	return r2
   588  }