Skip to content

Commit

Permalink
Add matcher support to Query Rules endpoint (#5111)
Browse files Browse the repository at this point in the history
* Add matcher support to Query Rules endpoint

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>

* Add CHANGELOG entry

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>

* Add tests and implement suggestions

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>

* Pre-allocate matcherSets

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>

* Add other matchtype testcase

Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
  • Loading branch information
saswatamcode committed Feb 3, 2022
1 parent 677c3ee commit 4357002
Show file tree
Hide file tree
Showing 6 changed files with 647 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
- [#4974](https://github.com/thanos-io/thanos/pull/4974) Store: Support tls_config configuration for connecting with Azure storage.
- [#4999](https://github.com/thanos-io/thanos/pull/4999) COS: Support `endpoint` configuration for vpc internal endpoint.
- [#5059](https://github.com/thanos-io/thanos/pull/5059) Compactor: Adding minimum retention flag validation for downsampling retention.
- [#5111](https://github.com/thanos-io/thanos/pull/5111) Add matcher support to Query Rules endpoint.

### Fixed

Expand Down
5 changes: 5 additions & 0 deletions pkg/api/query/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,10 +787,15 @@ func NewRulesHandler(client rules.UnaryClient, enablePartialResponse bool) func(
typ = int32(rulespb.RulesRequest_ALL)
}

if err := r.ParseForm(); err != nil {
return nil, nil, &api.ApiError{Typ: api.ErrorInternal, Err: errors.Errorf("error parsing request form='%v'", MatcherParam)}
}

// TODO(bwplotka): Allow exactly the same functionality as query API: passing replica, dedup and partial response as HTTP params as well.
req := &rulespb.RulesRequest{
Type: rulespb.RulesRequest_Type(typ),
PartialResponseStrategy: ps,
MatcherString: r.Form[MatcherParam],
}
tracing.DoInSpan(ctx, "retrieve_rules", func(ctx context.Context) {
groups, warnings, err = client.Rules(ctx, req)
Expand Down
59 changes: 59 additions & 0 deletions pkg/rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"context"
"sort"
"sync"
"text/template"
"text/template/parse"

"github.com/pkg/errors"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"

"github.com/thanos-io/thanos/pkg/rules/rulespb"
Expand Down Expand Up @@ -58,6 +61,16 @@ func (rr *GRPCClient) Rules(ctx context.Context, req *rulespb.RulesRequest) (*ru
return nil, nil, errors.Wrap(err, "proxy Rules")
}

var err error
matcherSets := make([][]*labels.Matcher, len(req.MatcherString))
for i, s := range req.MatcherString {
matcherSets[i], err = parser.ParseMetricSelector(s)
if err != nil {
return nil, nil, errors.Wrap(err, "parser ParseMetricSelector")
}
}

resp.groups = filterRules(resp.groups, matcherSets)
// TODO(bwplotka): Move to SortInterface with equal method and heap.
resp.groups = dedupGroups(resp.groups)
for _, g := range resp.groups {
Expand All @@ -67,6 +80,52 @@ func (rr *GRPCClient) Rules(ctx context.Context, req *rulespb.RulesRequest) (*ru
return &rulespb.RuleGroups{Groups: resp.groups}, resp.warnings, nil
}

// filterRules filters rules in a group according to given matcherSets.
func filterRules(ruleGroups []*rulespb.RuleGroup, matcherSets [][]*labels.Matcher) []*rulespb.RuleGroup {
if len(matcherSets) == 0 {
return ruleGroups
}

for _, g := range ruleGroups {
filteredRules := g.Rules[:0]
for _, r := range g.Rules {
rl := r.GetLabels()
if matches(matcherSets, rl) {
filteredRules = append(filteredRules, r)
}
}
g.Rules = filteredRules
}

return ruleGroups
}

// matches returns whether the non-templated labels satisfy all the matchers in matcherSets.
func matches(matcherSets [][]*labels.Matcher, l labels.Labels) bool {
if len(matcherSets) == 0 {
return true
}

var nonTemplatedLabels labels.Labels
labelTemplate := template.New("label")
for _, label := range l {
t, err := labelTemplate.Parse(label.Value)
// Label value is non-templated if it is one node of type NodeText.
if err == nil && len(t.Root.Nodes) == 1 && t.Root.Nodes[0].Type() == parse.NodeText {
nonTemplatedLabels = append(nonTemplatedLabels, label)
}
}

for _, matchers := range matcherSets {
for _, m := range matchers {
if v := nonTemplatedLabels.Get(m.Name); !m.Matches(v) {
return false
}
}
}
return true
}

// dedupRules re-sorts the set so that the same series with different replica
// labels are coming right after each other.
func dedupRules(rules []*rulespb.Rule, replicaLabels map[string]struct{}) []*rulespb.Rule {
Expand Down
Loading

0 comments on commit 4357002

Please sign in to comment.