lib/promrelabel: allows regex capture groups in target_label like Prometheus does

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/569
This commit is contained in:
Aliaksandr Valialkin 2020-06-19 02:20:29 +03:00
parent ac3700ed1e
commit 5820c0ffb7
5 changed files with 160 additions and 103 deletions

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"regexp" "regexp"
"strings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -124,6 +125,9 @@ func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedR
Modulus: modulus, Modulus: modulus,
Replacement: replacement, Replacement: replacement,
Action: action, Action: action,
hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"),
hasCaptureGroupInReplacement: strings.Contains(replacement, "$"),
}) })
return dst, nil return dst, nil
} }

View file

@ -60,6 +60,8 @@ func TestParseRelabelConfigsSuccess(t *testing.T) {
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
Action: "replace", Action: "replace",
hasCaptureGroupInReplacement: true,
}, },
}) })
} }

View file

@ -23,6 +23,9 @@ type ParsedRelabelConfig struct {
Modulus uint64 Modulus uint64
Replacement string Replacement string
Action string Action string
hasCaptureGroupInTargetLabel bool
hasCaptureGroupInReplacement bool
} }
// String returns human-readable representation for prc. // String returns human-readable representation for prc.
@ -103,51 +106,52 @@ func FinalizeLabels(dst, src []prompbmarshal.Label) []prompbmarshal.Label {
return dst return dst
} }
// applyRelabelConfig applies relabeling according to cfg. // applyRelabelConfig applies relabeling according to prc.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *ParsedRelabelConfig) []prompbmarshal.Label { func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *ParsedRelabelConfig) []prompbmarshal.Label {
src := labels[labelsOffset:] src := labels[labelsOffset:]
switch cfg.Action { switch prc.Action {
case "replace": case "replace":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, cfg.SourceLabels, cfg.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
if len(bb.B) == 0 && cfg.Regex == defaultRegexForRelabelConfig && !strings.Contains(cfg.Replacement, "$") { if len(bb.B) == 0 && prc.Regex == defaultRegexForRelabelConfig && !prc.hasCaptureGroupInReplacement && !prc.hasCaptureGroupInTargetLabel {
// Fast path for the following rule that just sets label value: // Fast path for the following rule that just sets label value:
// - target_label: foobar // - target_label: foobar
// replacement: something-here // replacement: something-here
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
return setLabelValue(labels, labelsOffset, cfg.TargetLabel, cfg.Replacement) return setLabelValue(labels, labelsOffset, prc.TargetLabel, prc.Replacement)
} }
match := cfg.Regex.FindSubmatchIndex(bb.B) match := prc.Regex.FindSubmatchIndex(bb.B)
if match == nil { if match == nil {
// Fast path - nothing to replace. // Fast path - nothing to replace.
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
return labels return labels
} }
sourceStr := bytesutil.ToUnsafeString(bb.B) sourceStr := bytesutil.ToUnsafeString(bb.B)
value := relabelBufPool.Get() nameStr := prc.TargetLabel
value.B = cfg.Regex.ExpandString(value.B[:0], cfg.Replacement, sourceStr, match) if prc.hasCaptureGroupInTargetLabel {
nameStr = prc.expandCaptureGroups(nameStr, sourceStr, match)
}
valueStr := prc.expandCaptureGroups(prc.Replacement, sourceStr, match)
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
valueStr := string(value.B) return setLabelValue(labels, labelsOffset, nameStr, valueStr)
relabelBufPool.Put(value)
return setLabelValue(labels, labelsOffset, cfg.TargetLabel, valueStr)
case "replace_all": case "replace_all":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, cfg.SourceLabels, cfg.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
if !cfg.Regex.Match(bb.B) { if !prc.Regex.Match(bb.B) {
// Fast path - nothing to replace. // Fast path - nothing to replace.
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
return labels return labels
} }
sourceStr := string(bb.B) // Make a copy of bb, since it can be returned from ReplaceAllString sourceStr := string(bb.B) // Make a copy of bb, since it can be returned from ReplaceAllString
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
valueStr := cfg.Regex.ReplaceAllString(sourceStr, cfg.Replacement) valueStr := prc.Regex.ReplaceAllString(sourceStr, prc.Replacement)
return setLabelValue(labels, labelsOffset, cfg.TargetLabel, valueStr) return setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
case "keep": case "keep":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, cfg.SourceLabels, cfg.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
keep := cfg.Regex.Match(bb.B) keep := prc.Regex.Match(bb.B)
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
if !keep { if !keep {
return labels[:labelsOffset] return labels[:labelsOffset]
@ -155,8 +159,8 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
return labels return labels
case "drop": case "drop":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, cfg.SourceLabels, cfg.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
drop := cfg.Regex.Match(bb.B) drop := prc.Regex.Match(bb.B)
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
if drop { if drop {
return labels[:labelsOffset] return labels[:labelsOffset]
@ -164,20 +168,20 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
return labels return labels
case "hashmod": case "hashmod":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, cfg.SourceLabels, cfg.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
h := xxhash.Sum64(bb.B) % cfg.Modulus h := xxhash.Sum64(bb.B) % prc.Modulus
value := strconv.Itoa(int(h)) value := strconv.Itoa(int(h))
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
return setLabelValue(labels, labelsOffset, cfg.TargetLabel, value) return setLabelValue(labels, labelsOffset, prc.TargetLabel, value)
case "labelmap": case "labelmap":
for i := range src { for i := range src {
label := &src[i] label := &src[i]
match := cfg.Regex.FindStringSubmatchIndex(label.Name) match := prc.Regex.FindStringSubmatchIndex(label.Name)
if match == nil { if match == nil {
continue continue
} }
value := relabelBufPool.Get() value := relabelBufPool.Get()
value.B = cfg.Regex.ExpandString(value.B[:0], cfg.Replacement, label.Name, match) value.B = prc.Regex.ExpandString(value.B[:0], prc.Replacement, label.Name, match)
label.Name = string(value.B) label.Name = string(value.B)
relabelBufPool.Put(value) relabelBufPool.Put(value)
} }
@ -185,16 +189,16 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
case "labelmap_all": case "labelmap_all":
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if !cfg.Regex.MatchString(label.Name) { if !prc.Regex.MatchString(label.Name) {
continue continue
} }
label.Name = cfg.Regex.ReplaceAllString(label.Name, cfg.Replacement) label.Name = prc.Regex.ReplaceAllString(label.Name, prc.Replacement)
} }
return labels return labels
case "labeldrop": case "labeldrop":
keepSrc := true keepSrc := true
for i := range src { for i := range src {
if cfg.Regex.MatchString(src[i].Name) { if prc.Regex.MatchString(src[i].Name) {
keepSrc = false keepSrc = false
break break
} }
@ -205,7 +209,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
dst := labels[:labelsOffset] dst := labels[:labelsOffset]
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if !cfg.Regex.MatchString(label.Name) { if !prc.Regex.MatchString(label.Name) {
dst = append(dst, *label) dst = append(dst, *label)
} }
} }
@ -213,7 +217,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
case "labelkeep": case "labelkeep":
keepSrc := true keepSrc := true
for i := range src { for i := range src {
if !cfg.Regex.MatchString(src[i].Name) { if !prc.Regex.MatchString(src[i].Name) {
keepSrc = false keepSrc = false
break break
} }
@ -224,17 +228,25 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, cfg *Par
dst := labels[:labelsOffset] dst := labels[:labelsOffset]
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if cfg.Regex.MatchString(label.Name) { if prc.Regex.MatchString(label.Name) {
dst = append(dst, *label) dst = append(dst, *label)
} }
} }
return dst return dst
default: default:
logger.Panicf("BUG: unknown `action`: %q", cfg.Action) logger.Panicf("BUG: unknown `action`: %q", prc.Action)
return labels return labels
} }
} }
func (prc *ParsedRelabelConfig) expandCaptureGroups(template, source string, match []int) string {
bb := relabelBufPool.Get()
bb.B = prc.Regex.ExpandString(bb.B[:0], template, source, match)
s := string(bb.B)
relabelBufPool.Put(bb)
return s
}
var relabelBufPool bytesutil.ByteBufferPool var relabelBufPool bytesutil.ByteBufferPool
func concatLabelValues(dst []byte, labels []prompbmarshal.Label, labelNames []string, separator string) []byte { func concatLabelValues(dst []byte, labels []prompbmarshal.Label, labelNames []string, separator string) []byte {

View file

@ -57,28 +57,31 @@ func TestApplyRelabelConfigs(t *testing.T) {
t.Run("replace-miss", func(t *testing.T) { t.Run("replace-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, nil, false, []prompbmarshal.Label{}) }, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, nil, false, []prompbmarshal.Label{}) }, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -93,11 +96,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: regexp.MustCompile(".+"), Regex: regexp.MustCompile(".+"),
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -114,12 +118,13 @@ func TestApplyRelabelConfigs(t *testing.T) {
t.Run("replace-hit", func(t *testing.T) { t.Run("replace-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"xxx", "foo"}, SourceLabels: []string{"xxx", "foo"},
Separator: ";", Separator: ";",
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1-b", Replacement: "a-$1-b",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -137,31 +142,62 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, },
}) })
}) })
t.Run("replace-hit-target-label-with-capture-group", func(t *testing.T) {
f([]ParsedRelabelConfig{
{
Action: "replace",
SourceLabels: []string{"xxx", "foo"},
Separator: ";",
TargetLabel: "bar-$1",
Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1-b",
hasCaptureGroupInTargetLabel: true,
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{
Name: "xxx",
Value: "yyy",
},
}, false, []prompbmarshal.Label{
{
Name: "bar-yyy;",
Value: "a-yyy;-b",
},
{
Name: "xxx",
Value: "yyy",
},
})
})
t.Run("replace_all-miss", func(t *testing.T) { t.Run("replace_all-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace_all", Action: "replace_all",
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, nil, false, []prompbmarshal.Label{}) }, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace_all", Action: "replace_all",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, nil, false, []prompbmarshal.Label{}) }, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace_all", Action: "replace_all",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -176,11 +212,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace_all", Action: "replace_all",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: regexp.MustCompile(".+"), Regex: regexp.MustCompile(".+"),
Replacement: "$1", Replacement: "$1",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -197,12 +234,13 @@ func TestApplyRelabelConfigs(t *testing.T) {
t.Run("replace_all-hit", func(t *testing.T) { t.Run("replace_all-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace_all", Action: "replace_all",
SourceLabels: []string{"xxx", "foo"}, SourceLabels: []string{"xxx", "foo"},
Separator: ";", Separator: ";",
TargetLabel: "xxx", TargetLabel: "xxx",
Regex: regexp.MustCompile("(;)"), Regex: regexp.MustCompile("(;)"),
Replacement: "-$1-", Replacement: "-$1-",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {
@ -219,11 +257,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
t.Run("replace-add-multi-labels", func(t *testing.T) { t.Run("replace-add-multi-labels", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"xxx"}, SourceLabels: []string{"xxx"},
TargetLabel: "bar", TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1", Replacement: "a-$1",
hasCaptureGroupInReplacement: true,
}, },
{ {
Action: "replace", Action: "replace",
@ -263,11 +302,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
t.Run("replace-self", func(t *testing.T) { t.Run("replace-self", func(t *testing.T) {
f([]ParsedRelabelConfig{ f([]ParsedRelabelConfig{
{ {
Action: "replace", Action: "replace",
SourceLabels: []string{"foo"}, SourceLabels: []string{"foo"},
TargetLabel: "foo", TargetLabel: "foo",
Regex: defaultRegexForRelabelConfig, Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1", Replacement: "a-$1",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, []prompbmarshal.Label{
{ {

View file

@ -952,6 +952,14 @@ scrape_configs:
AuthConfig: &promauth.Config{}, AuthConfig: &promauth.Config{},
}, },
}) })
prcs, err := promrelabel.ParseRelabelConfigs(nil, []promrelabel.RelabelConfig{{
SourceLabels: []string{"foo"},
TargetLabel: "abc",
}})
if err != nil {
t.Fatalf("unexpected error when parsing relabel configs: %s", err)
}
f(` f(`
scrape_configs: scrape_configs:
- job_name: foo - job_name: foo
@ -987,17 +995,8 @@ scrape_configs:
Value: "foo", Value: "foo",
}, },
}, },
AuthConfig: &promauth.Config{}, AuthConfig: &promauth.Config{},
MetricRelabelConfigs: []promrelabel.ParsedRelabelConfig{ MetricRelabelConfigs: prcs,
{
SourceLabels: []string{"foo"},
Separator: ";",
TargetLabel: "abc",
Regex: defaultRegexForRelabelConfig,
Replacement: "$1",
Action: "replace",
},
},
}, },
}) })
f(` f(`