mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-05 17:27:51 +00:00
Moving metric naming to it's own package
This commit is contained in:
parent
c2e176bb23
commit
6030912cc0
12 changed files with 93 additions and 227 deletions
226
pkg/naming/metric_namer.go
Normal file
226
pkg/naming/metric_namer.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package naming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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 handable by this namer.
|
||||
Selector() prom.Selector
|
||||
// FilterSeries checks to see which of the given series match any additional
|
||||
// constraints beyond the series query. It's assumed that the series given
|
||||
// already match 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)
|
||||
|
||||
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 MetricsQuery
|
||||
nameMatches *regexp.Regexp
|
||||
nameAs string
|
||||
seriesMatchers []*ReMatcher
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
||||
for i, rule := range cfg.Rules {
|
||||
resConv, err := NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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))
|
||||
for i, filterRaw := range rule.SeriesFilters {
|
||||
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})
|
||||
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)
|
||||
}
|
||||
seriesMatchers = append(seriesMatchers, matcher)
|
||||
}
|
||||
|
||||
var nameMatches *regexp.Regexp
|
||||
if rule.Name.Matches != "" {
|
||||
nameMatches, err = regexp.Compile(rule.Name.Matches)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to compile series name match expression %q associated with series query %q: %v", rule.Name.Matches, rule.SeriesQuery, err)
|
||||
}
|
||||
} else {
|
||||
// this will always succeed
|
||||
nameMatches = regexp.MustCompile(".*")
|
||||
}
|
||||
nameAs := rule.Name.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 associated with series query %q", rule.Name.Matches, rule.SeriesQuery)
|
||||
}
|
||||
}
|
||||
|
||||
namer := &metricNamer{
|
||||
seriesQuery: prom.Selector(rule.SeriesQuery),
|
||||
metricsQuery: metricsQuery,
|
||||
nameMatches: nameMatches,
|
||||
nameAs: nameAs,
|
||||
seriesMatchers: seriesMatchers,
|
||||
ResourceConverter: resConv,
|
||||
}
|
||||
|
||||
namers[i] = namer
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
49
pkg/naming/regex_matcher_test.go
Normal file
49
pkg/naming/regex_matcher_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package naming
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/directxman12/k8s-prometheus-adapter/pkg/config"
|
||||
)
|
||||
|
||||
func TestReMatcherIs(t *testing.T) {
|
||||
filter := config.RegexFilter{
|
||||
Is: "my_.*",
|
||||
}
|
||||
|
||||
matcher, err := NewReMatcher(filter)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := matcher.Matches("my_label")
|
||||
require.True(t, result)
|
||||
|
||||
result = matcher.Matches("your_label")
|
||||
require.False(t, result)
|
||||
}
|
||||
|
||||
func TestReMatcherIsNot(t *testing.T) {
|
||||
filter := config.RegexFilter{
|
||||
IsNot: "my_.*",
|
||||
}
|
||||
|
||||
matcher, err := NewReMatcher(filter)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := matcher.Matches("my_label")
|
||||
require.False(t, result)
|
||||
|
||||
result = matcher.Matches("your_label")
|
||||
require.True(t, result)
|
||||
}
|
||||
|
||||
func TestEnforcesIsOrIsNotButNotBoth(t *testing.T) {
|
||||
filter := config.RegexFilter{
|
||||
Is: "my_.*",
|
||||
IsNot: "your_.*",
|
||||
}
|
||||
|
||||
_, err := NewReMatcher(filter)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue