2020-04-29 14:27:08 +00:00
package discoveryutils
import (
2023-01-06 03:34:47 +00:00
"compress/gzip"
"context"
2020-05-04 17:48:02 +00:00
"crypto/tls"
2020-05-19 14:35:47 +00:00
"flag"
2020-05-04 17:48:02 +00:00
"fmt"
2023-01-06 03:34:47 +00:00
"io"
2020-05-04 17:48:02 +00:00
"net"
2020-04-29 14:27:08 +00:00
"net/http"
2023-01-06 03:34:47 +00:00
"net/url"
2020-05-04 17:48:02 +00:00
"strings"
2020-05-19 14:35:47 +00:00
"sync"
2020-04-29 14:27:08 +00:00
"time"
2020-05-04 17:48:02 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
2020-12-24 08:56:10 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
2020-05-19 14:35:47 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
2021-02-01 18:02:51 +00:00
"github.com/VictoriaMetrics/metrics"
2020-04-29 14:27:08 +00:00
)
2020-05-19 14:35:47 +00:00
var (
2020-06-20 14:52:49 +00:00
maxConcurrency = flag . Int ( "promscrape.discovery.concurrency" , 100 , "The maximum number of concurrent requests to Prometheus autodiscovery API (Consul, Kubernetes, etc.)" )
2020-05-19 14:35:47 +00:00
maxWaitTime = flag . Duration ( "promscrape.discovery.concurrentWaitTime" , time . Minute , "The maximum duration for waiting to perform API requests " +
"if more than -promscrape.discovery.concurrency requests are simultaneously performed" )
)
2020-04-29 14:27:08 +00:00
var defaultClient = & http . Client {
Timeout : 30 * time . Second ,
}
2023-01-06 03:34:47 +00:00
var (
concurrencyLimitCh chan struct { }
concurrencyLimitChOnce sync . Once
)
const (
// BlockingClientReadTimeout is the maximum duration for waiting the response from GetBlockingAPI*
BlockingClientReadTimeout = 10 * time . Minute
// DefaultClientReadTimeout is the maximum duration for waiting the response from GetAPI*
DefaultClientReadTimeout = time . Minute
// DefaultClientWriteTimeout is the maximum duration for waiting the request to be sent to GetAPI* and GetBlockingAPI*
DefaultClientWriteTimeout = 10 * time . Second
)
func concurrencyLimitChInit ( ) {
concurrencyLimitCh = make ( chan struct { } , * maxConcurrency )
}
2020-04-29 14:27:08 +00:00
// GetHTTPClient returns default client for http API requests.
func GetHTTPClient ( ) * http . Client {
return defaultClient
}
2020-05-04 17:48:02 +00:00
// Client is http client, which talks to the given apiServer.
type Client struct {
2023-01-06 03:34:47 +00:00
// client is used for short requests.
client * HTTPClient
2020-12-03 17:50:50 +00:00
// blockingClient is used for long-polling requests.
2023-01-06 03:34:47 +00:00
blockingClient * HTTPClient
2020-12-03 17:50:50 +00:00
apiServer string
2021-04-03 21:40:08 +00:00
2023-01-06 03:34:47 +00:00
dialAddr string
setHTTPHeaders func ( req * http . Request )
setHTTPProxyHeaders func ( req * http . Request )
clientCtx context . Context
clientCancel context . CancelFunc
}
// HTTPClient is a wrapper around http.Client with timeouts.
type HTTPClient struct {
client * http . Client
ReadTimeout time . Duration
WriteTimeout time . Duration
2020-05-04 17:48:02 +00:00
}
2022-07-06 23:25:31 +00:00
func addMissingPort ( addr string , isTLS bool ) string {
if strings . Contains ( addr , ":" ) {
return addr
}
if isTLS {
return addr + ":443"
}
return addr + ":80"
}
2021-04-03 21:40:08 +00:00
// NewClient returns new Client for the given args.
2021-10-26 18:21:08 +00:00
func NewClient ( apiServer string , ac * promauth . Config , proxyURL * proxy . URL , proxyAC * promauth . Config ) ( * Client , error ) {
2023-01-06 03:34:47 +00:00
u , err := url . Parse ( apiServer )
if err != nil {
return nil , fmt . Errorf ( "cannot parse provided url %q: %w" , apiServer , err )
}
2020-10-12 10:38:21 +00:00
// special case for unix socket connection
2023-01-06 03:34:47 +00:00
var dialFunc func ( addr string ) ( net . Conn , error )
if string ( u . Scheme ) == "unix" {
dialAddr := u . Path
2020-10-12 10:38:21 +00:00
apiServer = "http://"
dialFunc = func ( _ string ) ( net . Conn , error ) {
return net . Dial ( "unix" , dialAddr )
}
}
2020-12-24 08:52:37 +00:00
2023-01-06 03:34:47 +00:00
dialAddr := u . Host
isTLS := string ( u . Scheme ) == "https"
2021-04-03 21:40:08 +00:00
var tlsCfg * tls . Config
2021-03-09 16:54:09 +00:00
if isTLS {
2020-05-04 17:48:02 +00:00
tlsCfg = ac . NewTLSConfig ( )
}
2023-01-06 03:34:47 +00:00
setHTTPProxyHeaders := func ( req * http . Request ) { }
2022-07-06 23:25:31 +00:00
dialAddr = addMissingPort ( dialAddr , isTLS )
2020-12-24 08:56:10 +00:00
if dialFunc == nil {
2021-04-03 21:40:08 +00:00
var err error
dialFunc , err = proxyURL . NewDialFunc ( proxyAC )
2020-12-24 08:56:10 +00:00
if err != nil {
return nil , err
}
2023-01-06 03:34:47 +00:00
if proxyAC != nil {
setHTTPProxyHeaders = func ( req * http . Request ) {
proxyURL . SetHeaders ( proxyAC , req )
}
}
2020-12-24 08:56:10 +00:00
}
2023-01-06 03:34:47 +00:00
hcTransport := & http . Transport {
TLSClientConfig : tlsCfg ,
MaxConnsPerHost : 2 * * maxConcurrency ,
ResponseHeaderTimeout : * maxWaitTime ,
DialContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
return dialFunc ( dialAddr )
} ,
2020-05-04 17:48:02 +00:00
}
2023-01-06 03:34:47 +00:00
hc := & http . Client {
Timeout : DefaultClientReadTimeout ,
Transport : hcTransport ,
2020-12-03 17:47:40 +00:00
}
2023-01-06 03:34:47 +00:00
blockingTransport := & http . Transport {
TLSClientConfig : tlsCfg ,
MaxConnsPerHost : 64 * 1024 ,
DialContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
return dialFunc ( dialAddr )
} ,
}
blockingClient := & http . Client {
Timeout : BlockingClientReadTimeout ,
Transport : blockingTransport ,
2021-04-03 21:40:08 +00:00
}
2020-05-04 17:48:02 +00:00
2023-01-06 03:34:47 +00:00
setHTTPHeaders := func ( req * http . Request ) { }
if ac != nil {
setHTTPHeaders = func ( req * http . Request ) { ac . SetHeaders ( req , true ) }
}
2020-12-11 15:22:37 +00:00
2023-01-06 03:34:47 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
2020-05-19 14:35:47 +00:00
2023-01-06 03:34:47 +00:00
return & Client {
client : & HTTPClient { client : hc , ReadTimeout : DefaultClientReadTimeout , WriteTimeout : DefaultClientWriteTimeout } ,
blockingClient : & HTTPClient { client : blockingClient , ReadTimeout : BlockingClientReadTimeout , WriteTimeout : DefaultClientWriteTimeout } ,
apiServer : apiServer ,
dialAddr : dialAddr ,
setHTTPHeaders : setHTTPHeaders ,
setHTTPProxyHeaders : setHTTPProxyHeaders ,
clientCtx : ctx ,
clientCancel : cancel ,
} , nil
2020-05-19 14:35:47 +00:00
}
2020-12-03 17:50:50 +00:00
// Addr returns the address the client connects to.
func ( c * Client ) Addr ( ) string {
2023-01-06 03:34:47 +00:00
return c . dialAddr
2020-12-03 17:47:40 +00:00
}
2021-06-22 10:33:37 +00:00
// GetAPIResponseWithReqParams returns response for given absolute path with optional callback for request.
// modifyRequestParams should never reference data from request.
2023-01-06 03:34:47 +00:00
func ( c * Client ) GetAPIResponseWithReqParams ( path string , modifyRequestParams func ( request * http . Request ) ) ( [ ] byte , error ) {
2021-06-22 10:33:37 +00:00
return c . getAPIResponse ( path , modifyRequestParams )
}
2020-05-04 17:48:02 +00:00
// GetAPIResponse returns response for the given absolute path.
func ( c * Client ) GetAPIResponse ( path string ) ( [ ] byte , error ) {
2021-06-22 10:33:37 +00:00
return c . getAPIResponse ( path , nil )
}
// GetAPIResponse returns response for the given absolute path with optional callback for request.
2023-01-06 03:34:47 +00:00
func ( c * Client ) getAPIResponse ( path string , modifyRequest func ( request * http . Request ) ) ( [ ] byte , error ) {
2020-05-19 14:35:47 +00:00
// Limit the number of concurrent API requests.
concurrencyLimitChOnce . Do ( concurrencyLimitChInit )
t := timerpool . Get ( * maxWaitTime )
select {
case concurrencyLimitCh <- struct { } { } :
timerpool . Put ( t )
case <- t . C :
timerpool . Put ( t )
return nil , fmt . Errorf ( "too many outstanding requests to %q; try increasing -promscrape.discovery.concurrentWaitTime=%s or -promscrape.discovery.concurrency=%d" ,
c . apiServer , * maxWaitTime , * maxConcurrency )
}
defer func ( ) { <- concurrencyLimitCh } ( )
2023-01-06 03:34:47 +00:00
return c . getAPIResponseWithParamsAndClient ( c . client , path , modifyRequest , nil )
2020-12-03 17:47:40 +00:00
}
2020-05-19 14:35:47 +00:00
2020-12-03 17:47:40 +00:00
// GetBlockingAPIResponse returns response for given absolute path with blocking client and optional callback for api response,
// inspectResponse - should never reference data from response.
2023-01-06 03:34:47 +00:00
func ( c * Client ) GetBlockingAPIResponse ( path string , inspectResponse func ( resp * http . Response ) ) ( [ ] byte , error ) {
2021-06-22 10:33:37 +00:00
return c . getAPIResponseWithParamsAndClient ( c . blockingClient , path , nil , inspectResponse )
2020-12-03 17:47:40 +00:00
}
2021-06-22 10:33:37 +00:00
// getAPIResponseWithParamsAndClient returns response for the given absolute path with optional callback for request and for response.
2023-01-06 03:34:47 +00:00
func ( c * Client ) getAPIResponseWithParamsAndClient ( client * HTTPClient , path string , modifyRequest func ( req * http . Request ) , inspectResponse func ( resp * http . Response ) ) ( [ ] byte , error ) {
2020-05-04 17:48:02 +00:00
requestURL := c . apiServer + path
2023-01-06 03:34:47 +00:00
u , err := url . Parse ( requestURL )
if err != nil {
return nil , fmt . Errorf ( "cannot parse %q: %w" , requestURL , err )
}
u . Host = c . dialAddr
deadline := time . Now ( ) . Add ( client . WriteTimeout )
ctx , cancel := context . WithDeadline ( c . clientCtx , deadline )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "GET" , u . String ( ) , nil )
if err != nil {
return nil , fmt . Errorf ( "cannot create request for %q: %w" , requestURL , err )
2021-04-03 21:40:08 +00:00
}
2023-01-06 03:34:47 +00:00
req . Header . Set ( "Host" , c . dialAddr )
2020-05-04 17:48:02 +00:00
req . Header . Set ( "Accept-Encoding" , "gzip" )
2023-01-06 03:34:47 +00:00
c . setHTTPHeaders ( req )
c . setHTTPProxyHeaders ( req )
2021-06-22 10:33:37 +00:00
if modifyRequest != nil {
2023-01-06 03:34:47 +00:00
modifyRequest ( req )
2021-06-22 10:33:37 +00:00
}
2020-12-03 17:47:40 +00:00
2023-01-06 03:34:47 +00:00
resp , err := doRequestWithPossibleRetry ( client , req )
if err != nil {
2020-06-30 19:58:18 +00:00
return nil , fmt . Errorf ( "cannot fetch %q: %w" , requestURL , err )
2020-05-04 17:48:02 +00:00
}
2023-01-06 03:34:47 +00:00
reader := resp . Body
if resp . Header . Get ( "Content-Encoding" ) == "gzip" {
reader , err = gzip . NewReader ( resp . Body )
2020-05-04 17:48:02 +00:00
if err != nil {
2023-01-06 03:34:47 +00:00
return nil , fmt . Errorf ( "cannot create gzip reader for %q: %w" , requestURL , err )
2020-05-04 17:48:02 +00:00
}
}
2023-01-06 03:34:47 +00:00
data , err := io . ReadAll ( reader )
if err != nil {
return nil , fmt . Errorf ( "cannot ungzip response from %q: %w" , requestURL , err )
}
_ = resp . Body . Close ( )
2020-12-03 17:47:40 +00:00
if inspectResponse != nil {
2023-01-06 03:34:47 +00:00
inspectResponse ( resp )
2020-12-03 17:47:40 +00:00
}
2023-01-06 03:34:47 +00:00
statusCode := resp . StatusCode
if statusCode != http . StatusOK {
2020-05-04 17:48:02 +00:00
return nil , fmt . Errorf ( "unexpected status code returned from %q: %d; expecting %d; response body: %q" ,
2023-01-06 03:34:47 +00:00
requestURL , statusCode , http . StatusOK , data )
2020-05-04 17:48:02 +00:00
}
return data , nil
}
2020-08-13 19:31:42 +00:00
2022-12-09 02:29:10 +00:00
// APIServer returns the API server address
func ( c * Client ) APIServer ( ) string {
return c . apiServer
}
2023-01-06 03:34:47 +00:00
// Stop cancels all in-flight requests
func ( c * Client ) Stop ( ) {
c . clientCancel ( )
}
// DoRequestWithPossibleRetry performs the given req at client and stores the response at resp.
func DoRequestWithPossibleRetry ( hc * HTTPClient , req * http . Request , requestCounter , retryCounter * metrics . Counter ) ( * http . Response , error ) {
2021-05-13 07:38:43 +00:00
sleepTime := time . Second
2022-08-16 11:52:38 +00:00
requestCounter . Inc ( )
2023-01-06 03:34:47 +00:00
deadline , ok := req . Context ( ) . Deadline ( )
if ! ok {
deadline = time . Now ( ) . Add ( hc . WriteTimeout )
}
2020-08-13 19:31:42 +00:00
for {
2023-01-06 03:34:47 +00:00
resp , err := hc . client . Do ( req )
2020-08-13 19:31:42 +00:00
if err == nil {
2023-01-06 03:34:47 +00:00
statusCode := resp . StatusCode
if statusCode != http . StatusTooManyRequests {
return resp , nil
2022-08-16 11:52:38 +00:00
}
2023-01-06 03:34:47 +00:00
} else if err != net . ErrClosed && ! strings . Contains ( err . Error ( ) , "broken pipe" ) {
return nil , err
2020-08-13 19:31:42 +00:00
}
2022-08-16 11:52:38 +00:00
// Retry request after exponentially increased sleep.
2021-05-13 07:38:43 +00:00
maxSleepTime := time . Until ( deadline )
if sleepTime > maxSleepTime {
2023-01-06 03:34:47 +00:00
return nil , fmt . Errorf ( "the server closes all the connection attempts: %w" , err )
2020-08-13 19:31:42 +00:00
}
2021-05-13 07:38:43 +00:00
sleepTime += sleepTime
if sleepTime > maxSleepTime {
sleepTime = maxSleepTime
}
time . Sleep ( sleepTime )
2022-08-16 11:52:38 +00:00
retryCounter . Inc ( )
2020-08-13 19:31:42 +00:00
}
}
2021-02-01 18:02:51 +00:00
2023-01-06 03:34:47 +00:00
func doRequestWithPossibleRetry ( hc * HTTPClient , req * http . Request ) ( * http . Response , error ) {
return DoRequestWithPossibleRetry ( hc , req , discoveryRequests , discoveryRetries )
2022-08-16 11:52:38 +00:00
}
2021-02-01 18:02:51 +00:00
var (
discoveryRequests = metrics . NewCounter ( ` vm_promscrape_discovery_requests_total ` )
2022-08-16 11:52:38 +00:00
discoveryRetries = metrics . NewCounter ( ` vm_promscrape_discovery_retries_total ` )
2021-02-01 18:02:51 +00:00
)