github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/discovery/http/http.go (about)

     1  //Package http discovery
     2  //Copyright 2021 The Prometheus Authors
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  // http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package http
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"io/ioutil"
    22  	nhttp "net/http"
    23  	nurl "net/url"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	"github.com/pyroscope-io/pyroscope/pkg/build"
    31  	"github.com/pyroscope-io/pyroscope/pkg/scrape/config"
    32  	"github.com/pyroscope-io/pyroscope/pkg/scrape/discovery"
    33  	drefresh "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/refresh"
    34  	"github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/targetgroup"
    35  	"github.com/pyroscope-io/pyroscope/pkg/scrape/model"
    36  	"github.com/sirupsen/logrus"
    37  )
    38  
    39  var (
    40  	// DefaultSDConfig is the default HTTP SD configuration.
    41  	DefaultSDConfig = SDConfig{
    42  		RefreshInterval:  model.Duration(60 * time.Second),
    43  		HTTPClientConfig: config.DefaultHTTPClientConfig,
    44  	}
    45  	userAgent        = fmt.Sprintf("Pyroscope/%s", build.Version)
    46  	matchContentType = regexp.MustCompile(`^(?i:application\/json(;\s*charset=("utf-8"|utf-8))?)$`)
    47  )
    48  
    49  func init() {
    50  	discovery.RegisterConfig(&SDConfig{})
    51  }
    52  
    53  // SDConfig is the configuration for HTTP based discovery.
    54  type SDConfig struct {
    55  	HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
    56  	RefreshInterval  model.Duration          `yaml:"refresh-interval,omitempty"`
    57  	URL              string                  `yaml:"url"`
    58  }
    59  
    60  // Name returns the name of the Config.
    61  func (*SDConfig) Name() string { return "http" }
    62  
    63  // NewDiscoverer returns a Discoverer for the Config.
    64  func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
    65  	return NewDiscovery(c, opts.Logger)
    66  }
    67  
    68  // SetDirectory joins any relative file paths with dir.
    69  func (c *SDConfig) SetDirectory(dir string) {
    70  	c.HTTPClientConfig.SetDirectory(dir)
    71  }
    72  
    73  // UnmarshalYAML implements the yaml.Unmarshaler interface.
    74  func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
    75  	*c = DefaultSDConfig
    76  	type plain SDConfig
    77  	err := unmarshal((*plain)(c))
    78  	if err != nil {
    79  		return err
    80  	}
    81  	if c.URL == "" {
    82  		return fmt.Errorf("URL is missing")
    83  	}
    84  	parsedURL, err := nurl.Parse(c.URL)
    85  	if err != nil {
    86  		return err
    87  	}
    88  	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
    89  		return fmt.Errorf("URL scheme must be 'http' or 'https'")
    90  	}
    91  	if parsedURL.Host == "" {
    92  		return fmt.Errorf("host is missing in URL")
    93  	}
    94  	return nil
    95  }
    96  
    97  const httpSDURLLabel = model.MetaLabelPrefix + "url"
    98  
    99  // Discovery provides service discovery functionality based
   100  // on HTTP endpoints that return target groups in JSON format.
   101  type Discovery struct {
   102  	*drefresh.Discovery
   103  	url             string
   104  	client          *nhttp.Client
   105  	refreshInterval time.Duration
   106  	tgLastLength    int
   107  }
   108  
   109  // NewDiscovery returns a new HTTP discovery for the given config.
   110  func NewDiscovery(conf *SDConfig, logger logrus.FieldLogger) (*Discovery, error) {
   111  	client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http")
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	client.Timeout = time.Duration(conf.RefreshInterval)
   116  
   117  	d := &Discovery{
   118  		url:             conf.URL,
   119  		client:          client,
   120  		refreshInterval: time.Duration(conf.RefreshInterval), // Stored to be sent as headers.
   121  	}
   122  
   123  	d.Discovery = drefresh.NewDiscovery(
   124  		logger,
   125  		"http",
   126  		time.Duration(conf.RefreshInterval),
   127  		d.refresh,
   128  	)
   129  	return d, nil
   130  }
   131  
   132  func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
   133  	req, err := nhttp.NewRequest("GET", d.url, nil)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	req.Header.Set("User-Agent", userAgent)
   138  	req.Header.Set("Accept", "application/json")
   139  	req.Header.Set("X-Pyroscope-Refresh-Interval-Seconds", strconv.FormatFloat(d.refreshInterval.Seconds(), 'f', -1, 64))
   140  
   141  	resp, err := d.client.Do(req.WithContext(ctx))
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	defer func() {
   146  		io.Copy(ioutil.Discard, resp.Body)
   147  		resp.Body.Close()
   148  	}()
   149  
   150  	if resp.StatusCode != nhttp.StatusOK {
   151  		return nil, errors.Errorf("server returned HTTP status %s", resp.Status)
   152  	}
   153  
   154  	if !matchContentType.MatchString(strings.TrimSpace(resp.Header.Get("Content-Type"))) {
   155  		return nil, errors.Errorf("unsupported content type %q", resp.Header.Get("Content-Type"))
   156  	}
   157  
   158  	b, err := ioutil.ReadAll(resp.Body)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	var targetGroups []*targetgroup.Group
   164  
   165  	if err := json.Unmarshal(b, &targetGroups); err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	for i, tg := range targetGroups {
   170  		if tg == nil {
   171  			err = errors.New("nil target group item found")
   172  			return nil, err
   173  		}
   174  
   175  		tg.Source = urlSource(d.url, i)
   176  		if tg.Labels == nil {
   177  			tg.Labels = model.LabelSet{}
   178  		}
   179  		tg.Labels[httpSDURLLabel] = model.LabelValue(d.url)
   180  	}
   181  
   182  	// Generate empty updates for sources that disappeared.
   183  	l := len(targetGroups)
   184  	for i := l; i < d.tgLastLength; i++ {
   185  		targetGroups = append(targetGroups, &targetgroup.Group{Source: urlSource(d.url, i)})
   186  	}
   187  	d.tgLastLength = l
   188  
   189  	return targetGroups, nil
   190  }
   191  
   192  // urlSource returns a source ID for the i-th target group per URL.
   193  func urlSource(url string, i int) string {
   194  	return fmt.Sprintf("%s:%d", url, i)
   195  }