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

     1  // Copyright 2013 The Prometheus Authors
     2  // Copyright 2021 The Pyroscope Authors
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  // http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package scrape
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"hash/fnv"
    22  	"net"
    23  	"net/url"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  	"unicode/utf8"
    29  
    30  	"github.com/pyroscope-io/pyroscope/pkg/scrape/config"
    31  	"github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/targetgroup"
    32  	"github.com/pyroscope-io/pyroscope/pkg/scrape/labels"
    33  	"github.com/pyroscope-io/pyroscope/pkg/scrape/model"
    34  	"github.com/pyroscope-io/pyroscope/pkg/scrape/relabel"
    35  )
    36  
    37  // TargetHealth describes the health state of a target.
    38  type TargetHealth string
    39  
    40  // The possible health states of a target based on the last performed scrape.
    41  const (
    42  	HealthUnknown TargetHealth = "unknown"
    43  	HealthGood    TargetHealth = "up"
    44  	HealthBad     TargetHealth = "down"
    45  
    46  	spyName = "gospy"
    47  )
    48  
    49  // Target refers to a singular HTTP or HTTPS endpoint.
    50  type Target struct {
    51  	// Labels before any processing.
    52  	discoveredLabels labels.Labels
    53  	// Any labels that are added to this target and its metrics.
    54  	labels labels.Labels
    55  	// Additional parameters including profile path, URL params,
    56  	// and sample-type settings.
    57  	config *config.Profile
    58  
    59  	mtx                sync.RWMutex
    60  	lastError          error
    61  	lastScrape         time.Time
    62  	lastScrapeDuration time.Duration
    63  	health             TargetHealth
    64  }
    65  
    66  // NewTarget creates a reasonably configured target for querying.
    67  func NewTarget(origLabels, discoveredLabels labels.Labels, profile *config.Profile) *Target {
    68  	return &Target{
    69  		labels:           origLabels,
    70  		discoveredLabels: discoveredLabels,
    71  		config:           profile,
    72  		health:           HealthUnknown,
    73  	}
    74  }
    75  
    76  func (t *Target) String() string {
    77  	return t.URL().String()
    78  }
    79  
    80  // hash returns an identifying hash for the target.
    81  func (t *Target) hash() uint64 {
    82  	h := fnv.New64a()
    83  	_, _ = h.Write([]byte(fmt.Sprintf("%016d", t.labels.Hash())))
    84  	_, _ = h.Write([]byte(t.URL().String()))
    85  	return h.Sum64()
    86  }
    87  
    88  // offset returns the time until the next scrape cycle for the target.
    89  func (t *Target) offset(interval time.Duration) time.Duration {
    90  	now := time.Now().UnixNano()
    91  
    92  	// Base is a pinned to absolute time, no matter how often offset is called.
    93  	var (
    94  		base   = int64(interval) - now%int64(interval)
    95  		offset = t.hash() % uint64(interval)
    96  		next   = base + int64(offset)
    97  	)
    98  
    99  	if next > int64(interval) {
   100  		next -= int64(interval)
   101  	}
   102  	return time.Duration(next)
   103  }
   104  
   105  func (t *Target) SpyName() string {
   106  	for _, l := range t.labels {
   107  		if l.Name == model.SpyNameLabel && l.Value != "" {
   108  			return l.Value
   109  		}
   110  	}
   111  	// N.B: There is no need to check discovered labels:
   112  	// in most cases, the label is created at relabeling.
   113  	for _, l := range t.discoveredLabels {
   114  		if l.Name == model.SpyNameLabel && l.Value != "" {
   115  			return l.Value
   116  		}
   117  	}
   118  	return spyName
   119  }
   120  
   121  func (t *Target) IsCumulative() bool {
   122  	for _, x := range t.config.SampleTypes {
   123  		if x.Cumulative {
   124  			return true
   125  		}
   126  	}
   127  	return false
   128  }
   129  
   130  // Labels returns a copy of the set of all public labels of the target.
   131  func (t *Target) Labels() labels.Labels {
   132  	lset := make(labels.Labels, 0, len(t.labels))
   133  	for _, l := range t.labels {
   134  		if l.Name == model.AppNameLabel || !strings.HasPrefix(l.Name, model.ReservedLabelPrefix) {
   135  			lset = append(lset, l)
   136  		}
   137  	}
   138  	return lset
   139  }
   140  
   141  // DiscoveredLabels returns a copy of the target's labels before any processing.
   142  func (t *Target) DiscoveredLabels() labels.Labels {
   143  	t.mtx.Lock()
   144  	defer t.mtx.Unlock()
   145  	lset := make(labels.Labels, len(t.discoveredLabels))
   146  	copy(lset, t.discoveredLabels)
   147  	return lset
   148  }
   149  
   150  // SetDiscoveredLabels sets new DiscoveredLabels
   151  func (t *Target) SetDiscoveredLabels(l labels.Labels) {
   152  	t.mtx.Lock()
   153  	defer t.mtx.Unlock()
   154  	t.discoveredLabels = l
   155  }
   156  
   157  // URL returns a copy of the target's URL.
   158  func (t *Target) URL() *url.URL {
   159  	u := url.URL{
   160  		Scheme: t.labels.Get(model.SchemeLabel),
   161  		Host:   t.labels.Get(model.AddressLabel),
   162  		Path:   t.config.Path,
   163  	}
   164  	if t.config.Params != nil {
   165  		u.RawQuery = t.config.Params.Encode()
   166  	}
   167  	return &u
   168  }
   169  
   170  // LastError returns the error encountered during the last scrape.
   171  func (t *Target) LastError() error {
   172  	t.mtx.RLock()
   173  	defer t.mtx.RUnlock()
   174  	return t.lastError
   175  }
   176  
   177  // LastScrape returns the time of the last scrape.
   178  func (t *Target) LastScrape() time.Time {
   179  	t.mtx.RLock()
   180  	defer t.mtx.RUnlock()
   181  	return t.lastScrape
   182  }
   183  
   184  // LastScrapeDuration returns how long the last scrape of the target took.
   185  func (t *Target) LastScrapeDuration() time.Duration {
   186  	t.mtx.RLock()
   187  	defer t.mtx.RUnlock()
   188  	return t.lastScrapeDuration
   189  }
   190  
   191  // Health returns the last known health state of the target.
   192  func (t *Target) Health() TargetHealth {
   193  	t.mtx.RLock()
   194  	defer t.mtx.RUnlock()
   195  	return t.health
   196  }
   197  
   198  // intervalAndTimeout returns the interval and timeout derived from
   199  // the targets labels.
   200  func (t *Target) intervalAndTimeout(defaultInterval, defaultDuration time.Duration) (time.Duration, time.Duration, error) {
   201  	t.mtx.RLock()
   202  	defer t.mtx.RUnlock()
   203  
   204  	intervalLabel := t.labels.Get(model.ScrapeIntervalLabel)
   205  	interval, err := time.ParseDuration(intervalLabel)
   206  	if err != nil {
   207  		return defaultInterval, defaultDuration, fmt.Errorf("parsing interval label %q: %w", intervalLabel, err)
   208  	}
   209  	timeoutLabel := t.labels.Get(model.ScrapeTimeoutLabel)
   210  	timeout, err := time.ParseDuration(timeoutLabel)
   211  	if err != nil {
   212  		return defaultInterval, defaultDuration, fmt.Errorf("parsing timeout label %q: %w", timeoutLabel, err)
   213  	}
   214  
   215  	return interval, timeout, nil
   216  }
   217  
   218  func (t *Target) deltaDuration() (time.Duration, error) {
   219  	t.mtx.RLock()
   220  	defer t.mtx.RUnlock()
   221  	// TODO(kolesnikovae): Delta duration from/to labels.
   222  	d, ok := t.config.Params["seconds"]
   223  	if !ok || len(d) != 1 {
   224  		return 0, fmt.Errorf("delta duration is not defined")
   225  	}
   226  	seconds, err := strconv.Atoi(d[0])
   227  	if err != nil {
   228  		return 0, fmt.Errorf("invlid delta duration format %q: %w", d[0], err)
   229  	}
   230  	return time.Second * time.Duration(seconds), nil
   231  }
   232  
   233  // GetValue gets a label value from the entire label set.
   234  func (t *Target) GetValue(name string) string {
   235  	return t.labels.Get(name)
   236  }
   237  
   238  // Targets is a sortable list of targets.
   239  type Targets []*Target
   240  
   241  func (ts Targets) Len() int           { return len(ts) }
   242  func (ts Targets) Less(i, j int) bool { return ts[i].URL().String() < ts[j].URL().String() }
   243  func (ts Targets) Swap(i, j int)      { ts[i], ts[j] = ts[j], ts[i] }
   244  
   245  // PopulateLabels builds a label set from the given label set and scrape configuration.
   246  // It returns a label set before relabeling was applied as the second return value.
   247  // Returns the original discovered label set found before relabelling was applied if the target is dropped during relabeling.
   248  func PopulateLabels(lset labels.Labels, cfg *config.Config) (res, orig labels.Labels, err error) {
   249  	// Copy labels into the labelset for the target if they are not set already.
   250  	scrapeLabels := []labels.Label{
   251  		{Name: model.JobLabel, Value: cfg.JobName},
   252  		{Name: model.ScrapeIntervalLabel, Value: cfg.ScrapeInterval.String()},
   253  		{Name: model.ScrapeTimeoutLabel, Value: cfg.ScrapeTimeout.String()},
   254  		{Name: model.SchemeLabel, Value: cfg.Scheme},
   255  	}
   256  	lb := labels.NewBuilder(lset)
   257  
   258  	for _, l := range scrapeLabels {
   259  		if lv := lset.Get(l.Name); lv == "" {
   260  			lb.Set(l.Name, l.Value)
   261  		}
   262  	}
   263  
   264  	preRelabelLabels := lb.Labels()
   265  	lset = relabel.Process(preRelabelLabels, cfg.RelabelConfigs...)
   266  
   267  	// Check if the target was dropped.
   268  	if lset == nil {
   269  		return nil, preRelabelLabels, nil
   270  	}
   271  	addr := lset.Get(model.AddressLabel)
   272  	if addr == "" {
   273  		return nil, nil, errors.New("no address")
   274  	}
   275  	if v := lset.Get(model.AppNameLabel); v == "" {
   276  		return nil, nil, errors.New("no app name")
   277  	}
   278  
   279  	lb = labels.NewBuilder(lset)
   280  	// addPort checks whether we should add a default port to the address.
   281  	// If the address is not valid, we don't append a port either.
   282  	addPort := func(s string) bool {
   283  		// If we can split, a port exists and we don't have to add one.
   284  		if _, _, err := net.SplitHostPort(s); err == nil {
   285  			return false
   286  		}
   287  		// If adding a port makes it valid, the previous error
   288  		// was not due to an invalid address and we can append a port.
   289  		_, _, err := net.SplitHostPort(s + ":1234")
   290  		return err == nil
   291  	}
   292  
   293  	// If it's an address with no trailing port, infer it based on the used scheme.
   294  	if addPort(addr) {
   295  		// Addresses reaching this point are already wrapped in [] if necessary.
   296  		switch lset.Get(model.SchemeLabel) {
   297  		case "http", "":
   298  			addr = addr + ":80"
   299  		case "https":
   300  			addr = addr + ":443"
   301  		default:
   302  			return nil, nil, fmt.Errorf("invalid scheme: %q", cfg.Scheme)
   303  		}
   304  		lb.Set(model.AddressLabel, addr)
   305  	}
   306  
   307  	if err = config.CheckTargetAddress(addr); err != nil {
   308  		return nil, nil, err
   309  	}
   310  
   311  	var interval string
   312  	var intervalDuration time.Duration
   313  	if interval = lset.Get(model.ScrapeIntervalLabel); interval != cfg.ScrapeInterval.String() {
   314  		intervalDuration, err = time.ParseDuration(interval)
   315  		if err != nil {
   316  			return nil, nil, fmt.Errorf("error parsing scrape interval: %w", err)
   317  		}
   318  		if intervalDuration == 0 {
   319  			return nil, nil, errors.New("scrape interval cannot be 0")
   320  		}
   321  	}
   322  
   323  	var timeout string
   324  	var timeoutDuration time.Duration
   325  	if timeout = lset.Get(model.ScrapeTimeoutLabel); timeout != cfg.ScrapeTimeout.String() {
   326  		timeoutDuration, err = time.ParseDuration(timeout)
   327  		if err != nil {
   328  			return nil, nil, fmt.Errorf("error parsing scrape timeout: %w", err)
   329  		}
   330  		if timeoutDuration == 0 {
   331  			return nil, nil, errors.New("scrape timeout cannot be 0")
   332  		}
   333  	}
   334  
   335  	if timeoutDuration > intervalDuration {
   336  		return nil, nil, fmt.Errorf("scrape timeout cannot be greater than scrape interval (%q > %q)", timeout, interval)
   337  	}
   338  
   339  	// Meta labels are deleted after relabelling. Other internal labels propagate to
   340  	// the target which decides whether they will be part of their label set.
   341  	for _, l := range lset {
   342  		if strings.HasPrefix(l.Name, model.MetaLabelPrefix) {
   343  			lb.Del(l.Name)
   344  		}
   345  	}
   346  
   347  	// Default the instance label to the target address.
   348  	if v := lset.Get(model.InstanceLabel); v == "" {
   349  		lb.Set(model.InstanceLabel, addr)
   350  	}
   351  
   352  	res = lb.Labels()
   353  	for _, l := range res {
   354  		// Check label values are valid, drop the target if not.
   355  		if !isValidLabelValue(l.Value) {
   356  			return nil, nil, fmt.Errorf("invalid label value for %q: %q", l.Name, l.Value)
   357  		}
   358  	}
   359  
   360  	return res, preRelabelLabels, nil
   361  }
   362  
   363  func isValidLabelValue(v string) bool { return utf8.ValidString(v) }
   364  
   365  // TargetsFromGroup builds targets based on the given TargetGroup and config.
   366  func TargetsFromGroup(tg *targetgroup.Group, cfg *config.Config) ([]*Target, []error) {
   367  	targets := make([]*Target, 0, len(tg.Targets))
   368  	failures := []error{}
   369  
   370  	for i, tlset := range tg.Targets {
   371  		lbls := make([]labels.Label, 0, len(tlset)+len(tg.Labels))
   372  		for ln, lv := range tlset {
   373  			lbls = append(lbls, labels.Label{Name: string(ln), Value: string(lv)})
   374  		}
   375  		for ln, lv := range tg.Labels {
   376  			if _, ok := tlset[ln]; !ok {
   377  				lbls = append(lbls, labels.Label{Name: string(ln), Value: string(lv)})
   378  			}
   379  		}
   380  
   381  		lset := labels.New(lbls...)
   382  		lbls, origLabels, err := PopulateLabels(lset, cfg)
   383  		if err != nil {
   384  			failures = append(failures, fmt.Errorf("instance %d in group %s: %w", i, tg.Source, err))
   385  		}
   386  		if lbls == nil || origLabels == nil {
   387  			continue
   388  		}
   389  
   390  		// TODO(kolesnikovae):
   391  		//  Should we allow overrides for sample types, limits, etc?
   392  		//  Add all the configuration prams (e.g. URL params) to labels?
   393  		m := labels.Labels(lbls).Map()
   394  		for profileName := range cfg.Profiles {
   395  			if c, ok := buildConfig(cfg, profileName, m); ok {
   396  				// Targets should not have identical labels.
   397  				// origLabels is immutable.
   398  				labelsCopy := make([]labels.Label, len(lbls), len(lbls)+2)
   399  				copy(labelsCopy, lbls)
   400  				labelsCopy = append(labelsCopy,
   401  					labels.Label{Name: model.ProfilePathLabel, Value: c.Path},
   402  					labels.Label{Name: model.ProfileNameLabel, Value: profileName})
   403  				targets = append(targets, NewTarget(labelsCopy, origLabels, c))
   404  			}
   405  		}
   406  	}
   407  
   408  	return targets, failures
   409  }
   410  
   411  func buildConfig(cfg *config.Config, profileName string, lbls map[string]string) (*config.Profile, bool) {
   412  	prefix := model.ProfileLabelPrefix + profileName + "_"
   413  	// Note that input profile labels don't have '__' suffix.
   414  	switch lbls[prefix+"enabled"] {
   415  	case "true":
   416  	case "false":
   417  		return nil, false
   418  	default:
   419  		if !cfg.IsProfileEnabled(profileName) {
   420  			return nil, false
   421  		}
   422  	}
   423  	defaultConfig, ok := cfg.Profiles[profileName]
   424  	if !ok {
   425  		return nil, false
   426  	}
   427  	// It is assumed SampleTypes is immutable,
   428  	// therefore we can copy Profile value safely.
   429  	var c config.Profile
   430  	c = *defaultConfig
   431  	if path, ok := lbls[prefix+"path"]; ok {
   432  		c.Path = path
   433  	}
   434  	params := make(url.Values, len(c.Params))
   435  	for k, v := range c.Params {
   436  		params[k] = v
   437  	}
   438  	for k, v := range lbls {
   439  		pp := prefix + "param"
   440  		if !strings.HasPrefix(k, pp) {
   441  			continue
   442  		}
   443  		ks := k[len(pp):]
   444  		if len(params[k]) > 0 {
   445  			params[ks][0] = v
   446  		} else {
   447  			params[ks] = []string{v}
   448  		}
   449  	}
   450  	c.Params = params
   451  	return &c, true
   452  }