mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-05 17:27:51 +00:00
Refactoring external to leverage naming
This commit is contained in:
parent
3f7f249cb8
commit
ff409a0994
12 changed files with 269 additions and 1147 deletions
34
pkg/naming/errors.go
Normal file
34
pkg/naming/errors.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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 naming
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrUnsupportedOperator creates an error that represents the fact that we were requested to service a query that
|
||||
// Prometheus would be unable to support.
|
||||
ErrUnsupportedOperator = errors.New("operator not supported by prometheus")
|
||||
|
||||
// ErrMalformedQuery creates an error that represents the fact that we were requested to service a query
|
||||
// that was malformed in its operator/value combination.
|
||||
ErrMalformedQuery = errors.New("operator requires values")
|
||||
|
||||
// ErrQueryUnsupportedValues creates an error that represents an unsupported return value from the
|
||||
// specified query.
|
||||
ErrQueryUnsupportedValues = errors.New("operator does not support values")
|
||||
|
||||
// ErrLabelNotSpecified creates an error that represents the fact that we were requested to service a query
|
||||
// that was malformed in its label specification.
|
||||
ErrLabelNotSpecified = errors.New("label not specified")
|
||||
)
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"regexp"
|
||||
|
||||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
|
||||
|
|
@ -45,6 +46,9 @@ type MetricNamer interface {
|
|||
// QueryForSeries returns the query for a given series (not API metric name), with
|
||||
// the given namespace name (if relevant), resource, and resource names.
|
||||
QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error)
|
||||
// QueryForExternalSeries returns the query for a given series (not API metric name), with
|
||||
// the given namespace name (if relevant), resource, and resource names.
|
||||
QueryForExternalSeries(series string, namespace string, targetLabels labels.Selector) (prom.Selector, error)
|
||||
|
||||
ResourceConverter
|
||||
}
|
||||
|
|
@ -126,6 +130,12 @@ func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResourc
|
|||
return n.metricsQuery.Build(series, resource, namespace, nil, names...)
|
||||
}
|
||||
|
||||
func (n *metricNamer) QueryForExternalSeries(series string, namespace string, metricSelector labels.Selector) (prom.Selector, error) {
|
||||
//test := prom.Selector()
|
||||
//return test, nil
|
||||
return n.metricsQuery.BuildExternal(series, namespace, "", []string{}, metricSelector)
|
||||
}
|
||||
|
||||
func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) {
|
||||
matches := n.nameMatches.FindStringSubmatchIndex(series.Name)
|
||||
if matches == nil {
|
||||
|
|
@ -136,10 +146,10 @@ func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) {
|
|||
}
|
||||
|
||||
// NamersFromConfig produces a MetricNamer for each rule in the given config.
|
||||
func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMapper) ([]MetricNamer, error) {
|
||||
namers := make([]MetricNamer, len(cfg.Rules))
|
||||
func NamersFromConfig(cfg []config.DiscoveryRule, mapper apimeta.RESTMapper) ([]MetricNamer, error) {
|
||||
namers := make([]MetricNamer, len(cfg))
|
||||
|
||||
for i, rule := range cfg.Rules {
|
||||
for i, rule := range cfg {
|
||||
resConv, err := NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -205,38 +215,3 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap
|
|||
|
||||
return namers, nil
|
||||
}
|
||||
|
||||
// NewMetricNamer creates a MetricNamer capable of translating Prometheus series names
|
||||
// into custom metric names.
|
||||
func NewMetricNamer(mapping config.NameMapping) (MetricNamer, error) {
|
||||
var nameMatches *regexp.Regexp
|
||||
var err error
|
||||
if mapping.Matches != "" {
|
||||
nameMatches, err = regexp.Compile(mapping.Matches)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to compile series name match expression %q: %v", mapping.Matches, err)
|
||||
}
|
||||
} else {
|
||||
// this will always succeed
|
||||
nameMatches = regexp.MustCompile(".*")
|
||||
}
|
||||
nameAs := mapping.As
|
||||
if nameAs == "" {
|
||||
// check if we have an obvious default
|
||||
subexpNames := nameMatches.SubexpNames()
|
||||
if len(subexpNames) == 1 {
|
||||
// no capture groups, use the whole thing
|
||||
nameAs = "$0"
|
||||
} else if len(subexpNames) == 2 {
|
||||
// one capture group, use that
|
||||
nameAs = "$1"
|
||||
} else {
|
||||
return nil, fmt.Errorf("must specify an 'as' value for name matcher %q", mapping.Matches)
|
||||
}
|
||||
}
|
||||
|
||||
return &metricNamer{
|
||||
nameMatches: nameMatches,
|
||||
nameAs: nameAs,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ package naming
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
|
||||
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
|
||||
)
|
||||
|
|
@ -37,6 +40,7 @@ type MetricsQuery interface {
|
|||
// where we need to scope down more specifically than just the group-resource
|
||||
// (e.g. container metrics).
|
||||
Build(series string, groupRes schema.GroupResource, namespace string, extraGroupBy []string, resourceNames ...string) (prom.Selector, error)
|
||||
BuildExternal(seriesName string, namespace string, groupBy string, groupBySlice []string, metricSelector labels.Selector) (prom.Selector, error)
|
||||
}
|
||||
|
||||
// NewMetricsQuery constructs a new MetricsQuery by compiling the given Go template.
|
||||
|
|
@ -75,6 +79,12 @@ type queryTemplateArgs struct {
|
|||
GroupBySlice []string
|
||||
}
|
||||
|
||||
type queryPart struct {
|
||||
labelName string
|
||||
values []string
|
||||
operator selection.Operator
|
||||
}
|
||||
|
||||
func (q *metricsQuery) Build(series string, resource schema.GroupResource, namespace string, extraGroupBy []string, names ...string) (prom.Selector, error) {
|
||||
var exprs []string
|
||||
valuesByName := map[string][]string{}
|
||||
|
|
@ -123,3 +133,194 @@ func (q *metricsQuery) Build(series string, resource schema.GroupResource, names
|
|||
|
||||
return prom.Selector(queryBuff.String()), nil
|
||||
}
|
||||
|
||||
func (q *metricsQuery) BuildExternal(seriesName string, namespace string, groupBy string, groupBySlice []string, metricSelector labels.Selector) (prom.Selector, error) {
|
||||
queryParts := []queryPart{}
|
||||
|
||||
// Build up the query parts from the selector.
|
||||
queryParts = append(queryParts, q.createQueryPartsFromSelector(metricSelector)...)
|
||||
|
||||
// Convert our query parts into the types we need for our template.
|
||||
exprs, valuesByName, err := q.processQueryParts(queryParts)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
args := queryTemplateArgs{
|
||||
Series: seriesName,
|
||||
LabelMatchers: strings.Join(exprs, ","),
|
||||
LabelValuesByName: valuesByName,
|
||||
GroupBy: groupBy,
|
||||
GroupBySlice: groupBySlice,
|
||||
}
|
||||
|
||||
queryBuff := new(bytes.Buffer)
|
||||
if err := q.template.Execute(queryBuff, args); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if queryBuff.Len() == 0 {
|
||||
return "", fmt.Errorf("empty query produced by metrics query template")
|
||||
}
|
||||
|
||||
return prom.Selector(queryBuff.String()), nil
|
||||
}
|
||||
|
||||
func (q *metricsQuery) createQueryPartsFromSelector(metricSelector labels.Selector) []queryPart {
|
||||
requirements, _ := metricSelector.Requirements()
|
||||
|
||||
selectors := []queryPart{}
|
||||
for i := 0; i < len(requirements); i++ {
|
||||
selector := q.convertRequirement(requirements[i])
|
||||
|
||||
selectors = append(selectors, selector)
|
||||
}
|
||||
|
||||
return selectors
|
||||
}
|
||||
|
||||
func (q *metricsQuery) convertRequirement(requirement labels.Requirement) queryPart {
|
||||
labelName := requirement.Key()
|
||||
values := requirement.Values().List()
|
||||
|
||||
return queryPart{
|
||||
labelName: labelName,
|
||||
values: values,
|
||||
operator: requirement.Operator(),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *metricsQuery) processQueryParts(queryParts []queryPart) ([]string, map[string][]string, error) {
|
||||
// We've take the approach here that if we can't perfectly map their query into a Prometheus
|
||||
// query that we should abandon the effort completely.
|
||||
// The concern is that if we don't get a perfect match on their query parameters, the query result
|
||||
// might contain unexpected data that would cause them to take an erroneous action based on the result.
|
||||
|
||||
// Contains the expressions that we want to include as part of the query to Prometheus.
|
||||
// e.g. "namespace=my-namespace"
|
||||
// e.g. "some_label=some-value"
|
||||
var exprs []string
|
||||
|
||||
// Contains the list of label values we're targeting, by namespace.
|
||||
// e.g. "some_label" => ["value-one", "value-two"]
|
||||
valuesByName := map[string][]string{}
|
||||
|
||||
// Convert our query parts into template arguments.
|
||||
for _, qPart := range queryParts {
|
||||
// Be resilient against bad inputs.
|
||||
// We obviously can't generate label filters for these cases.
|
||||
fmt.Println("This is queryPart", qPart.labelName, qPart.operator, qPart.values)
|
||||
if qPart.labelName == "" {
|
||||
return nil, nil, ErrLabelNotSpecified
|
||||
}
|
||||
|
||||
if !q.operatorIsSupported(qPart.operator) {
|
||||
return nil, nil, ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
matcher, err := q.selectMatcher(qPart.operator, qPart.values)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
targetValue, err := q.selectTargetValue(qPart.operator, qPart.values)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
expression := matcher(qPart.labelName, targetValue)
|
||||
exprs = append(exprs, expression)
|
||||
valuesByName[qPart.labelName] = qPart.values
|
||||
}
|
||||
|
||||
return exprs, valuesByName, nil
|
||||
}
|
||||
|
||||
func (q *metricsQuery) selectMatcher(operator selection.Operator, values []string) (func(string, string) string, error) {
|
||||
|
||||
numValues := len(values)
|
||||
if numValues == 0 {
|
||||
switch operator {
|
||||
case selection.Exists:
|
||||
return prom.LabelNeq, nil
|
||||
case selection.DoesNotExist:
|
||||
return prom.LabelEq, nil
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn:
|
||||
return nil, ErrMalformedQuery
|
||||
}
|
||||
} else if numValues == 1 {
|
||||
switch operator {
|
||||
case selection.Equals, selection.DoubleEquals:
|
||||
return prom.LabelEq, nil
|
||||
case selection.NotEquals:
|
||||
return prom.LabelNeq, nil
|
||||
case selection.In, selection.Exists:
|
||||
return prom.LabelMatches, nil
|
||||
case selection.DoesNotExist, selection.NotIn:
|
||||
return prom.LabelNotMatches, nil
|
||||
}
|
||||
} else {
|
||||
// Since labels can only have one value, providing multiple
|
||||
// values results in a regex match, even if that's not what the user
|
||||
// asked for.
|
||||
switch operator {
|
||||
case selection.Equals, selection.DoubleEquals, selection.In, selection.Exists:
|
||||
return prom.LabelMatches, nil
|
||||
case selection.NotEquals, selection.DoesNotExist, selection.NotIn:
|
||||
return prom.LabelNotMatches, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("operator not supported by query builder")
|
||||
}
|
||||
|
||||
func (q *metricsQuery) selectTargetValue(operator selection.Operator, values []string) (string, error) {
|
||||
numValues := len(values)
|
||||
if numValues == 0 {
|
||||
switch operator {
|
||||
case selection.Exists, selection.DoesNotExist:
|
||||
// Return an empty string when values are equal to 0
|
||||
// When the operator is LabelNotMatches this will select series without the label
|
||||
// or with the label but a value of "".
|
||||
// When the operator is LabelMatches this will select series with the label
|
||||
// whose value is NOT "".
|
||||
return "", nil
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn:
|
||||
return "", ErrMalformedQuery
|
||||
}
|
||||
} else if numValues == 1 {
|
||||
switch operator {
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn:
|
||||
// Pass the value through as-is.
|
||||
// It's somewhat strange to do this for both the regex and equality
|
||||
// operators, but if we do it this way it gives the user a little more control.
|
||||
// They might choose to send an "IN" request and give a list of static values
|
||||
// or they could send a single value that's a regex, giving them a passthrough
|
||||
// for their label selector.
|
||||
return values[0], nil
|
||||
case selection.Exists, selection.DoesNotExist:
|
||||
return "", ErrQueryUnsupportedValues
|
||||
}
|
||||
} else {
|
||||
switch operator {
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn:
|
||||
// Pass the value through as-is.
|
||||
// It's somewhat strange to do this for both the regex and equality
|
||||
// operators, but if we do it this way it gives the user a little more control.
|
||||
// They might choose to send an "IN" request and give a list of static values
|
||||
// or they could send a single value that's a regex, giving them a passthrough
|
||||
// for their label selector.
|
||||
return strings.Join(values, "|"), nil
|
||||
case selection.Exists, selection.DoesNotExist:
|
||||
return "", ErrQueryUnsupportedValues
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("operator not supported by query builder")
|
||||
}
|
||||
|
||||
func (q *metricsQuery) operatorIsSupported(operator selection.Operator) bool {
|
||||
return operator != selection.GreaterThan && operator != selection.LessThan
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue