From 3f49bdaeffa00888e3233453d4bc00630aa5910d Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 24 Feb 2022 02:26:15 +0200 Subject: [PATCH] lib/promrelabel: add support for conditional relabeling via `if` filter Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1998 --- app/vmagent/README.md | 17 +- docs/CHANGELOG.md | 17 + docs/vmagent.md | 17 +- lib/promrelabel/config.go | 12 +- lib/promrelabel/if_expression.go | 169 ++++++++++ lib/promrelabel/if_expression_test.go | 162 +++++++++ lib/promrelabel/relabel.go | 35 ++ lib/promrelabel/relabel_test.go | 456 ++++++++++++++++++++++++++ 8 files changed, 878 insertions(+), 7 deletions(-) create mode 100644 lib/promrelabel/if_expression.go create mode 100644 lib/promrelabel/if_expression_test.go diff --git a/app/vmagent/README.md b/app/vmagent/README.md index ec551d5ea..48f1a3d73 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -264,7 +264,7 @@ Labels can be added to metrics by the following mechanisms: ## Relabeling -`vmagent` and VictoriaMetrics support Prometheus-compatible relabeling. +VictoriaMetrics components (including `vmagent`) support Prometheus-compatible relabeling. They provide the following additional actions on top of actions from the [Prometheus relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config): * `replace_all`: replaces all of the occurences of `regex` in the values of `source_labels` with the `replacement` and stores the results in the `target_label`. @@ -289,6 +289,21 @@ The `regex` value can be split into multiple lines for improved readability and - "foo_.+" ``` +VictoriaMetrics components support an optional `if` filter, which can be used for conditional relabeling. The `if` filter may contain arbitrary [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors). For example, the following relabeling rule drops targets, which don't match `foo{bar="baz"}` series selector: + +```yaml +- action: keep + if: 'foo{bar="baz"}' +``` + +This is equivalent to less clear traditional relabeling rule: + +```yaml +- action: keep + source_labels: [__name__, bar] + regex: 'foo;baz' +``` + The relabeling can be defined in the following places: * At the `scrape_config -> relabel_configs` section in `-promscrape.config` file. This relabeling is applied to target labels. This relabeling can be debugged by passing `relabel_debug: true` option to the corresponding `scrape_config` section. In this case `vmagent` logs target labels before and after the relabeling and then drops the logged target. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 045add236..50e54dc86 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,23 @@ The following tip changes can be tested by building VictoriaMetrics components f ## tip +* FEATURE: add support for conditional relabeling via `if` filter. The `if` filter can contain arbitrary [series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors). For example, the following rule drops targets matching `foo{bar="baz"}` series selector: + +```yml +- action: drop + if: 'foo{bar="baz"}' +``` + +This rule is equivalent to less clear traditional one: + +```yml +- action: drop + source_labels: [__name__, bar] + regex: 'foo;baz' +``` + + See [relabeling docs](https://docs.victoriametrics.com/vmagent.html#relabeling) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1998) for more details. + * FEATURE: reduce memory usage for various caches under [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). * BUGFIX: [vmgateway](https://docs.victoriametrics.com/vmgateway.html): properly parse JWT tokens if they are encoded with [URL-safe base64 encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5). diff --git a/docs/vmagent.md b/docs/vmagent.md index ecfbf065b..039426a55 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -268,7 +268,7 @@ Labels can be added to metrics by the following mechanisms: ## Relabeling -`vmagent` and VictoriaMetrics support Prometheus-compatible relabeling. +VictoriaMetrics components (including `vmagent`) support Prometheus-compatible relabeling. They provide the following additional actions on top of actions from the [Prometheus relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config): * `replace_all`: replaces all of the occurences of `regex` in the values of `source_labels` with the `replacement` and stores the results in the `target_label`. @@ -293,6 +293,21 @@ The `regex` value can be split into multiple lines for improved readability and - "foo_.+" ``` +VictoriaMetrics components support an optional `if` filter, which can be used for conditional relabeling. The `if` filter may contain arbitrary [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors). For example, the following relabeling rule drops targets, which don't match `foo{bar="baz"}` series selector: + +```yaml +- action: keep + if: 'foo{bar="baz"}' +``` + +This is equivalent to less clear traditional relabeling rule: + +```yaml +- action: keep + source_labels: [__name__, bar] + regex: 'foo;baz' +``` + The relabeling can be defined in the following places: * At the `scrape_config -> relabel_configs` section in `-promscrape.config` file. This relabeling is applied to target labels. This relabeling can be debugged by passing `relabel_debug: true` option to the corresponding `scrape_config` section. In this case `vmagent` logs target labels before and after the relabeling and then drops the logged target. diff --git a/lib/promrelabel/config.go b/lib/promrelabel/config.go index a113349d7..ce088c1f2 100644 --- a/lib/promrelabel/config.go +++ b/lib/promrelabel/config.go @@ -22,6 +22,7 @@ type RelabelConfig struct { Modulus uint64 `yaml:"modulus,omitempty"` Replacement *string `yaml:"replacement,omitempty"` Action string `yaml:"action,omitempty"` + If *IfExpression `yaml:"if,omitempty"` } // MultiLineRegex contains a regex, which can be split into multiple lines. @@ -44,7 +45,7 @@ type MultiLineRegex struct { func (mlr *MultiLineRegex) UnmarshalYAML(f func(interface{}) error) error { var v interface{} if err := f(&v); err != nil { - return err + return fmt.Errorf("cannot parse multiline regex: %w", err) } s, err := stringValue(v) if err != nil { @@ -224,11 +225,11 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) { return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=drop_if_equal`; got %q", sourceLabels) } case "keep": - if len(sourceLabels) == 0 { + if len(sourceLabels) == 0 && rc.If == nil { return nil, fmt.Errorf("missing `source_labels` for `action=keep`") } case "drop": - if len(sourceLabels) == 0 { + if len(sourceLabels) == 0 && rc.If == nil { return nil, fmt.Errorf("missing `source_labels` for `action=drop`") } case "hashmod": @@ -242,7 +243,7 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) { return nil, fmt.Errorf("unexpected `modulus` for `action=hashmod`: %d; must be greater than 0", modulus) } case "keep_metrics": - if rc.Regex == nil || rc.Regex.s == "" { + if (rc.Regex == nil || rc.Regex.s == "") && rc.If == nil { return nil, fmt.Errorf("`regex` must be non-empty for `action=keep_metrics`") } if len(sourceLabels) > 0 { @@ -251,7 +252,7 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) { sourceLabels = []string{"__name__"} action = "keep" case "drop_metrics": - if rc.Regex == nil || rc.Regex.s == "" { + if (rc.Regex == nil || rc.Regex.s == "") && rc.If == nil { return nil, fmt.Errorf("`regex` must be non-empty for `action=drop_metrics`") } if len(sourceLabels) > 0 { @@ -274,6 +275,7 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) { Modulus: modulus, Replacement: replacement, Action: action, + If: rc.If, regexOriginal: regexOriginalCompiled, hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"), diff --git a/lib/promrelabel/if_expression.go b/lib/promrelabel/if_expression.go new file mode 100644 index 000000000..231330ae8 --- /dev/null +++ b/lib/promrelabel/if_expression.go @@ -0,0 +1,169 @@ +package promrelabel + +import ( + "fmt" + "regexp" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" + "github.com/VictoriaMetrics/metricsql" +) + +// IfExpression represents `if` expression at RelabelConfig. +// +// The `if` expression can contain arbitrary PromQL-like label filters such as `metric_name{filters...}` +type IfExpression struct { + s string + lfs []*labelFilter +} + +// UnmarshalYAML unmarshals ie from YAML passed to f. +func (ie *IfExpression) UnmarshalYAML(f func(interface{}) error) error { + var s string + if err := f(&s); err != nil { + return fmt.Errorf("cannot unmarshal `if` option: %w", err) + } + expr, err := metricsql.Parse(s) + if err != nil { + return fmt.Errorf("cannot parse `if` series selector: %w", err) + } + me, ok := expr.(*metricsql.MetricExpr) + if !ok { + return fmt.Errorf("expecting `if` series selector; got %q", expr.AppendString(nil)) + } + lfs, err := metricExprToLabelFilters(me) + if err != nil { + return fmt.Errorf("cannot parse `if` filters: %w", err) + } + ie.s = s + ie.lfs = lfs + return nil +} + +// MarshalYAML marshals ie to YAML. +func (ie *IfExpression) MarshalYAML() (interface{}, error) { + return ie.s, nil +} + +// Match returns true if ie matches the given labels. +func (ie *IfExpression) Match(labels []prompbmarshal.Label) bool { + for _, lf := range ie.lfs { + if !lf.match(labels) { + return false + } + } + return true +} + +func metricExprToLabelFilters(me *metricsql.MetricExpr) ([]*labelFilter, error) { + lfs := make([]*labelFilter, len(me.LabelFilters)) + for i := range me.LabelFilters { + lf, err := newLabelFilter(&me.LabelFilters[i]) + if err != nil { + return nil, fmt.Errorf("cannot parse %s: %w", me.AppendString(nil), err) + } + lfs[i] = lf + } + return lfs, nil +} + +// labelFilter contains PromQL filter for `{label op "value"}` +type labelFilter struct { + label string + op string + value string + + // re contains compiled regexp for `=~` and `!~` op. + re *regexp.Regexp +} + +func newLabelFilter(mlf *metricsql.LabelFilter) (*labelFilter, error) { + lf := &labelFilter{ + label: toCanonicalLabelName(mlf.Label), + op: getFilterOp(mlf), + value: mlf.Value, + } + if lf.op == "=~" || lf.op == "!~" { + // PromQL regexps are anchored by default. + // See https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors + reString := "^(?:" + lf.value + ")$" + re, err := regexp.Compile(reString) + if err != nil { + return nil, fmt.Errorf("cannot parse regexp for %s: %w", mlf.AppendString(nil), err) + } + lf.re = re + } + return lf, nil +} + +func (lf *labelFilter) match(labels []prompbmarshal.Label) bool { + switch lf.op { + case "=": + return lf.equalValue(labels) + case "!=": + return !lf.equalValue(labels) + case "=~": + return lf.equalRegexp(labels) + case "!~": + return !lf.equalRegexp(labels) + default: + logger.Panicf("BUG: unexpected operation for label filter: %s", lf.op) + } + return false +} + +func (lf *labelFilter) equalValue(labels []prompbmarshal.Label) bool { + labelNameMatches := 0 + for _, label := range labels { + if toCanonicalLabelName(label.Name) != lf.label { + continue + } + labelNameMatches++ + if label.Value == lf.value { + return true + } + } + if labelNameMatches == 0 { + // Special case for {non_existing_label=""}, which matches anything except of non-empty non_existing_label + return lf.value == "" + } + return false +} + +func (lf *labelFilter) equalRegexp(labels []prompbmarshal.Label) bool { + labelNameMatches := 0 + for _, label := range labels { + if toCanonicalLabelName(label.Name) != lf.label { + continue + } + labelNameMatches++ + if lf.re.MatchString(label.Value) { + return true + } + } + if labelNameMatches == 0 { + // Special case for {non_existing_label=~"something|"}, which matches empty non_existing_label + return lf.re.MatchString("") + } + return false +} + +func toCanonicalLabelName(labelName string) string { + if labelName == "__name__" { + return "" + } + return labelName +} + +func getFilterOp(mlf *metricsql.LabelFilter) string { + if mlf.IsNegative { + if mlf.IsRegexp { + return "!~" + } + return "!=" + } + if mlf.IsRegexp { + return "=~" + } + return "=" +} diff --git a/lib/promrelabel/if_expression_test.go b/lib/promrelabel/if_expression_test.go new file mode 100644 index 000000000..972d53b5e --- /dev/null +++ b/lib/promrelabel/if_expression_test.go @@ -0,0 +1,162 @@ +package promrelabel + +import ( + "bytes" + "fmt" + "testing" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus" + "gopkg.in/yaml.v2" +) + +func TestIfExpressionUnmarshalFailure(t *testing.T) { + f := func(s string) { + t.Helper() + var ie IfExpression + err := yaml.UnmarshalStrict([]byte(s), &ie) + if err == nil { + t.Fatalf("expecting non-nil error") + } + } + f(`{`) + f(`{x:y}`) + f(`[]`) + f(`"{"`) + f(`'{'`) + f(`foo{bar`) + f(`foo{bar}`) + f(`foo{bar=`) + f(`foo{bar="`) + f(`foo{bar='`) + f(`foo{bar=~"("}`) + f(`foo{bar!~"("}`) + f(`foo{bar==aaa}`) + f(`foo{bar=="b"}`) + f(`'foo+bar'`) + f(`'foo{bar=~"a[b"}'`) +} + +func TestIfExpressionUnmarshalSuccess(t *testing.T) { + f := func(s string) { + t.Helper() + var ie IfExpression + if err := yaml.UnmarshalStrict([]byte(s), &ie); err != nil { + t.Fatalf("unexpected error during unmarshal: %s", err) + } + b, err := yaml.Marshal(&ie) + if err != nil { + t.Fatalf("unexpected error during marshal: %s", err) + } + b = bytes.TrimSpace(b) + if string(b) != s { + t.Fatalf("unexpected marshaled data;\ngot\n%s\nwant\n%s", b, s) + } + } + f(`'{}'`) + f(`foo`) + f(`foo{bar="baz"}`) + f(`'{a="b", c!="d", e=~"g", h!~"d"}'`) + f(`foo{bar="zs",a=~"b|c"}`) +} + +func TestIfExpressionMatch(t *testing.T) { + f := func(ifExpr, metricWithLabels string) { + t.Helper() + var ie IfExpression + if err := yaml.UnmarshalStrict([]byte(ifExpr), &ie); err != nil { + t.Fatalf("unexpected error during unmarshal: %s", err) + } + labels, err := parseMetricWithLabels(metricWithLabels) + if err != nil { + t.Fatalf("cannot parse %s: %s", metricWithLabels, err) + } + if !ie.Match(labels) { + t.Fatalf("unexpected mismatch of ifExpr=%s for %s", ifExpr, metricWithLabels) + } + } + f(`foo`, `foo`) + f(`foo`, `foo{bar="baz",a="b"}`) + f(`foo{bar="a"}`, `foo{bar="a"}`) + f(`foo{bar="a"}`, `foo{x="y",bar="a",baz="b"}`) + f(`'{a=~"x|abc",y!="z"}'`, `m{x="aa",a="abc"}`) + f(`'{a=~"x|abc",y!="z"}'`, `m{x="aa",a="abc",y="qwe"}`) + f(`'{__name__="foo"}'`, `foo{bar="baz"}`) + f(`'{__name__=~"foo|bar"}'`, `bar`) + f(`'{__name__!=""}'`, `foo`) + f(`'{__name__!=""}'`, `bar{baz="aa",b="c"}`) + f(`'{__name__!~"a.+"}'`, `bar{baz="aa",b="c"}`) + f(`foo{a!~"a.+"}`, `foo{a="baa"}`) + f(`'{foo=""}'`, `bar`) + f(`'{foo!=""}'`, `aa{foo="b"}`) + f(`'{foo=~".*"}'`, `abc`) + f(`'{foo=~".*"}'`, `abc{foo="bar"}`) + f(`'{foo!~".+"}'`, `abc`) + f(`'{foo=~"bar|"}'`, `abc`) + f(`'{foo=~"bar|"}'`, `abc{foo="bar"}`) + f(`'{foo!~"bar|"}'`, `abc{foo="baz"}`) +} + +func TestIfExpressionMismatch(t *testing.T) { + f := func(ifExpr, metricWithLabels string) { + t.Helper() + var ie IfExpression + if err := yaml.UnmarshalStrict([]byte(ifExpr), &ie); err != nil { + t.Fatalf("unexpected error during unmarshal: %s", err) + } + labels, err := parseMetricWithLabels(metricWithLabels) + if err != nil { + t.Fatalf("cannot parse %s: %s", metricWithLabels, err) + } + if ie.Match(labels) { + t.Fatalf("unexpected match of ifExpr=%s for %s", ifExpr, metricWithLabels) + } + } + f(`foo`, `bar`) + f(`foo`, `a{foo="bar"}`) + f(`foo{bar="a"}`, `foo`) + f(`foo{bar="a"}`, `foo{bar="b"}`) + f(`foo{bar="a"}`, `foo{baz="b",a="b"}`) + f(`'{a=~"x|abc",y!="z"}'`, `m{x="aa",a="xabc"}`) + f(`'{a=~"x|abc",y!="z"}'`, `m{x="aa",a="abc",y="z"}`) + f(`'{__name__!~".+"}'`, `foo`) + f(`'{a!~"a.+"}'`, `foo{a="abc"}`) + f(`'{foo=""}'`, `bar{foo="aa"}`) + f(`'{foo!=""}'`, `aa`) + f(`'{foo=~".+"}'`, `abc`) + f(`'{foo!~".+"}'`, `abc{foo="x"}`) + f(`'{foo=~"bar|"}'`, `abc{foo="baz"}`) + f(`'{foo!~"bar|"}'`, `abc`) + f(`'{foo!~"bar|"}'`, `abc{foo="bar"}`) +} + +func parseMetricWithLabels(metricWithLabels string) ([]prompbmarshal.Label, error) { + // add a value to metricWithLabels, so it could be parsed by prometheus protocol parser. + s := metricWithLabels + " 123" + var rows prometheus.Rows + var err error + rows.UnmarshalWithErrLogger(s, func(s string) { + err = fmt.Errorf("error during metric parse: %s", s) + }) + if err != nil { + return nil, err + } + if len(rows.Rows) != 1 { + return nil, fmt.Errorf("unexpected number of rows parsed; got %d; want 1", len(rows.Rows)) + } + r := rows.Rows[0] + var lfs []prompbmarshal.Label + if r.Metric != "" { + lfs = append(lfs, prompbmarshal.Label{ + Name: "__name__", + Value: r.Metric, + }) + } + for _, tag := range r.Tags { + lfs = append(lfs, prompbmarshal.Label{ + Name: tag.Key, + Value: tag.Value, + }) + } + return lfs, nil +} diff --git a/lib/promrelabel/relabel.go b/lib/promrelabel/relabel.go index fa8875986..7950a6e6f 100644 --- a/lib/promrelabel/relabel.go +++ b/lib/promrelabel/relabel.go @@ -23,6 +23,7 @@ type parsedRelabelConfig struct { Modulus uint64 Replacement string Action string + If *IfExpression regexOriginal *regexp.Regexp hasCaptureGroupInTargetLabel bool @@ -137,8 +138,17 @@ func FinalizeLabels(dst, src []prompbmarshal.Label) []prompbmarshal.Label { // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset int) []prompbmarshal.Label { src := labels[labelsOffset:] + if prc.If != nil && !prc.If.Match(labels) { + if prc.Action == "keep" { + // Drop the target on `if` mismatch for `action: keep` + return labels[:labelsOffset] + } + // Do not apply prc actions on `if` mismatch. + return labels + } switch prc.Action { case "replace": + // Store `replacement` at `target_label` if the `regex` matches `source_labels` joined with `separator` bb := relabelBufPool.Get() bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) if prc.Regex == defaultRegexForRelabelConfig && !prc.hasCaptureGroupInTargetLabel { @@ -174,6 +184,8 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset relabelBufPool.Put(bb) return setLabelValue(labels, labelsOffset, nameStr, valueStr) case "replace_all": + // Replace all the occurences of `regex` at `source_labels` joined with `separator` with the `replacement` + // and store the result at `target_label` bb := relabelBufPool.Get() bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) sourceStr := string(bb.B) @@ -208,6 +220,15 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset } return labels case "keep": + // Keep the target if `source_labels` joined with `separator` match the `regex`. + if prc.Regex == defaultRegexForRelabelConfig { + // Fast path for the case with `if` and without explicitly set `regex`: + // + // - action: keep + // if: 'some{label=~"filters"}' + // + return labels + } bb := relabelBufPool.Get() bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) keep := prc.matchString(bytesutil.ToUnsafeString(bb.B)) @@ -217,6 +238,15 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset } return labels case "drop": + // Drop the target if `source_labels` joined with `separator` don't match the `regex`. + if prc.Regex == defaultRegexForRelabelConfig { + // Fast path for the case with `if` and without explicitly set `regex`: + // + // - action: drop + // if: 'some{label=~"filters"}' + // + return labels[:labelsOffset] + } bb := relabelBufPool.Get() bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) drop := prc.matchString(bytesutil.ToUnsafeString(bb.B)) @@ -226,6 +256,7 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset } return labels case "hashmod": + // Calculate the `modulus` from the hash of `source_labels` joined with `separator` and store it at `target_label` bb := relabelBufPool.Get() bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) h := xxhash.Sum64(bb.B) % prc.Modulus @@ -233,6 +264,7 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset relabelBufPool.Put(bb) return setLabelValue(labels, labelsOffset, prc.TargetLabel, value) case "labelmap": + // Replace label names with the `replacement` if they match `regex` for i := range src { label := &src[i] labelName, ok := prc.replaceFullString(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement) @@ -242,12 +274,14 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset } return labels case "labelmap_all": + // Replace all the occurences of `regex` at label names with `replacement` for i := range src { label := &src[i] label.Name, _ = prc.replaceStringSubmatches(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement) } return labels case "labeldrop": + // Drop labels with names matching the `regex` dst := labels[:labelsOffset] for i := range src { label := &src[i] @@ -257,6 +291,7 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset } return dst case "labelkeep": + // Keep labels with names matching the `regex` dst := labels[:labelsOffset] for i := range src { label := &src[i] diff --git a/lib/promrelabel/relabel_test.go b/lib/promrelabel/relabel_test.go index ee9a7b967..bbc176681 100644 --- a/lib/promrelabel/relabel_test.go +++ b/lib/promrelabel/relabel_test.go @@ -134,6 +134,25 @@ func TestApplyRelabelConfigs(t *testing.T) { source_labels: ["foo"] target_label: "bar" regex: ".+" +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "yyy", + }, + }, false, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "yyy", + }, + }) + }) + t.Run("replace-if-miss", func(t *testing.T) { + f(` +- action: replace + if: '{foo="bar"}' + source_labels: ["xxx", "foo"] + target_label: "bar" + replacement: "a-$1-b" `, []prompbmarshal.Label{ { Name: "xxx", @@ -152,6 +171,29 @@ func TestApplyRelabelConfigs(t *testing.T) { source_labels: ["xxx", "foo"] target_label: "bar" replacement: "a-$1-b" +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "yyy", + }, + }, false, []prompbmarshal.Label{ + { + Name: "bar", + Value: "a-yyy;-b", + }, + { + Name: "xxx", + Value: "yyy", + }, + }) + }) + t.Run("replace-if-hit", func(t *testing.T) { + f(` +- action: replace + if: '{xxx=~".y."}' + source_labels: ["xxx", "foo"] + target_label: "bar" + replacement: "a-$1-b" `, []prompbmarshal.Label{ { Name: "xxx", @@ -333,6 +375,26 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("replace_all-if-miss", func(t *testing.T) { + f(` +- action: replace_all + if: 'foo' + source_labels: ["xxx"] + target_label: "xxx" + regex: "-" + replacement: "." +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "a-b-c", + }, + }, false, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "a-b-c", + }, + }) + }) t.Run("replace_all-hit", func(t *testing.T) { f(` - action: replace_all @@ -340,6 +402,26 @@ func TestApplyRelabelConfigs(t *testing.T) { target_label: "xxx" regex: "-" replacement: "." +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "a-b-c", + }, + }, false, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "a.b.c", + }, + }) + }) + t.Run("replace_all-if-hit", func(t *testing.T) { + f(` +- action: replace_all + if: '{non_existing_label=~".*"}' + source_labels: ["xxx"] + target_label: "xxx" + regex: "-" + replacement: "." `, []prompbmarshal.Label{ { Name: "xxx", @@ -530,6 +612,33 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }, true, []prompbmarshal.Label{}) }) + t.Run("keep-if-miss", func(t *testing.T) { + f(` +- action: keep + if: '{foo="bar"}' +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, false, []prompbmarshal.Label{}) + }) + t.Run("keep-if-hit", func(t *testing.T) { + f(` +- action: keep + if: '{foo="yyy"}' +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, false, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }) + }) t.Run("keep-hit", func(t *testing.T) { f(` - action: keep @@ -577,6 +686,33 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }, true, []prompbmarshal.Label{}) }) + t.Run("keep_metrics-if-miss", func(t *testing.T) { + f(` +- action: keep_metrics + if: 'bar' +`, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }, true, []prompbmarshal.Label{}) + }) + t.Run("keep_metrics-if-hit", func(t *testing.T) { + f(` +- action: keep_metrics + if: 'foo' +`, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }, true, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }) + }) t.Run("keep_metrics-hit", func(t *testing.T) { f(` - action: keep_metrics @@ -617,6 +753,33 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("drop-if-miss", func(t *testing.T) { + f(` +- action: drop + if: '{foo="bar"}' +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, true, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }) + }) + t.Run("drop-if-hit", func(t *testing.T) { + f(` +- action: drop + if: '{foo="yyy"}' +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, true, []prompbmarshal.Label{}) + }) t.Run("drop-hit", func(t *testing.T) { f(` - action: drop @@ -659,6 +822,33 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("drop_metrics-if-miss", func(t *testing.T) { + f(` +- action: drop_metrics + if: bar +`, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }, true, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }) + }) + t.Run("drop_metrics-if-hit", func(t *testing.T) { + f(` +- action: drop_metrics + if: foo +`, []prompbmarshal.Label{ + { + Name: "__name__", + Value: "foo", + }, + }, true, []prompbmarshal.Label{}) + }) t.Run("drop_metrics-hit", func(t *testing.T) { f(` - action: drop_metrics @@ -694,6 +884,48 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("hashmod-if-miss", func(t *testing.T) { + f(` +- action: hashmod + if: '{foo="bar"}' + source_labels: [foo] + target_label: aaa + modulus: 123 +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, true, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }) + }) + t.Run("hashmod-if-hit", func(t *testing.T) { + f(` +- action: hashmod + if: '{foo="yyy"}' + source_labels: [foo] + target_label: aaa + modulus: 123 +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + }, true, []prompbmarshal.Label{ + { + Name: "aaa", + Value: "73", + }, + { + Name: "foo", + Value: "yyy", + }, + }) + }) t.Run("hashmod-hit", func(t *testing.T) { f(` - action: hashmod @@ -716,6 +948,62 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("labelmap-copy-label-if-miss", func(t *testing.T) { + f(` +- action: labelmap + if: '{foo="yyy",foobar="aab"}' + regex: "foo" + replacement: "bar" +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }, true, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }) + }) + t.Run("labelmap-copy-label-if-hit", func(t *testing.T) { + f(` +- action: labelmap + if: '{foo="yyy",foobar="aaa"}' + regex: "foo" + replacement: "bar" +`, []prompbmarshal.Label{ + { + Name: "foo", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }, true, []prompbmarshal.Label{ + { + Name: "bar", + Value: "yyy", + }, + { + Name: "foo", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }) + }) t.Run("labelmap-copy-label", func(t *testing.T) { f(` - action: labelmap @@ -830,6 +1118,58 @@ func TestApplyRelabelConfigs(t *testing.T) { }, }) }) + t.Run("labelmap_all-if-miss", func(t *testing.T) { + f(` +- action: labelmap_all + if: foobar + regex: "\\." + replacement: "-" +`, []prompbmarshal.Label{ + { + Name: "foo.bar.baz", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }, true, []prompbmarshal.Label{ + { + Name: "foo.bar.baz", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }) + }) + t.Run("labelmap_all-if-hit", func(t *testing.T) { + f(` +- action: labelmap_all + if: '{foo.bar.baz="yyy"}' + regex: "\\." + replacement: "-" +`, []prompbmarshal.Label{ + { + Name: "foo.bar.baz", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }, true, []prompbmarshal.Label{ + { + Name: "foo-bar-baz", + Value: "yyy", + }, + { + Name: "foobar", + Value: "aaa", + }, + }) + }) t.Run("labelmap_all", func(t *testing.T) { f(` - action: labelmap_all @@ -895,6 +1235,66 @@ func TestApplyRelabelConfigs(t *testing.T) { Value: "bbb", }, }) + // if-miss + f(` +- action: labeldrop + if: foo + regex: dropme +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "yyy", + }, + { + Name: "dropme", + Value: "aaa", + }, + { + Name: "foo", + Value: "bar", + }, + }, false, []prompbmarshal.Label{ + { + Name: "dropme", + Value: "aaa", + }, + { + Name: "foo", + Value: "bar", + }, + { + Name: "xxx", + Value: "yyy", + }, + }) + // if-hit + f(` +- action: labeldrop + if: '{xxx="yyy"}' + regex: dropme +`, []prompbmarshal.Label{ + { + Name: "xxx", + Value: "yyy", + }, + { + Name: "dropme", + Value: "aaa", + }, + { + Name: "foo", + Value: "bar", + }, + }, false, []prompbmarshal.Label{ + { + Name: "foo", + Value: "bar", + }, + { + Name: "xxx", + Value: "yyy", + }, + }) f(` - action: labeldrop regex: dropme @@ -1059,6 +1459,62 @@ func TestApplyRelabelConfigs(t *testing.T) { Value: "aaa", }, }) + // if-miss + f(` +- action: labelkeep + if: '{aaaa="awefx"}' + regex: keepme +`, []prompbmarshal.Label{ + { + Name: "keepme", + Value: "aaa", + }, + { + Name: "aaaa", + Value: "awef", + }, + { + Name: "keepme-aaa", + Value: "234", + }, + }, false, []prompbmarshal.Label{ + { + Name: "aaaa", + Value: "awef", + }, + { + Name: "keepme", + Value: "aaa", + }, + { + Name: "keepme-aaa", + Value: "234", + }, + }) + // if-hit + f(` +- action: labelkeep + if: '{aaaa="awef"}' + regex: keepme +`, []prompbmarshal.Label{ + { + Name: "keepme", + Value: "aaa", + }, + { + Name: "aaaa", + Value: "awef", + }, + { + Name: "keepme-aaa", + Value: "234", + }, + }, false, []prompbmarshal.Label{ + { + Name: "keepme", + Value: "aaa", + }, + }) f(` - action: labelkeep regex: keepme