mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
9fc2817f41
Cache `action: replace` results for non-trivial regexs and return them next time instead of performing CPU-intensive regex replacement. Optimize also `action: labelmap_all` and `action: replace_all` in the same way.
381 lines
12 KiB
Go
381 lines
12 KiB
Go
package promrelabel
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/regexutil"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// RelabelConfig represents relabel config.
|
|
//
|
|
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
|
|
type RelabelConfig struct {
|
|
SourceLabels []string `yaml:"source_labels,flow,omitempty"`
|
|
Separator *string `yaml:"separator,omitempty"`
|
|
TargetLabel string `yaml:"target_label,omitempty"`
|
|
Regex *MultiLineRegex `yaml:"regex,omitempty"`
|
|
Modulus uint64 `yaml:"modulus,omitempty"`
|
|
Replacement *string `yaml:"replacement,omitempty"`
|
|
Action string `yaml:"action,omitempty"`
|
|
If *IfExpression `yaml:"if,omitempty"`
|
|
|
|
// Match is used together with Labels for `action: graphite`. For example:
|
|
// - action: graphite
|
|
// match: 'foo.*.*.bar'
|
|
// labels:
|
|
// job: '$1'
|
|
// instance: '${2}:8080'
|
|
Match string `yaml:"match,omitempty"`
|
|
|
|
// Labels is used together with Match for `action: graphite`. For example:
|
|
// - action: graphite
|
|
// match: 'foo.*.*.bar'
|
|
// labels:
|
|
// job: '$1'
|
|
// instance: '${2}:8080'
|
|
Labels map[string]string `yaml:"labels,omitempty"`
|
|
}
|
|
|
|
// MultiLineRegex contains a regex, which can be split into multiple lines.
|
|
//
|
|
// These lines are joined with "|" then.
|
|
// For example:
|
|
//
|
|
// regex:
|
|
// - foo
|
|
// - bar
|
|
//
|
|
// is equivalent to:
|
|
//
|
|
// regex: "foo|bar"
|
|
type MultiLineRegex struct {
|
|
S string
|
|
}
|
|
|
|
// UnmarshalYAML unmarshals mlr from YAML passed to f.
|
|
func (mlr *MultiLineRegex) UnmarshalYAML(f func(interface{}) error) error {
|
|
var v interface{}
|
|
if err := f(&v); err != nil {
|
|
return fmt.Errorf("cannot parse multiline regex: %w", err)
|
|
}
|
|
s, err := stringValue(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mlr.S = s
|
|
return nil
|
|
}
|
|
|
|
func stringValue(v interface{}) (string, error) {
|
|
if v == nil {
|
|
return "null", nil
|
|
}
|
|
switch x := v.(type) {
|
|
case []interface{}:
|
|
a := make([]string, len(x))
|
|
for i, xx := range x {
|
|
s, err := stringValue(xx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
a[i] = s
|
|
}
|
|
return strings.Join(a, "|"), nil
|
|
case string:
|
|
return x, nil
|
|
case float64:
|
|
return strconv.FormatFloat(x, 'f', -1, 64), nil
|
|
case int:
|
|
return strconv.Itoa(x), nil
|
|
case bool:
|
|
if x {
|
|
return "true", nil
|
|
}
|
|
return "false", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected type for `regex`: %T; want string or []string", v)
|
|
}
|
|
}
|
|
|
|
// MarshalYAML marshals mlr to YAML.
|
|
func (mlr *MultiLineRegex) MarshalYAML() (interface{}, error) {
|
|
if strings.ContainsAny(mlr.S, "([") {
|
|
// The mlr.S contains groups. Fall back to returning the regexp as is without splitting it into parts.
|
|
// This fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2928 .
|
|
return mlr.S, nil
|
|
}
|
|
a := strings.Split(mlr.S, "|")
|
|
if len(a) == 1 {
|
|
return a[0], nil
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// ParsedConfigs represents parsed relabel configs.
|
|
type ParsedConfigs struct {
|
|
prcs []*parsedRelabelConfig
|
|
relabelDebug bool
|
|
}
|
|
|
|
// Len returns the number of relabel configs in pcs.
|
|
func (pcs *ParsedConfigs) Len() int {
|
|
if pcs == nil {
|
|
return 0
|
|
}
|
|
return len(pcs.prcs)
|
|
}
|
|
|
|
// String returns human-readabale representation for pcs.
|
|
func (pcs *ParsedConfigs) String() string {
|
|
if pcs == nil {
|
|
return ""
|
|
}
|
|
var a []string
|
|
for _, prc := range pcs.prcs {
|
|
s := "[" + prc.String() + "]"
|
|
a = append(a, s)
|
|
}
|
|
return fmt.Sprintf("%s, relabelDebug=%v", strings.Join(a, ","), pcs.relabelDebug)
|
|
}
|
|
|
|
// LoadRelabelConfigs loads relabel configs from the given path.
|
|
func LoadRelabelConfigs(path string, relabelDebug bool) (*ParsedConfigs, error) {
|
|
data, err := fs.ReadFileOrHTTP(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read `relabel_configs` from %q: %w", path, err)
|
|
}
|
|
data = envtemplate.Replace(data)
|
|
pcs, err := ParseRelabelConfigsData(data, relabelDebug)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot unmarshal `relabel_configs` from %q: %w", path, err)
|
|
}
|
|
return pcs, nil
|
|
}
|
|
|
|
// ParseRelabelConfigsData parses relabel configs from the given data.
|
|
func ParseRelabelConfigsData(data []byte, relabelDebug bool) (*ParsedConfigs, error) {
|
|
var rcs []RelabelConfig
|
|
if err := yaml.UnmarshalStrict(data, &rcs); err != nil {
|
|
return nil, err
|
|
}
|
|
return ParseRelabelConfigs(rcs, relabelDebug)
|
|
}
|
|
|
|
// ParseRelabelConfigs parses rcs to dst.
|
|
func ParseRelabelConfigs(rcs []RelabelConfig, relabelDebug bool) (*ParsedConfigs, error) {
|
|
if len(rcs) == 0 {
|
|
return nil, nil
|
|
}
|
|
prcs := make([]*parsedRelabelConfig, len(rcs))
|
|
for i := range rcs {
|
|
prc, err := parseRelabelConfig(&rcs[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error when parsing `relabel_config` #%d: %w", i+1, err)
|
|
}
|
|
prcs[i] = prc
|
|
}
|
|
return &ParsedConfigs{
|
|
prcs: prcs,
|
|
relabelDebug: relabelDebug,
|
|
}, nil
|
|
}
|
|
|
|
var (
|
|
defaultOriginalRegexForRelabelConfig = regexp.MustCompile(".*")
|
|
defaultRegexForRelabelConfig = regexp.MustCompile("^(.*)$")
|
|
defaultPromRegex = func() *regexutil.PromRegex {
|
|
pr, err := regexutil.NewPromRegex(".*")
|
|
if err != nil {
|
|
panic(fmt.Errorf("BUG: unexpected error: %s", err))
|
|
}
|
|
return pr
|
|
}()
|
|
)
|
|
|
|
func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
|
|
sourceLabels := rc.SourceLabels
|
|
separator := ";"
|
|
if rc.Separator != nil {
|
|
separator = *rc.Separator
|
|
}
|
|
action := strings.ToLower(rc.Action)
|
|
if action == "" {
|
|
action = "replace"
|
|
}
|
|
targetLabel := rc.TargetLabel
|
|
regexAnchored := defaultRegexForRelabelConfig
|
|
regexOriginalCompiled := defaultOriginalRegexForRelabelConfig
|
|
promRegex := defaultPromRegex
|
|
if rc.Regex != nil && !isDefaultRegex(rc.Regex.S) {
|
|
regex := rc.Regex.S
|
|
regexOrig := regex
|
|
if rc.Action != "replace_all" && rc.Action != "labelmap_all" {
|
|
regex = regexutil.RemoveStartEndAnchors(regex)
|
|
regexOrig = regex
|
|
regex = "^(?:" + regex + ")$"
|
|
}
|
|
re, err := regexp.Compile(regex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse `regex` %q: %w", regex, err)
|
|
}
|
|
regexAnchored = re
|
|
reOriginal, err := regexp.Compile(regexOrig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse `regex` %q: %w", regexOrig, err)
|
|
}
|
|
regexOriginalCompiled = reOriginal
|
|
promRegex, err = regexutil.NewPromRegex(regexOrig)
|
|
if err != nil {
|
|
logger.Panicf("BUG: cannot parse already parsed regex %q: %s", regexOrig, err)
|
|
}
|
|
}
|
|
modulus := rc.Modulus
|
|
replacement := "$1"
|
|
if rc.Replacement != nil {
|
|
replacement = *rc.Replacement
|
|
}
|
|
var graphiteMatchTemplate *graphiteMatchTemplate
|
|
if rc.Match != "" {
|
|
graphiteMatchTemplate = newGraphiteMatchTemplate(rc.Match)
|
|
}
|
|
var graphiteLabelRules []graphiteLabelRule
|
|
if rc.Labels != nil {
|
|
graphiteLabelRules = newGraphiteLabelRules(rc.Labels)
|
|
}
|
|
switch action {
|
|
case "graphite":
|
|
if graphiteMatchTemplate == nil {
|
|
return nil, fmt.Errorf("missing `match` for `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
if len(graphiteLabelRules) == 0 {
|
|
return nil, fmt.Errorf("missing `labels` for `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
if len(rc.SourceLabels) > 0 {
|
|
return nil, fmt.Errorf("`source_labels` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
if rc.TargetLabel != "" {
|
|
return nil, fmt.Errorf("`target_label` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
if rc.Replacement != nil {
|
|
return nil, fmt.Errorf("`replacement` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
if rc.Regex != nil {
|
|
return nil, fmt.Errorf("`regex` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
|
|
}
|
|
case "replace":
|
|
if targetLabel == "" {
|
|
return nil, fmt.Errorf("missing `target_label` for `action=replace`")
|
|
}
|
|
case "replace_all":
|
|
if len(sourceLabels) == 0 {
|
|
return nil, fmt.Errorf("missing `source_labels` for `action=replace_all`")
|
|
}
|
|
if targetLabel == "" {
|
|
return nil, fmt.Errorf("missing `target_label` for `action=replace_all`")
|
|
}
|
|
case "keep_if_equal":
|
|
if len(sourceLabels) < 2 {
|
|
return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=keep_if_equal`; got %q", sourceLabels)
|
|
}
|
|
case "drop_if_equal":
|
|
if len(sourceLabels) < 2 {
|
|
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 && rc.If == nil {
|
|
return nil, fmt.Errorf("missing `source_labels` for `action=keep`")
|
|
}
|
|
case "drop":
|
|
if len(sourceLabels) == 0 && rc.If == nil {
|
|
return nil, fmt.Errorf("missing `source_labels` for `action=drop`")
|
|
}
|
|
case "hashmod":
|
|
if len(sourceLabels) == 0 {
|
|
return nil, fmt.Errorf("missing `source_labels` for `action=hashmod`")
|
|
}
|
|
if targetLabel == "" {
|
|
return nil, fmt.Errorf("missing `target_label` for `action=hashmod`")
|
|
}
|
|
if modulus < 1 {
|
|
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 == "") && rc.If == nil {
|
|
return nil, fmt.Errorf("`regex` must be non-empty for `action=keep_metrics`")
|
|
}
|
|
if len(sourceLabels) > 0 {
|
|
return nil, fmt.Errorf("`source_labels` must be empty for `action=keep_metrics`; got %q", sourceLabels)
|
|
}
|
|
sourceLabels = []string{"__name__"}
|
|
action = "keep"
|
|
case "drop_metrics":
|
|
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 {
|
|
return nil, fmt.Errorf("`source_labels` must be empty for `action=drop_metrics`; got %q", sourceLabels)
|
|
}
|
|
sourceLabels = []string{"__name__"}
|
|
action = "drop"
|
|
case "uppercase", "lowercase":
|
|
if len(sourceLabels) == 0 {
|
|
return nil, fmt.Errorf("missing `source_labels` for `action=%s`", action)
|
|
}
|
|
if targetLabel == "" {
|
|
return nil, fmt.Errorf("missing `target_label` for `action=%s`", action)
|
|
}
|
|
case "labelmap":
|
|
case "labelmap_all":
|
|
case "labeldrop":
|
|
case "labelkeep":
|
|
default:
|
|
return nil, fmt.Errorf("unknown `action` %q", action)
|
|
}
|
|
if action != "graphite" {
|
|
if graphiteMatchTemplate != nil {
|
|
return nil, fmt.Errorf("`match` config cannot be applied to `action=%s`; it is applied only to `action=graphite`", action)
|
|
}
|
|
if len(graphiteLabelRules) > 0 {
|
|
return nil, fmt.Errorf("`labels` config cannot be applied to `action=%s`; it is applied only to `action=graphite`", action)
|
|
}
|
|
}
|
|
prc := &parsedRelabelConfig{
|
|
SourceLabels: sourceLabels,
|
|
Separator: separator,
|
|
TargetLabel: targetLabel,
|
|
RegexAnchored: regexAnchored,
|
|
Modulus: modulus,
|
|
Replacement: replacement,
|
|
Action: action,
|
|
If: rc.If,
|
|
|
|
graphiteMatchTemplate: graphiteMatchTemplate,
|
|
graphiteLabelRules: graphiteLabelRules,
|
|
|
|
regex: promRegex,
|
|
regexOriginal: regexOriginalCompiled,
|
|
|
|
hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"),
|
|
hasCaptureGroupInReplacement: strings.Contains(replacement, "$"),
|
|
hasLabelReferenceInReplacement: strings.Contains(replacement, "{{"),
|
|
}
|
|
prc.stringReplacer = bytesutil.NewFastStringTransformer(prc.replaceFullStringSlow)
|
|
prc.submatchReplacer = bytesutil.NewFastStringTransformer(prc.replaceStringSubmatchesSlow)
|
|
return prc, nil
|
|
}
|
|
|
|
func isDefaultRegex(expr string) bool {
|
|
prefix, suffix := regexutil.Simplify(expr)
|
|
if prefix != "" {
|
|
return false
|
|
}
|
|
return suffix == ".*"
|
|
}
|