rule: allow merging labels from group level

Support merging labels from groups to rule labels

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
This commit is contained in:
Seena Fallah 2022-10-18 20:43:32 +02:00
parent d4f098ae80
commit f253d36361
9 changed files with 99 additions and 23 deletions

View file

@ -58,6 +58,7 @@ import (
_ "github.com/prometheus/prometheus/plugins" // Register plugins. _ "github.com/prometheus/prometheus/plugins" // Register plugins.
"github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/promqltest" "github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/util/documentcli" "github.com/prometheus/prometheus/util/documentcli"
) )
@ -889,30 +890,30 @@ func compare(a, b compareRuleType) int {
func checkDuplicates(groups []rulefmt.RuleGroup) []compareRuleType { func checkDuplicates(groups []rulefmt.RuleGroup) []compareRuleType {
var duplicates []compareRuleType var duplicates []compareRuleType
var rules compareRuleTypes var cRules compareRuleTypes
for _, group := range groups { for _, group := range groups {
for _, rule := range group.Rules { for _, rule := range group.Rules {
rules = append(rules, compareRuleType{ cRules = append(cRules, compareRuleType{
metric: ruleMetric(rule), metric: ruleMetric(rule),
label: labels.FromMap(rule.Labels), label: rules.FromMaps(group.Labels, rule.Labels),
}) })
} }
} }
if len(rules) < 2 { if len(cRules) < 2 {
return duplicates return duplicates
} }
sort.Sort(rules) sort.Sort(cRules)
last := rules[0] last := cRules[0]
for i := 1; i < len(rules); i++ { for i := 1; i < len(cRules); i++ {
if compare(last, rules[i]) == 0 { if compare(last, cRules[i]) == 0 {
// Don't add a duplicated rule multiple times. // Don't add a duplicated rule multiple times.
if len(duplicates) == 0 || compare(last, duplicates[len(duplicates)-1]) != 0 { if len(duplicates) == 0 || compare(last, duplicates[len(duplicates)-1]) != 0 {
duplicates = append(duplicates, rules[i]) duplicates = append(duplicates, cRules[i])
} }
} }
last = rules[i] last = cRules[i]
} }
return duplicates return duplicates

View file

@ -21,6 +21,8 @@ An example rules file with an alert would be:
```yaml ```yaml
groups: groups:
- name: example - name: example
labels:
team: myteam
rules: rules:
- alert: HighRequestLatency - alert: HighRequestLatency
expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5 expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5

View file

@ -89,6 +89,11 @@ name: <string>
# Offset the rule evaluation timestamp of this particular group by the specified duration into the past. # Offset the rule evaluation timestamp of this particular group by the specified duration into the past.
[ query_offset: <duration> | default = global.rule_query_offset ] [ query_offset: <duration> | default = global.rule_query_offset ]
# Labels to add or overwrite before storing the result for its rules.
# Labels defined in <rule> will override the key if it has a collision.
labels:
[ <labelname>: <labelvalue> ]
rules: rules:
[ - <rule> ... ] [ - <rule> ... ]
``` ```

View file

@ -111,6 +111,20 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) {
) )
} }
for k, v := range g.Labels {
if !model.LabelName(k).IsValid() || k == model.MetricNameLabel {
errs = append(
errs, fmt.Errorf("invalid label name: %s", k),
)
}
if !model.LabelValue(v).IsValid() {
errs = append(
errs, fmt.Errorf("invalid label value: %s", v),
)
}
}
set[g.Name] = struct{}{} set[g.Name] = struct{}{}
for i, r := range g.Rules { for i, r := range g.Rules {
@ -136,11 +150,12 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) {
// RuleGroup is a list of sequentially evaluated recording and alerting rules. // RuleGroup is a list of sequentially evaluated recording and alerting rules.
type RuleGroup struct { type RuleGroup struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Interval model.Duration `yaml:"interval,omitempty"` Interval model.Duration `yaml:"interval,omitempty"`
QueryOffset *model.Duration `yaml:"query_offset,omitempty"` QueryOffset *model.Duration `yaml:"query_offset,omitempty"`
Limit int `yaml:"limit,omitempty"` Limit int `yaml:"limit,omitempty"`
Rules []RuleNode `yaml:"rules"` Rules []RuleNode `yaml:"rules"`
Labels map[string]string `yaml:"labels,omitempty"`
} }
// Rule describes an alerting or recording rule. // Rule describes an alerting or recording rule.

View file

@ -108,6 +108,23 @@ groups:
severity: "page" severity: "page"
annotations: annotations:
summary: "Instance {{ $labels.instance }} down" summary: "Instance {{ $labels.instance }} down"
`,
shouldPass: true,
},
{
ruleString: `
groups:
- name: example
labels:
team: myteam
rules:
- alert: InstanceDown
expr: up == 0
for: 5m
labels:
severity: "page"
annotations:
summary: "Instance {{ $labels.instance }} down"
`, `,
shouldPass: true, shouldPass: true,
}, },

View file

@ -312,13 +312,15 @@ func (m *Manager) LoadGroups(
return nil, []error{fmt.Errorf("%s: %w", fn, err)} return nil, []error{fmt.Errorf("%s: %w", fn, err)}
} }
mLabels := FromMaps(rg.Labels, r.Labels)
if r.Alert.Value != "" { if r.Alert.Value != "" {
rules = append(rules, NewAlertingRule( rules = append(rules, NewAlertingRule(
r.Alert.Value, r.Alert.Value,
expr, expr,
time.Duration(r.For), time.Duration(r.For),
time.Duration(r.KeepFiringFor), time.Duration(r.KeepFiringFor),
labels.FromMap(r.Labels), mLabels,
labels.FromMap(r.Annotations), labels.FromMap(r.Annotations),
externalLabels, externalLabels,
externalURL, externalURL,
@ -330,7 +332,7 @@ func (m *Manager) LoadGroups(
rules = append(rules, NewRecordingRule( rules = append(rules, NewRecordingRule(
r.Record.Value, r.Record.Value,
expr, expr,
labels.FromMap(r.Labels), mLabels,
)) ))
} }
@ -501,3 +503,16 @@ func (c sequentialRuleEvalController) Allow(_ context.Context, _ *Group, _ Rule)
} }
func (c sequentialRuleEvalController) Done(_ context.Context) {} func (c sequentialRuleEvalController) Done(_ context.Context) {}
// FromMaps returns new sorted Labels from the given maps, overriding each other in order.
func FromMaps(maps ...map[string]string) labels.Labels {
mLables := make(map[string]string)
for _, m := range maps {
for k, v := range m {
mLables[k] = v
}
}
return labels.FromMap(mLables)
}

View file

@ -853,10 +853,11 @@ type ruleGroupsTest struct {
// ruleGroupTest forms a testing struct for running tests over rules. // ruleGroupTest forms a testing struct for running tests over rules.
type ruleGroupTest struct { type ruleGroupTest struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Interval model.Duration `yaml:"interval,omitempty"` Interval model.Duration `yaml:"interval,omitempty"`
Limit int `yaml:"limit,omitempty"` Limit int `yaml:"limit,omitempty"`
Rules []rulefmt.Rule `yaml:"rules"` Rules []rulefmt.Rule `yaml:"rules"`
Labels map[string]string `yaml:"labels,omitempty"`
} }
func formatRules(r *rulefmt.RuleGroups) ruleGroupsTest { func formatRules(r *rulefmt.RuleGroups) ruleGroupsTest {
@ -879,6 +880,7 @@ func formatRules(r *rulefmt.RuleGroups) ruleGroupsTest {
Interval: g.Interval, Interval: g.Interval,
Limit: g.Limit, Limit: g.Limit,
Rules: rtmp, Rules: rtmp,
Labels: g.Labels,
}) })
} }
return ruleGroupsTest{ return ruleGroupsTest{
@ -2154,3 +2156,18 @@ func optsFactory(storage storage.Storage, maxInflight, inflightQueries *atomic.I
}, },
} }
} }
func TestLabels_FromMaps(t *testing.T) {
mLabels := FromMaps(
map[string]string{"aaa": "101", "bbb": "222"},
map[string]string{"aaa": "111", "ccc": "333"},
)
expected := labels.New(
labels.Label{Name: "aaa", Value: "111"},
labels.Label{Name: "bbb", Value: "222"},
labels.Label{Name: "ccc", Value: "333"},
)
require.Equal(t, expected, mLabels, "unexpected labelset")
}

View file

@ -37,6 +37,7 @@ interface RuleGroup {
file: string; file: string;
rules: Rule[]; rules: Rule[];
interval: number; interval: number;
labels: Record<string, string>;
} }
const kvSearchRule = new KVSearch<Rule>({ const kvSearchRule = new KVSearch<Rule>({
@ -93,6 +94,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
name: group.name, name: group.name,
interval: group.interval, interval: group.interval,
rules: ruleFilterList.map((value) => value.original), rules: ruleFilterList.map((value) => value.original),
labels: group.labels,
}); });
} }
} }
@ -114,6 +116,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
name: group.name, name: group.name,
interval: group.interval, interval: group.interval,
rules: group.rules.filter((value) => filter[value.state]), rules: group.rules.filter((value) => filter[value.state]),
labels: group.labels,
}; };
if (newGroup.rules.length > 0) { if (newGroup.rules.length > 0) {
result.push(newGroup); result.push(newGroup);

View file

@ -17,6 +17,7 @@ interface RuleGroup {
rules: Rule[]; rules: Rule[];
evaluationTime: string; evaluationTime: string;
lastEvaluation: string; lastEvaluation: string;
labels: Record<string, string>;
} }
export interface RulesMap { export interface RulesMap {
@ -105,10 +106,10 @@ export const RulesContent: FC<RulesContentProps> = ({ response }) => {
<strong>keep_firing_for:</strong> {formatDuration(r.keepFiringFor * 1000)} <strong>keep_firing_for:</strong> {formatDuration(r.keepFiringFor * 1000)}
</div> </div>
)} )}
{r.labels && Object.keys(r.labels).length > 0 && ( {Object.keys(Object.assign({ ...g.labels }, { ...r.labels })).length > 0 && (
<div> <div>
<strong>labels:</strong> <strong>labels:</strong>
{Object.entries(r.labels).map(([key, value]) => ( {Object.entries(Object.assign({ ...g.labels }, { ...r.labels })).map(([key, value]) => (
<div className="ml-4" key={key}> <div className="ml-4" key={key}>
{key}: {value} {key}: {value}
</div> </div>