From 74c0c53e4fb1eec12bcdf32a0cf145c5c015969f Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 17 Aug 2018 11:49:30 -0400 Subject: [PATCH] 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. --- pkg/custom-provider/metric_namer.go | 89 +++++------------------ pkg/naming/metrics_query.go | 109 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 pkg/naming/metrics_query.go diff --git a/pkg/custom-provider/metric_namer.go b/pkg/custom-provider/metric_namer.go index 44aff73f..2a3fe2f8 100644 --- a/pkg/custom-provider/metric_namer.go +++ b/pkg/custom-provider/metric_namer.go @@ -1,11 +1,9 @@ package provider import ( - "bytes" "fmt" "regexp" "strings" - "text/template" apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" @@ -84,24 +82,16 @@ func (m *reMatcher) Matches(val string) bool { } type metricNamer struct { - seriesQuery prom.Selector - metricsQueryTemplate *template.Template - nameMatches *regexp.Regexp - nameAs string - seriesMatchers []*reMatcher + seriesQuery prom.Selector + metricsQuery naming.MetricsQuery + nameMatches *regexp.Regexp + nameAs string + seriesMatchers []*reMatcher naming.ResourceConverter } // 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 { if len(n.seriesMatchers) == 0 { return initialSeries @@ -122,48 +112,7 @@ SeriesLoop: } func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) { - var exprs []string - 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 + return n.metricsQuery.Build(series, resource, namespace, nil, names...) } 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)) 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 { - 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)) @@ -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{ - seriesQuery: prom.Selector(rule.SeriesQuery), - metricsQueryTemplate: metricsQueryTemplate, - nameMatches: nameMatches, - nameAs: nameAs, - seriesMatchers: seriesMatchers, - ResourceConverter: resConv, + seriesQuery: prom.Selector(rule.SeriesQuery), + metricsQuery: metricsQuery, + nameMatches: nameMatches, + nameAs: nameAs, + seriesMatchers: seriesMatchers, + ResourceConverter: resConv, } namers[i] = namer diff --git a/pkg/naming/metrics_query.go b/pkg/naming/metrics_query.go new file mode 100644 index 00000000..2ca3e462 --- /dev/null +++ b/pkg/naming/metrics_query.go @@ -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 +}