diff --git a/pkg/external-provider/errors.go b/pkg/external-provider/errors.go index 1459dd98..970960e8 100644 --- a/pkg/external-provider/errors.go +++ b/pkg/external-provider/errors.go @@ -2,26 +2,20 @@ package provider import "errors" -// NewOperatorNotSupportedByPrometheusError creates an error that represents the fact that we were requested to service a query that -// Prometheus would be unable to support. -func NewOperatorNotSupportedByPrometheusError() error { - return errors.New("operator not supported by prometheus") -} +var ( + // ErrorNewOperatorNotSupportedByPrometheus creates an error that represents the fact that we were requested to service a query that + // Prometheus would be unable to support. + ErrorNewOperatorNotSupportedByPrometheus = errors.New("operator not supported by prometheus") -// NewOperatorRequiresValuesError creates an error that represents the fact that we were requested to service a query -// that was malformed in its operator/value combination. -func NewOperatorRequiresValuesError() error { - return errors.New("operator requires values") -} + // ErrorNewOperatorRequiresValues creates an error that represents the fact that we were requested to service a query + // that was malformed in its operator/value combination. + ErrorNewOperatorRequiresValues = errors.New("operator requires values") -// NewOperatorDoesNotSupportValuesError creates an error that represents the fact that we were requested to service a query -// that was malformed in its operator/value combination. -func NewOperatorDoesNotSupportValuesError() error { - return errors.New("operator does not support values") -} + // ErrorNewOperatorDoesNotSupportValues creates an error that represents the fact that we were requested to service a query + // that was malformed in its operator/value combination. + ErrorNewOperatorDoesNotSupportValues = errors.New("operator does not support values") -// NewLabelNotSpecifiedError creates an error that represents the fact that we were requested to service a query -// that was malformed in its label specification. -func NewLabelNotSpecifiedError() error { - return errors.New("label not specified") -} + // ErrorNewLabelNotSpecified creates an error that represents the fact that we were requested to service a query + // that was malformed in its label specification. + ErrorNewLabelNotSpecified = errors.New("label not specified") +) diff --git a/pkg/external-provider/external_series_registry.go b/pkg/external-provider/external_series_registry.go index e34769b7..b6586aaf 100644 --- a/pkg/external-provider/external_series_registry.go +++ b/pkg/external-provider/external_series_registry.go @@ -48,12 +48,11 @@ func NewExternalSeriesRegistry(lister MetricListerWithNotification, mapper apime } func (r *externalSeriesRegistry) filterAndStoreMetrics(result MetricUpdateResult) { - newSeriesSlices := result.series converters := result.converters if len(newSeriesSlices) != len(converters) { - glog.Errorf("need one set of series per converter") + glog.Fatal("need one set of series per converter") } apiMetricsCache := make([]provider.ExternalMetricInfo, 0) rawMetricsCache := make(map[string]SeriesConverter) @@ -84,6 +83,7 @@ func (r *externalSeriesRegistry) filterAndStoreMetrics(result MetricUpdateResult r.metrics = apiMetricsCache r.rawMetrics = rawMetricsCache + } func (r *externalSeriesRegistry) ListAllMetrics() []provider.ExternalMetricInfo { diff --git a/pkg/external-provider/metric_converter.go b/pkg/external-provider/metric_converter.go index 44238f1b..c9cc3c2f 100644 --- a/pkg/external-provider/metric_converter.go +++ b/pkg/external-provider/metric_converter.go @@ -85,7 +85,6 @@ func (c *metricConverter) convertVector(queryResult prom.QueryResult) (*external } for _, val := range toConvert { - singleMetric, err := c.convertSample(val) if err != nil { diff --git a/pkg/external-provider/metric_namer.go b/pkg/external-provider/metric_namer.go deleted file mode 100644 index 64053344..00000000 --- a/pkg/external-provider/metric_namer.go +++ /dev/null @@ -1,159 +0,0 @@ -package provider - -import ( - "fmt" - "regexp" - "strings" - - "k8s.io/apimachinery/pkg/runtime/schema" - - prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" - "github.com/directxman12/k8s-prometheus-adapter/pkg/config" - "github.com/directxman12/k8s-prometheus-adapter/pkg/naming" -) - -var nsGroupResource = schema.GroupResource{Resource: "namespaces"} -var groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_") - -// MetricNamer knows how to convert Prometheus series names and label names to -// metrics API resources, and vice-versa. MetricNamers should be safe to access -// concurrently. Returned group-resources are "normalized" as per the -// MetricInfo#Normalized method. Group-resources passed as arguments must -// themselves be normalized. -type MetricNamer interface { - // Selector produces the appropriate Prometheus series selector to match all - // series handlable by this namer. - Selector() prom.Selector - // FilterSeries checks to see which of the given series match any additional - // constrains beyond the series query. It's assumed that the series given - // already matche the series query. - FilterSeries(series []prom.Series) []prom.Series - // MetricNameForSeries returns the name (as presented in the API) for a given series. - MetricNameForSeries(series prom.Series) (string, error) - // 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) - - naming.ResourceConverter -} - -func (n *metricNamer) Selector() prom.Selector { - return n.seriesQuery -} - -// reMatcher either positively or negatively matches a regex -type reMatcher struct { - regex *regexp.Regexp - positive bool -} - -func newReMatcher(cfg config.RegexFilter) (*reMatcher, error) { - if cfg.Is != "" && cfg.IsNot != "" { - return nil, fmt.Errorf("cannot have both an `is` (%q) and `isNot` (%q) expression in a single filter", cfg.Is, cfg.IsNot) - } - if cfg.Is == "" && cfg.IsNot == "" { - return nil, fmt.Errorf("must have either an `is` or `isNot` expression in a filter") - } - - var positive bool - var regexRaw string - if cfg.Is != "" { - positive = true - regexRaw = cfg.Is - } else { - positive = false - regexRaw = cfg.IsNot - } - - regex, err := regexp.Compile(regexRaw) - if err != nil { - return nil, fmt.Errorf("unable to compile series filter %q: %v", regexRaw, err) - } - - return &reMatcher{ - regex: regex, - positive: positive, - }, nil -} - -func (m *reMatcher) Matches(val string) bool { - return m.regex.MatchString(val) == m.positive -} - -type metricNamer struct { - seriesQuery prom.Selector - metricsQuery naming.MetricsQuery - nameMatches *regexp.Regexp - nameAs string - seriesMatchers []*reMatcher - - naming.ResourceConverter -} - -// queryTemplateArgs are the arguments for the metrics query template. -func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series { - if len(n.seriesMatchers) == 0 { - return initialSeries - } - - finalSeries := make([]prom.Series, 0, len(initialSeries)) -SeriesLoop: - for _, series := range initialSeries { - for _, matcher := range n.seriesMatchers { - if !matcher.Matches(series.Name) { - continue SeriesLoop - } - } - finalSeries = append(finalSeries, series) - } - - return finalSeries -} - -func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) { - return n.metricsQuery.Build(series, resource, namespace, nil, names...) -} - -func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) { - matches := n.nameMatches.FindStringSubmatchIndex(series.Name) - if matches == nil { - return "", fmt.Errorf("series name %q did not match expected pattern %q", series.Name, n.nameMatches.String()) - } - outNameBytes := n.nameMatches.ExpandString(nil, n.nameAs, series.Name, matches) - return string(outNameBytes), 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 -} diff --git a/pkg/external-provider/provider.go b/pkg/external-provider/provider.go index a800f4c7..74a13e95 100644 --- a/pkg/external-provider/provider.go +++ b/pkg/external-provider/provider.go @@ -18,6 +18,7 @@ import ( "k8s.io/metrics/pkg/apis/external_metrics" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" + "github.com/directxman12/k8s-prometheus-adapter/pkg/naming" ) // TODO: Make sure everything has the proper licensing disclosure at the top. @@ -56,7 +57,7 @@ func (p *externalPrometheusProvider) ListAllExternalMetrics() []provider.Externa func (p *externalPrometheusProvider) selectGroupResource(namespace string) schema.GroupResource { if namespace == "default" { - return nsGroupResource + return naming.NsGroupResource } return schema.GroupResource{ diff --git a/pkg/external-provider/query_builder.go b/pkg/external-provider/query_builder.go index bb9b6d9c..0161ba6f 100644 --- a/pkg/external-provider/query_builder.go +++ b/pkg/external-provider/query_builder.go @@ -92,11 +92,11 @@ func (n *queryBuilder) processQueryParts(queryParts []queryPart) ([]string, map[ // 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, NewLabelNotSpecifiedError() + return nil, nil, ErrorNewLabelNotSpecified } if !n.operatorIsSupported(qPart.operator) { - return nil, nil, NewOperatorNotSupportedByPrometheusError() + return nil, nil, ErrorNewOperatorNotSupportedByPrometheus } matcher, err := n.selectMatcher(qPart.operator, qPart.values) @@ -128,7 +128,7 @@ func (n *queryBuilder) selectMatcher(operator selection.Operator, values []strin case selection.DoesNotExist: return prom.LabelEq, nil case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: - return nil, NewOperatorRequiresValuesError() + return nil, ErrorNewOperatorRequiresValues } } else if numValues == 1 { switch operator { @@ -168,7 +168,7 @@ func (n *queryBuilder) selectTargetValue(operator selection.Operator, values []s // whose value is NOT "". return "", nil case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.In, selection.NotIn: - return "", NewOperatorRequiresValuesError() + return "", ErrorNewOperatorRequiresValues } } else if numValues == 1 { switch operator { @@ -194,7 +194,7 @@ func (n *queryBuilder) selectTargetValue(operator selection.Operator, values []s // for their label selector. return strings.Join(values, "|"), nil case selection.Exists, selection.DoesNotExist: - return "", NewOperatorDoesNotSupportValuesError() + return "", ErrorNewOperatorDoesNotSupportValues } } diff --git a/pkg/external-provider/series_converter.go b/pkg/external-provider/series_converter.go index f4fe222c..fe20d1ee 100644 --- a/pkg/external-provider/series_converter.go +++ b/pkg/external-provider/series_converter.go @@ -49,7 +49,7 @@ type seriesConverter struct { resourceConverter naming.ResourceConverter queryBuilder QueryBuilder seriesFilterer SeriesFilterer - metricNamer MetricNamer + metricNamer naming.MetricNamer mapper apimeta.RESTMapper } @@ -122,7 +122,7 @@ func (c *seriesConverter) buildNamespaceQueryPartForSeries(namespace string) (qu // If we've been given a namespace, then we need to set up // the label requirements to target that namespace. if namespace != "default" { - namespaceLbl, err := c.resourceConverter.LabelForResource(nsGroupResource) + namespaceLbl, err := c.resourceConverter.LabelForResource(naming.NsGroupResource) if err != nil { return result, err } @@ -226,7 +226,7 @@ func converterFromRule(rule config.DiscoveryRule, mapper apimeta.RESTMapper) (Se } } - metricNamer, err := NewMetricNamer(rule.Name) + metricNamer, err := naming.NewMetricNamer(rule.Name) if err != nil { return nil, fmt.Errorf("unable to create a MetricNamer associated with series query %q: %v", rule.SeriesQuery, err) } @@ -242,7 +242,7 @@ func converterFromRule(rule config.DiscoveryRule, mapper apimeta.RESTMapper) (Se } func (c *seriesConverter) buildNamespaceQueryPartForExternalSeries(namespace string) (queryPart, error) { - namespaceLbl, _ := c.metricNamer.LabelForResource(nsGroupResource) + namespaceLbl, _ := c.metricNamer.LabelForResource(naming.NsGroupResource) return queryPart{ labelName: string(namespaceLbl), diff --git a/pkg/external-provider/series_filterer.go b/pkg/external-provider/series_filterer.go index 153dd6eb..e75c883b 100644 --- a/pkg/external-provider/series_filterer.go +++ b/pkg/external-provider/series_filterer.go @@ -5,6 +5,7 @@ import ( prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" "github.com/directxman12/k8s-prometheus-adapter/pkg/config" + "github.com/directxman12/k8s-prometheus-adapter/pkg/naming" ) // SeriesFilterer provides functions for filtering collections of Prometheus series @@ -15,15 +16,15 @@ type SeriesFilterer interface { } type seriesFilterer struct { - seriesMatchers []*reMatcher + seriesMatchers []*naming.ReMatcher } // NewSeriesFilterer creates a SeriesFilterer that will remove any series that do not // meet the requirements of the provided RegexFilter(s). func NewSeriesFilterer(filters []config.RegexFilter) (SeriesFilterer, error) { - seriesMatchers := make([]*reMatcher, len(filters)) + seriesMatchers := make([]*naming.ReMatcher, len(filters)) for i, filterRaw := range filters { - matcher, err := newReMatcher(filterRaw) + matcher, err := naming.NewReMatcher(filterRaw) if err != nil { return nil, fmt.Errorf("unable to generate series name filter: %v", err) } @@ -36,7 +37,7 @@ func NewSeriesFilterer(filters []config.RegexFilter) (SeriesFilterer, error) { } func (n *seriesFilterer) AddRequirement(filterRaw config.RegexFilter) error { - matcher, err := newReMatcher(filterRaw) + matcher, err := naming.NewReMatcher(filterRaw) if err != nil { return fmt.Errorf("unable to generate series name filter: %v", err) } diff --git a/pkg/custom-provider/metric_namer.go b/pkg/naming/metric_namer.go similarity index 75% rename from pkg/custom-provider/metric_namer.go rename to pkg/naming/metric_namer.go index 2c00037e..a2a8b434 100644 --- a/pkg/custom-provider/metric_namer.go +++ b/pkg/naming/metric_namer.go @@ -1,21 +1,16 @@ -package provider +package naming import ( "fmt" "regexp" - "strings" apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" "github.com/directxman12/k8s-prometheus-adapter/pkg/config" - "github.com/directxman12/k8s-prometheus-adapter/pkg/naming" ) -var nsGroupResource = schema.GroupResource{Resource: "namespaces"} -var groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_") - // MetricNamer knows how to convert Prometheus series names and label names to // metrics API resources, and vice-versa. MetricNamers should be safe to access // concurrently. Returned group-resources are "normalized" as per the @@ -35,20 +30,20 @@ type MetricNamer interface { // the given namespace name (if relevant), resource, and resource names. QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) - naming.ResourceConverter + ResourceConverter } -func (r *metricNamer) Selector() prom.Selector { - return r.seriesQuery +func (n *metricNamer) Selector() prom.Selector { + return n.seriesQuery } -// reMatcher either positively or negatively matches a regex -type reMatcher struct { +// ReMatcher either positively or negatively matches a regex +type ReMatcher struct { regex *regexp.Regexp positive bool } -func newReMatcher(cfg config.RegexFilter) (*reMatcher, error) { +func NewReMatcher(cfg config.RegexFilter) (*ReMatcher, error) { if cfg.Is != "" && cfg.IsNot != "" { return nil, fmt.Errorf("cannot have both an `is` (%q) and `isNot` (%q) expression in a single filter", cfg.Is, cfg.IsNot) } @@ -71,24 +66,24 @@ func newReMatcher(cfg config.RegexFilter) (*reMatcher, error) { return nil, fmt.Errorf("unable to compile series filter %q: %v", regexRaw, err) } - return &reMatcher{ + return &ReMatcher{ regex: regex, positive: positive, }, nil } -func (m *reMatcher) Matches(val string) bool { +func (m *ReMatcher) Matches(val string) bool { return m.regex.MatchString(val) == m.positive } type metricNamer struct { seriesQuery prom.Selector - metricsQuery naming.MetricsQuery + metricsQuery MetricsQuery nameMatches *regexp.Regexp nameAs string - seriesMatchers []*reMatcher + seriesMatchers []*ReMatcher - naming.ResourceConverter + ResourceConverter } // queryTemplateArgs are the arguments for the metrics query template. @@ -129,26 +124,26 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap namers := make([]MetricNamer, len(cfg.Rules)) for i, rule := range cfg.Rules { - resConv, err := naming.NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper) + resConv, err := NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper) if err != nil { return nil, err } - metricsQuery, err := naming.NewMetricsQuery(rule.MetricsQuery, resConv) + metricsQuery, err := 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)) for i, filterRaw := range rule.SeriesFilters { - matcher, err := newReMatcher(filterRaw) + matcher, err := NewReMatcher(filterRaw) if err != nil { return nil, fmt.Errorf("unable to generate series name filter associated with series query %q: %v", rule.SeriesQuery, err) } seriesMatchers[i] = matcher } if rule.Name.Matches != "" { - matcher, err := newReMatcher(config.RegexFilter{Is: rule.Name.Matches}) + matcher, err := NewReMatcher(config.RegexFilter{Is: rule.Name.Matches}) if err != nil { return nil, fmt.Errorf("unable to generate series name filter from name rules associated with series query %q: %v", rule.SeriesQuery, err) } @@ -194,3 +189,38 @@ 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 +} diff --git a/pkg/naming/metrics_query.go b/pkg/naming/metrics_query.go index 2ca3e462..e9ad17b0 100644 --- a/pkg/naming/metrics_query.go +++ b/pkg/naming/metrics_query.go @@ -64,7 +64,7 @@ func (q *metricsQuery) Build(series string, resource schema.GroupResource, names valuesByName := map[string][]string{} if namespace != "" { - namespaceLbl, err := q.resConverter.LabelForResource(nsGroupResource) + namespaceLbl, err := q.resConverter.LabelForResource(NsGroupResource) if err != nil { return "", err } diff --git a/pkg/external-provider/regex_matcher_test.go b/pkg/naming/regex_matcher_test.go similarity index 86% rename from pkg/external-provider/regex_matcher_test.go rename to pkg/naming/regex_matcher_test.go index 98d61400..fb0e1ef6 100644 --- a/pkg/external-provider/regex_matcher_test.go +++ b/pkg/naming/regex_matcher_test.go @@ -1,4 +1,4 @@ -package provider +package naming import ( "testing" @@ -13,7 +13,7 @@ func TestReMatcherIs(t *testing.T) { Is: "my_.*", } - matcher, err := newReMatcher(filter) + matcher, err := NewReMatcher(filter) require.NoError(t, err) result := matcher.Matches("my_label") @@ -28,7 +28,7 @@ func TestReMatcherIsNot(t *testing.T) { IsNot: "my_.*", } - matcher, err := newReMatcher(filter) + matcher, err := NewReMatcher(filter) require.NoError(t, err) result := matcher.Matches("my_label") @@ -44,6 +44,6 @@ func TestEnforcesIsOrIsNotButNotBoth(t *testing.T) { IsNot: "your_.*", } - _, err := newReMatcher(filter) + _, err := NewReMatcher(filter) require.Error(t, err) } diff --git a/pkg/naming/resource_converter.go b/pkg/naming/resource_converter.go index 43a14c51..275f77dd 100644 --- a/pkg/naming/resource_converter.go +++ b/pkg/naming/resource_converter.go @@ -18,8 +18,8 @@ import ( ) var ( - groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_") - nsGroupResource = schema.GroupResource{Resource: "namespaces"} + GroupNameSanitizer = strings.NewReplacer(".", "_", "-", "_") + NsGroupResource = schema.GroupResource{Resource: "namespaces"} ) // ResourceConverter knows the relationship between Kubernetes group-resources and Prometheus labels, @@ -118,7 +118,7 @@ func (r *resourceConverter) makeLabelForResource(resource schema.GroupResource) return "", fmt.Errorf("unable to singularize resource %s: %v", resource.String(), err) } convResource := schema.GroupResource{ - Group: groupNameSanitizer.Replace(resource.Group), + Group: GroupNameSanitizer.Replace(resource.Group), Resource: singularRes, } @@ -177,7 +177,7 @@ func (r *resourceConverter) ResourcesForSeries(series prom.Series) ([]schema.Gro } } - if groupRes == nsGroupResource { + if groupRes == NsGroupResource { namespaced = true } }