2021-06-11 16:04:45 +00:00
|
|
|
// Copyright 2021 The Prometheus Authors
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2022-06-03 11:47:14 +00:00
|
|
|
"errors"
|
2021-06-11 16:04:45 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2021-06-18 09:49:30 +00:00
|
|
|
"strconv"
|
2021-06-28 06:46:53 +00:00
|
|
|
"strings"
|
2021-06-11 16:04:45 +00:00
|
|
|
"time"
|
|
|
|
|
2021-06-11 19:23:58 +00:00
|
|
|
"github.com/go-kit/log"
|
2022-02-12 23:58:27 +00:00
|
|
|
"github.com/grafana/regexp"
|
2022-03-08 13:10:45 +00:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2021-06-11 16:04:45 +00:00
|
|
|
"github.com/prometheus/common/config"
|
|
|
|
"github.com/prometheus/common/model"
|
|
|
|
"github.com/prometheus/common/version"
|
|
|
|
|
|
|
|
"github.com/prometheus/prometheus/discovery"
|
|
|
|
"github.com/prometheus/prometheus/discovery/refresh"
|
|
|
|
"github.com/prometheus/prometheus/discovery/targetgroup"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// DefaultSDConfig is the default HTTP SD configuration.
|
|
|
|
DefaultSDConfig = SDConfig{
|
|
|
|
RefreshInterval: model.Duration(60 * time.Second),
|
|
|
|
HTTPClientConfig: config.DefaultHTTPClientConfig,
|
|
|
|
}
|
2021-06-28 06:46:53 +00:00
|
|
|
userAgent = fmt.Sprintf("Prometheus/%s", version.Version)
|
|
|
|
matchContentType = regexp.MustCompile(`^(?i:application\/json(;\s*charset=("utf-8"|utf-8))?)$`)
|
2021-06-11 16:04:45 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
discovery.RegisterConfig(&SDConfig{})
|
|
|
|
}
|
|
|
|
|
|
|
|
// SDConfig is the configuration for HTTP based discovery.
|
|
|
|
type SDConfig struct {
|
|
|
|
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
|
|
|
|
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
|
|
|
|
URL string `yaml:"url"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the name of the Config.
|
|
|
|
func (*SDConfig) Name() string { return "http" }
|
|
|
|
|
|
|
|
// NewDiscoverer returns a Discoverer for the Config.
|
|
|
|
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
|
2023-10-23 13:55:36 +00:00
|
|
|
return NewDiscovery(c, opts.Logger, opts.HTTPClientOptions, opts.Registerer)
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetDirectory joins any relative file paths with dir.
|
|
|
|
func (c *SDConfig) SetDirectory(dir string) {
|
|
|
|
c.HTTPClientConfig.SetDirectory(dir)
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
|
|
func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
|
|
*c = DefaultSDConfig
|
|
|
|
type plain SDConfig
|
|
|
|
err := unmarshal((*plain)(c))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if c.URL == "" {
|
|
|
|
return fmt.Errorf("URL is missing")
|
|
|
|
}
|
|
|
|
parsedURL, err := url.Parse(c.URL)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
|
|
return fmt.Errorf("URL scheme must be 'http' or 'https'")
|
|
|
|
}
|
|
|
|
if parsedURL.Host == "" {
|
|
|
|
return fmt.Errorf("host is missing in URL")
|
|
|
|
}
|
2023-09-03 20:59:17 +00:00
|
|
|
return c.HTTPClientConfig.Validate()
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const httpSDURLLabel = model.MetaLabelPrefix + "url"
|
|
|
|
|
|
|
|
// Discovery provides service discovery functionality based
|
|
|
|
// on HTTP endpoints that return target groups in JSON format.
|
|
|
|
type Discovery struct {
|
|
|
|
*refresh.Discovery
|
|
|
|
url string
|
|
|
|
client *http.Client
|
|
|
|
refreshInterval time.Duration
|
2021-07-01 16:02:13 +00:00
|
|
|
tgLastLength int
|
2023-10-23 13:55:36 +00:00
|
|
|
failuresCount prometheus.Counter
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewDiscovery returns a new HTTP discovery for the given config.
|
2023-10-23 13:55:36 +00:00
|
|
|
func NewDiscovery(conf *SDConfig, logger log.Logger, clientOpts []config.HTTPClientOption, reg prometheus.Registerer) (*Discovery, error) {
|
2021-06-11 16:04:45 +00:00
|
|
|
if logger == nil {
|
|
|
|
logger = log.NewNopLogger()
|
|
|
|
}
|
|
|
|
|
2022-03-24 22:16:59 +00:00
|
|
|
client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http", clientOpts...)
|
2021-06-11 16:04:45 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
client.Timeout = time.Duration(conf.RefreshInterval)
|
|
|
|
|
|
|
|
d := &Discovery{
|
|
|
|
url: conf.URL,
|
|
|
|
client: client,
|
|
|
|
refreshInterval: time.Duration(conf.RefreshInterval), // Stored to be sent as headers.
|
2023-10-23 13:55:36 +00:00
|
|
|
failuresCount: prometheus.NewCounter(
|
|
|
|
prometheus.CounterOpts{
|
|
|
|
Name: "prometheus_sd_http_failures_total",
|
|
|
|
Help: "Number of HTTP service discovery refresh failures.",
|
|
|
|
}),
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
d.Discovery = refresh.NewDiscovery(
|
2023-10-23 13:55:36 +00:00
|
|
|
refresh.Options{
|
|
|
|
Logger: logger,
|
|
|
|
Mech: "http",
|
|
|
|
Interval: time.Duration(conf.RefreshInterval),
|
|
|
|
RefreshF: d.Refresh,
|
|
|
|
Registry: reg,
|
|
|
|
Metrics: []prometheus.Collector{d.failuresCount},
|
|
|
|
},
|
2021-06-11 16:04:45 +00:00
|
|
|
)
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
2022-06-13 19:06:15 +00:00
|
|
|
func (d *Discovery) Refresh(ctx context.Context) ([]*targetgroup.Group, error) {
|
2021-06-11 16:04:45 +00:00
|
|
|
req, err := http.NewRequest("GET", d.url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set("User-Agent", userAgent)
|
|
|
|
req.Header.Set("Accept", "application/json")
|
2021-06-18 09:49:30 +00:00
|
|
|
req.Header.Set("X-Prometheus-Refresh-Interval-Seconds", strconv.FormatFloat(d.refreshInterval.Seconds(), 'f', -1, 64))
|
2021-06-11 16:04:45 +00:00
|
|
|
|
|
|
|
resp, err := d.client.Do(req.WithContext(ctx))
|
|
|
|
if err != nil {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2021-06-11 16:04:45 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer func() {
|
2022-04-27 09:24:36 +00:00
|
|
|
io.Copy(io.Discard, resp.Body)
|
2021-06-11 16:04:45 +00:00
|
|
|
resp.Body.Close()
|
|
|
|
}()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2022-06-03 11:47:14 +00:00
|
|
|
return nil, fmt.Errorf("server returned HTTP status %s", resp.Status)
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
2021-06-28 06:46:53 +00:00
|
|
|
if !matchContentType.MatchString(strings.TrimSpace(resp.Header.Get("Content-Type"))) {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2022-06-03 11:47:14 +00:00
|
|
|
return nil, fmt.Errorf("unsupported content type %q", resp.Header.Get("Content-Type"))
|
2021-06-11 16:04:45 +00:00
|
|
|
}
|
|
|
|
|
2022-04-27 09:24:36 +00:00
|
|
|
b, err := io.ReadAll(resp.Body)
|
2021-06-11 16:04:45 +00:00
|
|
|
if err != nil {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2021-06-11 16:04:45 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var targetGroups []*targetgroup.Group
|
|
|
|
|
|
|
|
if err := json.Unmarshal(b, &targetGroups); err != nil {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2021-06-11 16:04:45 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, tg := range targetGroups {
|
|
|
|
if tg == nil {
|
2023-10-23 13:55:36 +00:00
|
|
|
d.failuresCount.Inc()
|
2021-06-11 16:04:45 +00:00
|
|
|
err = errors.New("nil target group item found")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
tg.Source = urlSource(d.url, i)
|
|
|
|
if tg.Labels == nil {
|
|
|
|
tg.Labels = model.LabelSet{}
|
|
|
|
}
|
|
|
|
tg.Labels[httpSDURLLabel] = model.LabelValue(d.url)
|
|
|
|
}
|
|
|
|
|
2021-07-01 16:02:13 +00:00
|
|
|
// Generate empty updates for sources that disappeared.
|
|
|
|
l := len(targetGroups)
|
|
|
|
for i := l; i < d.tgLastLength; i++ {
|
|
|
|
targetGroups = append(targetGroups, &targetgroup.Group{Source: urlSource(d.url, i)})
|
|
|
|
}
|
|
|
|
d.tgLastLength = l
|
|
|
|
|
2021-06-11 16:04:45 +00:00
|
|
|
return targetGroups, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// urlSource returns a source ID for the i-th target group per URL.
|
|
|
|
func urlSource(url string, i int) string {
|
|
|
|
return fmt.Sprintf("%s:%d", url, i)
|
|
|
|
}
|