diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:03 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:18 +0000 |
commit | 5da14042f70711ea5cf66e034699730335462f66 (patch) | |
tree | 0f6354ccac934ed87a2d555f45be4c831cf92f4a /src/go/collectors/go.d.plugin/pkg/prometheus/selector | |
parent | Releasing debian version 1.44.3-2. (diff) | |
download | netdata-5da14042f70711ea5cf66e034699730335462f66.tar.xz netdata-5da14042f70711ea5cf66e034699730335462f66.zip |
Merging upstream version 1.45.3+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/go/collectors/go.d.plugin/pkg/prometheus/selector')
9 files changed, 947 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/README.md b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/README.md new file mode 100644 index 000000000..33506c742 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/README.md @@ -0,0 +1,102 @@ +<!-- +title: "Time series selector" +custom_edit_url: "https://github.com/netdata/netdata/blob/master/src/go/collectors/go.d.plugin/pkg/prometheus/selector/README.md" +sidebar_label: "Time series selector" +learn_status: "Published" +learn_rel_path: "Developers/External plugins/go.d.plugin/Helper Packages" +--> + +# Time series selector + +Selectors allow selecting and filtering of a set of time series. + +## Simple Selector + +In the simplest form you need to specify only a metric name. + +### Syntax + +```cmd + <line> ::= <metric_name_pattern> + <metric_name_pattern> ::= simple pattern +``` + +The metric name pattern syntax is [simple pattern](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md). + +### Examples + +This example selects all time series that have the `go_memstats_alloc_bytes` metric name: + +```cmd +go_memstats_alloc_bytes +``` + +This example selects all time series with metric names starts with `go_memstats_`: + +```cmd +go_memstats_* +``` + +This example selects all time series with metric names starts with `go_` except `go_memstats_`: + +```cmd +!go_memstats_* go_* +``` + +## Advanced Selector + +It is possible to filter these time series further by appending a comma separated list of label matchers in curly braces (`{}`). + +### Syntax + +```cmd + <line> ::= [ <metric_name_pattern> ]{ <list_of_selectors> } + <metric_name_pattern> ::= simple pattern + <list_of_selectors> ::= a comma separated list <label_name><op><label_value_pattern> + <label_name> ::= an exact label name + <op> ::= [ '=', '!=', '=~', '!~', '=*', '!*' ] + <label_value_pattern> ::= a label value pattern, depends on <op> +``` + +The metric name pattern syntax is [simple pattern](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md). + +Label matching operators: + +- `=`: Match labels that are exactly equal to the provided string. +- `!=`: Match labels that are not equal to the provided string. +- `=~`: Match labels that [regex-match](https://golang.org/pkg/regexp/syntax/) the provided string. +- `!~`: Match labels that do not [regex-match](https://golang.org/pkg/regexp/syntax/) the provided string. +- `=*`: Match labels that [simple-pattern-match](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md) the provided string. +- `!*`: Match labels that do not [simple-pattern-match](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md) the provided string. + +### Examples + +This example selects all time series that: + +- have the `node_cooling_device_cur_state` metric name and +- label `type` value not equal to `Fan`: + +```cmd +node_cooling_device_cur_state{type!="Fan"} +``` + +This example selects all time series that: + +- have the `node_filesystem_size_bytes` metric name and +- label `device` value is either `/dev/nvme0n1p1` or `/dev/nvme0n1p2` and +- label `fstype` is equal to `ext4` + +```cmd +node_filesystem_size_bytes{device=~"/dev/nvme0n1p1$|/dev/nvme0n1p2$",fstype="ext4"} +``` + +Label matchers can also be applied to metric names by matching against the internal `__name__` label. + +For example, the expression `node_filesystem_size_bytes` is equivalent to `{__name__="node_filesystem_size_bytes"}`. +This allows using all operators (other than `=*`) for metric names matching. + +The following expression selects all metrics that have a name starting with `node_`: + +```cmd +{__name__=*"node_*"} +``` diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr.go new file mode 100644 index 000000000..7593513a5 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import "fmt" + +type Expr struct { + Allow []string `yaml:"allow" json:"allow"` + Deny []string `yaml:"deny" json:"deny"` +} + +func (e Expr) Empty() bool { + return len(e.Allow) == 0 && len(e.Deny) == 0 + +} + +func (e Expr) Parse() (Selector, error) { + if e.Empty() { + return nil, nil + } + + var srs []Selector + var allow Selector + var deny Selector + + for _, item := range e.Allow { + sr, err := Parse(item) + if err != nil { + return nil, fmt.Errorf("parse selector '%s': %v", item, err) + } + srs = append(srs, sr) + } + + switch len(srs) { + case 0: + allow = trueSelector{} + case 1: + allow = srs[0] + default: + allow = Or(srs[0], srs[1], srs[2:]...) + } + + srs = srs[:0] + for _, item := range e.Deny { + sr, err := Parse(item) + if err != nil { + return nil, fmt.Errorf("parse selector '%s': %v", item, err) + } + srs = append(srs, sr) + } + + switch len(srs) { + case 0: + deny = falseSelector{} + case 1: + deny = srs[0] + default: + deny = Or(srs[0], srs[1], srs[2:]...) + } + + return And(allow, Not(deny)), nil +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr_test.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr_test.go new file mode 100644 index 000000000..598cef9b8 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr_test.go @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "testing" + + "github.com/prometheus/prometheus/model/labels" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExpr_Empty(t *testing.T) { + tests := map[string]struct { + expr Expr + expected bool + }{ + "empty: both allow and deny": { + expr: Expr{ + Allow: []string{}, + Deny: []string{}, + }, + expected: true, + }, + "nil: both allow and deny": { + expected: true, + }, + "nil, empty: allow, deny": { + expr: Expr{ + Deny: []string{""}, + }, + expected: false, + }, + "empty, nil: allow, deny": { + expr: Expr{ + Allow: []string{""}, + }, + expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected { + assert.True(t, test.expr.Empty()) + } else { + assert.False(t, test.expr.Empty()) + } + }) + } +} + +func TestExpr_Parse(t *testing.T) { + tests := map[string]struct { + expr Expr + expectedSr Selector + expectedErr bool + }{ + "not set: both allow and deny": { + expr: Expr{}, + }, + "set: both allow and deny": { + expr: Expr{ + Allow: []string{ + "go_memstats_*", + "node_*", + }, + Deny: []string{ + "go_memstats_frees_total", + "node_cooling_*", + }, + }, + expectedSr: andSelector{ + lhs: orSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustSPName("node_*"), + }, + rhs: Not(orSelector{ + lhs: mustSPName("go_memstats_frees_total"), + rhs: mustSPName("node_cooling_*"), + }), + }, + }, + "set: only includes": { + expr: Expr{ + Allow: []string{ + "go_memstats_*", + "node_*", + }, + }, + expectedSr: andSelector{ + lhs: orSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustSPName("node_*"), + }, + rhs: Not(falseSelector{}), + }, + }, + "set: only excludes": { + expr: Expr{ + Deny: []string{ + "go_memstats_frees_total", + "node_cooling_*", + }, + }, + expectedSr: andSelector{ + lhs: trueSelector{}, + rhs: Not(orSelector{ + lhs: mustSPName("go_memstats_frees_total"), + rhs: mustSPName("node_cooling_*"), + }), + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m, err := test.expr.Parse() + + if test.expectedErr { + assert.Error(t, err) + } else { + assert.Equal(t, test.expectedSr, m) + } + }) + } +} + +func TestExprSelector_Matches(t *testing.T) { + tests := map[string]struct { + expr Expr + lbs labels.Labels + expectedMatches bool + }{ + "allow matches: single pattern": { + expr: Expr{ + Allow: []string{"go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: true, + }, + "allow matches: several patterns": { + expr: Expr{ + Allow: []string{"node_*", "go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: true, + }, + "allow not matches": { + expr: Expr{ + Allow: []string{"node_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "deny matches: single pattern": { + expr: Expr{ + Deny: []string{"go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "deny matches: several patterns": { + expr: Expr{ + Deny: []string{"node_*", "go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "deny not matches": { + expr: Expr{ + Deny: []string{"node_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: true, + }, + "allow and deny matches: single pattern": { + expr: Expr{ + Allow: []string{"go_*"}, + Deny: []string{"go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "allow and deny matches: several patterns": { + expr: Expr{ + Allow: []string{"node_*", "go_*"}, + Deny: []string{"node_*", "go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "allow matches and deny not matches": { + expr: Expr{ + Allow: []string{"go_*"}, + Deny: []string{"node_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: true, + }, + "allow not matches and deny matches": { + expr: Expr{ + Allow: []string{"node_*"}, + Deny: []string{"go_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + "allow not matches and deny not matches": { + expr: Expr{ + Allow: []string{"node_*"}, + Deny: []string{"node_*"}, + }, + lbs: []labels.Label{{Name: labels.MetricName, Value: "go_memstats_alloc_bytes"}}, + expectedMatches: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + sr, err := test.expr.Parse() + require.NoError(t, err) + + if test.expectedMatches { + assert.True(t, sr.Matches(test.lbs)) + } else { + assert.False(t, sr.Matches(test.lbs)) + } + }) + } +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical.go new file mode 100644 index 000000000..1556d1715 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "github.com/prometheus/prometheus/model/labels" +) + +type ( + trueSelector struct{} + falseSelector struct{} + negSelector struct{ s Selector } + andSelector struct{ lhs, rhs Selector } + orSelector struct{ lhs, rhs Selector } +) + +func (trueSelector) Matches(_ labels.Labels) bool { return true } +func (falseSelector) Matches(_ labels.Labels) bool { return false } +func (s negSelector) Matches(lbs labels.Labels) bool { return !s.s.Matches(lbs) } +func (s andSelector) Matches(lbs labels.Labels) bool { return s.lhs.Matches(lbs) && s.rhs.Matches(lbs) } +func (s orSelector) Matches(lbs labels.Labels) bool { return s.lhs.Matches(lbs) || s.rhs.Matches(lbs) } + +// True returns a selector which always returns true +func True() Selector { + return trueSelector{} +} + +// And returns a selector which returns true only if all of it's sub-selectors return true +func And(lhs, rhs Selector, others ...Selector) Selector { + s := andSelector{lhs: lhs, rhs: rhs} + if len(others) == 0 { + return s + } + return And(s, others[0], others[1:]...) +} + +// Or returns a selector which returns true if any of it's sub-selectors return true +func Or(lhs, rhs Selector, others ...Selector) Selector { + s := orSelector{lhs: lhs, rhs: rhs} + if len(others) == 0 { + return s + } + return Or(s, others[0], others[1:]...) +} + +// Not returns a selector which returns the negation of the sub-selector's result +func Not(s Selector) Selector { + return negSelector{s} +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical_test.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical_test.go new file mode 100644 index 000000000..239c7f715 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical_test.go @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "testing" + + "github.com/prometheus/prometheus/model/labels" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTrueSelector_Matches(t *testing.T) { + tests := map[string]struct { + sr trueSelector + lbs labels.Labels + expected bool + }{ + "not empty labels": { + lbs: labels.Labels{{Name: labels.MetricName, Value: "name"}}, + expected: true, + }, + "empty labels": { + expected: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected { + assert.True(t, test.sr.Matches(test.lbs)) + } else { + assert.False(t, test.sr.Matches(test.lbs)) + } + }) + } +} + +func TestFalseSelector_Matches(t *testing.T) { + tests := map[string]struct { + sr falseSelector + lbs labels.Labels + expected bool + }{ + "not empty labels": { + lbs: labels.Labels{{Name: labels.MetricName, Value: "name"}}, + expected: false, + }, + "empty labels": { + expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected { + assert.True(t, test.sr.Matches(test.lbs)) + } else { + assert.False(t, test.sr.Matches(test.lbs)) + } + }) + } +} + +func TestNegSelector_Matches(t *testing.T) { + tests := map[string]struct { + sr negSelector + lbs labels.Labels + expected bool + }{ + "true matcher": { + sr: negSelector{trueSelector{}}, + lbs: labels.Labels{{Name: labels.MetricName, Value: "name"}}, + expected: false, + }, + "false matcher": { + sr: negSelector{falseSelector{}}, + lbs: labels.Labels{{Name: labels.MetricName, Value: "name"}}, + expected: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected { + assert.True(t, test.sr.Matches(test.lbs)) + } else { + assert.False(t, test.sr.Matches(test.lbs)) + } + }) + } +} + +func TestAndSelector_Matches(t *testing.T) { + tests := map[string]struct { + sr andSelector + lbs labels.Labels + expected bool + }{ + "true, true": { + sr: andSelector{lhs: trueSelector{}, rhs: trueSelector{}}, + expected: true, + }, + "true, false": { + sr: andSelector{lhs: trueSelector{}, rhs: falseSelector{}}, + expected: false, + }, + "false, true": { + sr: andSelector{lhs: trueSelector{}, rhs: falseSelector{}}, + expected: false, + }, + "false, false": { + sr: andSelector{lhs: falseSelector{}, rhs: falseSelector{}}, + expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expected, test.sr.Matches(test.lbs)) + }) + } +} + +func TestOrSelector_Matches(t *testing.T) { + tests := map[string]struct { + sr orSelector + lbs labels.Labels + expected bool + }{ + "true, true": { + sr: orSelector{lhs: trueSelector{}, rhs: trueSelector{}}, + expected: true, + }, + "true, false": { + sr: orSelector{lhs: trueSelector{}, rhs: falseSelector{}}, + expected: true, + }, + "false, true": { + sr: orSelector{lhs: trueSelector{}, rhs: falseSelector{}}, + expected: true, + }, + "false, false": { + sr: orSelector{lhs: falseSelector{}, rhs: falseSelector{}}, + expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expected, test.sr.Matches(test.lbs)) + }) + } +} + +func Test_And(t *testing.T) { + tests := map[string]struct { + srs []Selector + expected Selector + }{ + "2 selectors": { + srs: []Selector{trueSelector{}, trueSelector{}}, + expected: andSelector{ + lhs: trueSelector{}, + rhs: trueSelector{}, + }, + }, + "4 selectors": { + srs: []Selector{trueSelector{}, trueSelector{}, trueSelector{}, trueSelector{}}, + expected: andSelector{ + lhs: andSelector{ + lhs: andSelector{ + lhs: trueSelector{}, + rhs: trueSelector{}, + }, + rhs: trueSelector{}, + }, + rhs: trueSelector{}}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require.GreaterOrEqual(t, len(test.srs), 2) + + s := And(test.srs[0], test.srs[1], test.srs[2:]...) + assert.Equal(t, test.expected, s) + }) + } +} + +func Test_Or(t *testing.T) { + tests := map[string]struct { + srs []Selector + expected Selector + }{ + "2 selectors": { + srs: []Selector{trueSelector{}, trueSelector{}}, + expected: orSelector{ + lhs: trueSelector{}, + rhs: trueSelector{}, + }, + }, + "4 selectors": { + srs: []Selector{trueSelector{}, trueSelector{}, trueSelector{}, trueSelector{}}, + expected: orSelector{ + lhs: orSelector{ + lhs: orSelector{ + lhs: trueSelector{}, + rhs: trueSelector{}, + }, + rhs: trueSelector{}, + }, + rhs: trueSelector{}}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require.GreaterOrEqual(t, len(test.srs), 2) + + s := Or(test.srs[0], test.srs[1], test.srs[2:]...) + assert.Equal(t, test.expected, s) + }) + } +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse.go new file mode 100644 index 000000000..29c1d4fbf --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse.go @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "fmt" + "regexp" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/matcher" +) + +var ( + reLV = regexp.MustCompile(`^(?P<label_name>[a-zA-Z0-9_]+)(?P<op>=~|!~|=\*|!\*|=|!=)"(?P<pattern>.+)"$`) +) + +func Parse(expr string) (Selector, error) { + var srs []Selector + lvs := strings.Split(unsugarExpr(expr), ",") + + for _, lv := range lvs { + sr, err := parseSelector(lv) + if err != nil { + return nil, err + } + srs = append(srs, sr) + } + + switch len(srs) { + case 0: + return nil, nil + case 1: + return srs[0], nil + default: + return And(srs[0], srs[1], srs[2:]...), nil + } +} + +func parseSelector(line string) (Selector, error) { + sub := reLV.FindStringSubmatch(strings.TrimSpace(line)) + if sub == nil { + return nil, fmt.Errorf("invalid selector syntax: '%s'", line) + } + + name, op, pattern := sub[1], sub[2], strings.Trim(sub[3], "\"") + + var m matcher.Matcher + var err error + + switch op { + case OpEqual, OpNegEqual: + m, err = matcher.NewStringMatcher(pattern, true, true) + case OpRegexp, OpNegRegexp: + m, err = matcher.NewRegExpMatcher(pattern) + case OpSimplePatterns, OpNegSimplePatterns: + m, err = matcher.NewSimplePatternsMatcher(pattern) + default: + err = fmt.Errorf("unknown matching operator: %s", op) + } + if err != nil { + return nil, err + } + + sr := labelSelector{ + name: name, + m: m, + } + + if neg := strings.HasPrefix(op, "!"); neg { + return Not(sr), nil + } + return sr, nil +} + +func unsugarExpr(expr string) string { + // name => __name__=*"name" + // name{label="value"} => __name__=*"name",label="value" + // {label="value"} => label="value" + expr = strings.TrimSpace(expr) + + switch idx := strings.IndexByte(expr, '{'); true { + case idx == -1: + expr = fmt.Sprintf(`__name__%s"%s"`, + OpSimplePatterns, + strings.TrimSpace(expr), + ) + case idx == 0: + expr = strings.Trim(expr, "{}") + default: + expr = fmt.Sprintf(`__name__%s"%s",%s`, + OpSimplePatterns, + strings.TrimSpace(expr[:idx]), + strings.Trim(expr[idx:], "{}"), + ) + } + return expr +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse_test.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse_test.go new file mode 100644 index 000000000..ba764e039 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse_test.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "fmt" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/matcher" + + "github.com/prometheus/prometheus/model/labels" + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + tests := map[string]struct { + input string + expectedSr Selector + expectedErr bool + }{ + "sp op: only metric name": { + input: "go_memstats_alloc_bytes !go_memstats_* *", + expectedSr: mustSPName("go_memstats_alloc_bytes !go_memstats_* *"), + }, + "string op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"value"}`, OpEqual), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustString("label", "value"), + }, + }, + "neg string op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"value"}`, OpNegEqual), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: Not(mustString("label", "value")), + }, + }, + "regexp op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"valu.+"}`, OpRegexp), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustRegexp("label", "valu.+"), + }, + }, + "neg regexp op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"valu.+"}`, OpNegRegexp), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: Not(mustRegexp("label", "valu.+")), + }, + }, + "sp op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"valu*"}`, OpSimplePatterns), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustSP("label", "valu*"), + }, + }, + "neg sp op: metric name with labels": { + input: fmt.Sprintf(`go_memstats_*{label%s"valu*"}`, OpNegSimplePatterns), + expectedSr: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: Not(mustSP("label", "valu*")), + }, + }, + "metric name with several labels": { + input: fmt.Sprintf(`go_memstats_*{label1%s"value1",label2%s"value2"}`, OpEqual, OpEqual), + expectedSr: andSelector{ + lhs: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustString("label1", "value1"), + }, + rhs: mustString("label2", "value2"), + }, + }, + "only labels (unsugar)": { + input: fmt.Sprintf(`{__name__%s"go_memstats_*",label1%s"value1",label2%s"value2"}`, + OpSimplePatterns, OpEqual, OpEqual), + expectedSr: andSelector{ + lhs: andSelector{ + lhs: mustSPName("go_memstats_*"), + rhs: mustString("label1", "value1"), + }, + rhs: mustString("label2", "value2"), + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + sr, err := Parse(test.input) + + if test.expectedErr { + assert.Error(t, err) + } else { + assert.Equal(t, test.expectedSr, sr) + } + }) + } +} + +func mustSPName(pattern string) Selector { + return mustSP(labels.MetricName, pattern) +} + +func mustString(name string, pattern string) Selector { + return labelSelector{name: name, m: matcher.Must(matcher.NewStringMatcher(pattern, true, true))} +} + +func mustRegexp(name string, pattern string) Selector { + return labelSelector{name: name, m: matcher.Must(matcher.NewRegExpMatcher(pattern))} +} + +func mustSP(name string, pattern string) Selector { + return labelSelector{name: name, m: matcher.Must(matcher.NewSimplePatternsMatcher(pattern))} +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector.go new file mode 100644 index 000000000..28203fca1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "github.com/netdata/netdata/go/go.d.plugin/pkg/matcher" + + "github.com/prometheus/prometheus/model/labels" +) + +type Selector interface { + Matches(lbs labels.Labels) bool +} + +const ( + OpEqual = "=" + OpNegEqual = "!=" + OpRegexp = "=~" + OpNegRegexp = "!~" + OpSimplePatterns = "=*" + OpNegSimplePatterns = "!*" +) + +type labelSelector struct { + name string + m matcher.Matcher +} + +func (s labelSelector) Matches(lbs labels.Labels) bool { + if s.name == labels.MetricName { + return s.m.MatchString(lbs[0].Value) + } + if label, ok := lookupLabel(s.name, lbs[1:]); ok { + return s.m.MatchString(label.Value) + } + return false +} + +type Func func(lbs labels.Labels) bool + +func (fn Func) Matches(lbs labels.Labels) bool { + return fn(lbs) +} + +func lookupLabel(name string, lbs labels.Labels) (labels.Label, bool) { + for _, label := range lbs { + if label.Name == name { + return label, true + } + } + return labels.Label{}, false +} diff --git a/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector_test.go b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector_test.go new file mode 100644 index 000000000..aa3110b03 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector_test.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package selector + +import ( + "testing" +) + +func TestLabelMatcher_Matches(t *testing.T) { + +} |