mirror of https://github.com/prometheus/prometheus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
7.7 KiB
268 lines
7.7 KiB
// 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 puppetdb |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"net" |
|
"net/http" |
|
"net/url" |
|
"path" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/go-kit/log" |
|
"github.com/grafana/regexp" |
|
"github.com/prometheus/client_golang/prometheus" |
|
"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" |
|
) |
|
|
|
const ( |
|
pdbLabel = model.MetaLabelPrefix + "puppetdb_" |
|
pdbLabelQuery = pdbLabel + "query" |
|
pdbLabelCertname = pdbLabel + "certname" |
|
pdbLabelResource = pdbLabel + "resource" |
|
pdbLabelType = pdbLabel + "type" |
|
pdbLabelTitle = pdbLabel + "title" |
|
pdbLabelExported = pdbLabel + "exported" |
|
pdbLabelTags = pdbLabel + "tags" |
|
pdbLabelFile = pdbLabel + "file" |
|
pdbLabelEnvironment = pdbLabel + "environment" |
|
pdbLabelParameter = pdbLabel + "parameter_" |
|
separator = "," |
|
) |
|
|
|
var ( |
|
// DefaultSDConfig is the default PuppetDB SD configuration. |
|
DefaultSDConfig = SDConfig{ |
|
RefreshInterval: model.Duration(60 * time.Second), |
|
Port: 80, |
|
HTTPClientConfig: config.DefaultHTTPClientConfig, |
|
} |
|
matchContentType = regexp.MustCompile(`^(?i:application\/json(;\s*charset=("utf-8"|utf-8))?)$`) |
|
userAgent = fmt.Sprintf("Prometheus/%s", version.Version) |
|
) |
|
|
|
func init() { |
|
discovery.RegisterConfig(&SDConfig{}) |
|
} |
|
|
|
// SDConfig is the configuration for PuppetDB based discovery. |
|
type SDConfig struct { |
|
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` |
|
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` |
|
URL string `yaml:"url"` |
|
Query string `yaml:"query"` |
|
IncludeParameters bool `yaml:"include_parameters"` |
|
Port int `yaml:"port"` |
|
} |
|
|
|
// NewDiscovererMetrics implements discovery.Config. |
|
func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics { |
|
return &puppetdbMetrics{ |
|
refreshMetrics: rmi, |
|
} |
|
} |
|
|
|
// Name returns the name of the Config. |
|
func (*SDConfig) Name() string { return "puppetdb" } |
|
|
|
// NewDiscoverer returns a Discoverer for the Config. |
|
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { |
|
return NewDiscovery(c, opts.Logger, opts.Metrics) |
|
} |
|
|
|
// 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") |
|
} |
|
if c.Query == "" { |
|
return fmt.Errorf("query missing") |
|
} |
|
return c.HTTPClientConfig.Validate() |
|
} |
|
|
|
// Discovery provides service discovery functionality based |
|
// on PuppetDB resources. |
|
type Discovery struct { |
|
*refresh.Discovery |
|
url string |
|
query string |
|
port int |
|
includeParameters bool |
|
client *http.Client |
|
} |
|
|
|
// NewDiscovery returns a new PuppetDB discovery for the given config. |
|
func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) { |
|
m, ok := metrics.(*puppetdbMetrics) |
|
if !ok { |
|
return nil, fmt.Errorf("invalid discovery metrics type") |
|
} |
|
|
|
if logger == nil { |
|
logger = log.NewNopLogger() |
|
} |
|
|
|
client, err := config.NewClientFromConfig(conf.HTTPClientConfig, "http") |
|
if err != nil { |
|
return nil, err |
|
} |
|
client.Timeout = time.Duration(conf.RefreshInterval) |
|
|
|
u, err := url.Parse(conf.URL) |
|
if err != nil { |
|
return nil, err |
|
} |
|
u.Path = path.Join(u.Path, "pdb/query/v4") |
|
|
|
d := &Discovery{ |
|
url: u.String(), |
|
port: conf.Port, |
|
query: conf.Query, |
|
includeParameters: conf.IncludeParameters, |
|
client: client, |
|
} |
|
|
|
d.Discovery = refresh.NewDiscovery( |
|
refresh.Options{ |
|
Logger: logger, |
|
Mech: "puppetdb", |
|
Interval: time.Duration(conf.RefreshInterval), |
|
RefreshF: d.refresh, |
|
MetricsInstantiator: m.refreshMetrics, |
|
}, |
|
) |
|
return d, nil |
|
} |
|
|
|
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { |
|
body := struct { |
|
Query string `json:"query"` |
|
}{d.query} |
|
bodyBytes, err := json.Marshal(body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
req, err := http.NewRequest(http.MethodPost, d.url, bytes.NewBuffer(bodyBytes)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
req.Header.Set("User-Agent", userAgent) |
|
req.Header.Set("Accept", "application/json") |
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
resp, err := d.client.Do(req.WithContext(ctx)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer func() { |
|
io.Copy(io.Discard, resp.Body) |
|
resp.Body.Close() |
|
}() |
|
|
|
if resp.StatusCode != http.StatusOK { |
|
return nil, fmt.Errorf("server returned HTTP status %s", resp.Status) |
|
} |
|
|
|
if ct := resp.Header.Get("Content-Type"); !matchContentType.MatchString(ct) { |
|
return nil, fmt.Errorf("unsupported content type %s", resp.Header.Get("Content-Type")) |
|
} |
|
|
|
b, err := io.ReadAll(resp.Body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var resources []Resource |
|
|
|
if err := json.Unmarshal(b, &resources); err != nil { |
|
return nil, err |
|
} |
|
|
|
tg := &targetgroup.Group{ |
|
// Use a pseudo-URL as source. |
|
Source: d.url + "?query=" + d.query, |
|
} |
|
|
|
for _, resource := range resources { |
|
labels := model.LabelSet{ |
|
pdbLabelQuery: model.LabelValue(d.query), |
|
pdbLabelCertname: model.LabelValue(resource.Certname), |
|
pdbLabelResource: model.LabelValue(resource.Resource), |
|
pdbLabelType: model.LabelValue(resource.Type), |
|
pdbLabelTitle: model.LabelValue(resource.Title), |
|
pdbLabelExported: model.LabelValue(strconv.FormatBool(resource.Exported)), |
|
pdbLabelFile: model.LabelValue(resource.File), |
|
pdbLabelEnvironment: model.LabelValue(resource.Environment), |
|
} |
|
|
|
addr := net.JoinHostPort(resource.Certname, strconv.FormatUint(uint64(d.port), 10)) |
|
labels[model.AddressLabel] = model.LabelValue(addr) |
|
|
|
if len(resource.Tags) > 0 { |
|
// We surround the separated list with the separator as well. This way regular expressions |
|
// in relabeling rules don't have to consider tag positions. |
|
tags := separator + strings.Join(resource.Tags, separator) + separator |
|
labels[pdbLabelTags] = model.LabelValue(tags) |
|
} |
|
|
|
// Parameters are not included by default. This should only be enabled |
|
// on select resources as it might expose secrets on the Prometheus UI |
|
// for certain resources. |
|
if d.includeParameters { |
|
for k, v := range resource.Parameters.toLabels() { |
|
labels[pdbLabelParameter+k] = v |
|
} |
|
} |
|
|
|
tg.Targets = append(tg.Targets, labels) |
|
} |
|
|
|
return []*targetgroup.Group{tg}, nil |
|
}
|
|
|