summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/pkg/prometheus/selector
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 12:08:03 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 12:08:18 +0000
commit5da14042f70711ea5cf66e034699730335462f66 (patch)
tree0f6354ccac934ed87a2d555f45be4c831cf92f4a /src/go/collectors/go.d.plugin/pkg/prometheus/selector
parentReleasing debian version 1.44.3-2. (diff)
downloadnetdata-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')
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/README.md102
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr.go62
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/expr_test.go231
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical.go49
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/logical_test.go226
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse.go97
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/parse_test.go117
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector.go52
-rw-r--r--src/go/collectors/go.d.plugin/pkg/prometheus/selector/selector_test.go11
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) {
+
+}