Refactor metrics query building to interface

This moves the metrics query building to a separate interface in the
naming package so that it can be used across providers.
This commit is contained in:
Solly Ross 2018-08-17 11:49:30 -04:00
parent 7dd9e94aea
commit 74c0c53e4f
2 changed files with 128 additions and 70 deletions

View file

@ -1,11 +1,9 @@
package provider package provider
import ( import (
"bytes"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"text/template"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -84,24 +82,16 @@ func (m *reMatcher) Matches(val string) bool {
} }
type metricNamer struct { type metricNamer struct {
seriesQuery prom.Selector seriesQuery prom.Selector
metricsQueryTemplate *template.Template metricsQuery naming.MetricsQuery
nameMatches *regexp.Regexp nameMatches *regexp.Regexp
nameAs string nameAs string
seriesMatchers []*reMatcher seriesMatchers []*reMatcher
naming.ResourceConverter naming.ResourceConverter
} }
// queryTemplateArgs are the arguments for the metrics query template. // queryTemplateArgs are the arguments for the metrics query template.
type queryTemplateArgs struct {
Series string
LabelMatchers string
LabelValuesByName map[string][]string
GroupBy string
GroupBySlice []string
}
func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series { func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series {
if len(n.seriesMatchers) == 0 { if len(n.seriesMatchers) == 0 {
return initialSeries return initialSeries
@ -122,48 +112,7 @@ SeriesLoop:
} }
func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) { func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) {
var exprs []string return n.metricsQuery.Build(series, resource, namespace, nil, names...)
valuesByName := map[string][]string{}
if namespace != "" {
namespaceLbl, err := n.LabelForResource(nsGroupResource)
if err != nil {
return "", err
}
exprs = append(exprs, prom.LabelEq(string(namespaceLbl), namespace))
valuesByName[string(namespaceLbl)] = []string{namespace}
}
resourceLbl, err := n.LabelForResource(resource)
if err != nil {
return "", err
}
matcher := prom.LabelEq
targetValue := names[0]
if len(names) > 1 {
matcher = prom.LabelMatches
targetValue = strings.Join(names, "|")
}
exprs = append(exprs, matcher(string(resourceLbl), targetValue))
valuesByName[string(resourceLbl)] = names
args := queryTemplateArgs{
Series: series,
LabelMatchers: strings.Join(exprs, ","),
LabelValuesByName: valuesByName,
GroupBy: string(resourceLbl),
GroupBySlice: []string{string(resourceLbl)},
}
queryBuff := new(bytes.Buffer)
if err := n.metricsQueryTemplate.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 (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) { func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) {
@ -180,9 +129,14 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap
namers := make([]MetricNamer, len(cfg.Rules)) namers := make([]MetricNamer, len(cfg.Rules))
for i, rule := range cfg.Rules { for i, rule := range cfg.Rules {
metricsQueryTemplate, err := template.New("metrics-query").Delims("<<", ">>").Parse(rule.MetricsQuery) resConv, err := naming.NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse metrics query template %q associated with series query %q: %v", rule.MetricsQuery, rule.SeriesQuery, err) return nil, err
}
metricsQuery, err := naming.NewMetricsQuery(rule.MetricsQuery, resConv)
if err != nil {
return nil, fmt.Errorf("unable to construct metrics query associated with series query %q: %v", rule.SeriesQuery, err)
} }
seriesMatchers := make([]*reMatcher, len(rule.SeriesFilters)) seriesMatchers := make([]*reMatcher, len(rule.SeriesFilters))
@ -226,18 +180,13 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap
} }
} }
resConv, err := naming.NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper)
if err != nil {
return nil, err
}
namer := &metricNamer{ namer := &metricNamer{
seriesQuery: prom.Selector(rule.SeriesQuery), seriesQuery: prom.Selector(rule.SeriesQuery),
metricsQueryTemplate: metricsQueryTemplate, metricsQuery: metricsQuery,
nameMatches: nameMatches, nameMatches: nameMatches,
nameAs: nameAs, nameAs: nameAs,
seriesMatchers: seriesMatchers, seriesMatchers: seriesMatchers,
ResourceConverter: resConv, ResourceConverter: resConv,
} }
namers[i] = namer namers[i] = namer

109
pkg/naming/metrics_query.go Normal file
View file

@ -0,0 +1,109 @@
package naming
import (
"bytes"
"fmt"
"strings"
"text/template"
"k8s.io/apimachinery/pkg/runtime/schema"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
)
// MetricsQuery represents a compiled metrics query for some set of
// series that can be converted into an series of Prometheus expressions to
// be passed to a client.
type MetricsQuery interface {
// Build constructs Prometheus expressions to represent this query
// over the given group-resource. If namespace is empty, the resource
// is considered to be root-scoped. extraGroupBy may be used for cases
// 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)
}
// NewMetricsQuery constructs a new MetricsQuery by compiling the given Go template.
// The delimiters on the template are `<<` and `>>`, and it may use the following fields:
// - Series: the series in question
// - LabelMatchers: a pre-stringified form of the label matchers for the resources in the query
// - LabelMatchersByName: the raw map-form of the above matchers
// - GroupBy: the group-by clause to use for the resources in the query (stringified)
// - GroupBySlice: the raw slice form of the above group-by clause
func NewMetricsQuery(queryTemplate string, resourceConverter ResourceConverter) (MetricsQuery, error) {
templ, err := template.New("metrics-query").Delims("<<", ">>").Parse(queryTemplate)
if err != nil {
return nil, fmt.Errorf("unable to parse metrics query template %q: %v", queryTemplate, err)
}
return &metricsQuery{
resConverter: resourceConverter,
template: templ,
}, nil
}
// metricsQuery is a MetricsQuery based on a compiled Go text template.
// with the delimiters as `<<` and `>>`, and the arguments found in
// queryTemplateArgs.
type metricsQuery struct {
resConverter ResourceConverter
template *template.Template
}
// queryTemplateArgs contains the arguments for the template used in metricsQuery.
type queryTemplateArgs struct {
Series string
LabelMatchers string
LabelValuesByName map[string][]string
GroupBy string
GroupBySlice []string
}
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{}
if namespace != "" {
namespaceLbl, err := q.resConverter.LabelForResource(nsGroupResource)
if err != nil {
return "", err
}
exprs = append(exprs, prom.LabelEq(string(namespaceLbl), namespace))
valuesByName[string(namespaceLbl)] = []string{namespace}
}
resourceLbl, err := q.resConverter.LabelForResource(resource)
if err != nil {
return "", err
}
matcher := prom.LabelEq
targetValue := names[0]
if len(names) > 1 {
matcher = prom.LabelMatches
targetValue = strings.Join(names, "|")
}
exprs = append(exprs, matcher(string(resourceLbl), targetValue))
valuesByName[string(resourceLbl)] = names
groupBy := make([]string, 0, len(extraGroupBy)+1)
groupBy = append(groupBy, string(resourceLbl))
groupBy = append(groupBy, extraGroupBy...)
args := queryTemplateArgs{
Series: series,
LabelMatchers: strings.Join(exprs, ","),
LabelValuesByName: valuesByName,
GroupBy: strings.Join(groupBy, ","),
GroupBySlice: groupBy,
}
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
}