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 }