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 }