pkg/operator/apis/monitoring/v1/rules_config.go (158 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package v1 import ( "fmt" "github.com/GoogleCloudPlatform/prometheus-engine/pkg/export" model "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/rulefmt" "github.com/prometheus/prometheus/promql/parser" "gopkg.in/yaml.v3" ) func (r *Rules) RuleGroupsConfig(projectID, location, cluster string) (string, error) { return ruleGroupsConfig(r.Spec.Groups, map[string]string{ export.KeyProjectID: projectID, export.KeyLocation: location, export.KeyCluster: cluster, export.KeyNamespace: r.Namespace, }) } func (r *ClusterRules) RuleGroupsConfig(projectID, location, cluster string) (string, error) { return ruleGroupsConfig(r.Spec.Groups, map[string]string{ export.KeyProjectID: projectID, export.KeyLocation: location, export.KeyCluster: cluster, }) } func (r *GlobalRules) RuleGroupsConfig() (string, error) { return ruleGroupsConfig(r.Spec.Groups, map[string]string{}) } func ruleGroupsConfig(ruleGroups []RuleGroup, labelSet map[string]string) (string, error) { rs, err := fromAPIRules(ruleGroups) if err != nil { return "", fmt.Errorf("converting rules failed: %w", err) } if err := scope(&rs, labelSet); err != nil { return "", fmt.Errorf("isolating rules failed: %w", err) } result, err := yaml.Marshal(rs) if err != nil { return "", fmt.Errorf("marshalling rules failed: %w", err) } return string(result), nil } // fromAPIRules constructs rule groups from a list of rule groups in the // resource API format. It ensures that the groups are valid according to the // Prometheus upstream validation logic. func fromAPIRules(groups []RuleGroup) (result rulefmt.RuleGroups, err error) { for _, g := range groups { var rules []rulefmt.RuleNode for _, r := range g.Rules { rule := rulefmt.RuleNode{ Labels: r.Labels, Annotations: r.Annotations, } rule.Expr.SetString(r.Expr) if r.Record != "" { rule.Record.SetString(r.Record) } if r.Alert != "" { rule.Alert.SetString(r.Alert) } if r.For != "" { rule.For, err = model.ParseDuration(r.For) if err != nil { return result, fmt.Errorf("parse 'for' duration: %w", err) } } rules = append(rules, rule) } group := rulefmt.RuleGroup{ Name: g.Name, Rules: rules, } if g.Interval != "" { group.Interval, err = model.ParseDuration(g.Interval) if err != nil { return result, fmt.Errorf("parse evaluation interval: %w", err) } } result.Groups = append(result.Groups, group) } // Do a marshal/unmarshal cycle to run the upstream validation. b, err := yaml.Marshal(result) if err != nil { return result, err } var validate rulefmt.RuleGroups if err := yaml.Unmarshal(b, &validate); err != nil { return result, fmt.Errorf("loading rules failed: %w", err) } return result, nil } // scope all rules in the given groups to the given labels. All metric selectors // check for equality on the labels and all rule results are annotated with them again. // This ensures that the scope is preserved in output data, even if the given label keys // are aggregated away. // An error is returned if metric selectors have a conflicting selector set. func scope(groups *rulefmt.RuleGroups, lset map[string]string) error { for _, g := range groups.Groups { for i, r := range g.Rules { expr, err := parser.ParseExpr(r.Expr.Value) if err != nil { return fmt.Errorf("parse PromQL expression: %w", err) } // Traverse the query and inject label matchers to all metric selectors err = walkExpr(expr, func(n parser.Node) error { vs, ok := n.(*parser.VectorSelector) if ok { for name, value := range lset { if err := setSelector(vs, name, value); err != nil { return fmt.Errorf("set isolation selector %s=%q on %s: %w", name, value, vs, err) } } } return nil }) if err != nil { return err } r.Expr.SetString(expr.String()) // Add labels to produced label sets (metrics or alerts) in case // they got aggregated away. for name, value := range lset { if err := setLabel(&r, name, value); err != nil { return fmt.Errorf("set result isolation label %s=%q on %v: %w", name, value, r, err) } } g.Rules[i] = r } } return nil } func setLabel(r *rulefmt.RuleNode, name, value string) error { if v, ok := r.Labels[name]; ok { return fmt.Errorf("label %q already set on rule with unexpected value %q", name, v) } if value == "" { return nil } if r.Labels == nil { r.Labels = map[string]string{} } r.Labels[name] = value return nil } func setSelector(s *parser.VectorSelector, name, value string) error { for _, m := range s.LabelMatchers { if m.Name != name { continue } if m.Type != labels.MatchEqual || m.Value != value { return fmt.Errorf("conflicting label matcher %s found", m) } } if value != "" { s.LabelMatchers = append(s.LabelMatchers, &labels.Matcher{ Type: labels.MatchEqual, Name: name, Value: value, }) } return nil } func walkExpr(node parser.Node, f func(parser.Node) error) error { for _, c := range parser.Children(node) { if err := walkExpr(c, f); err != nil { return err } } return f(node) }