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 }