github.com/grafana/pyroscope@v1.18.0/pkg/model/recording_rule.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	prometheusmodel "github.com/prometheus/common/model"
     8  	"github.com/prometheus/prometheus/model/labels"
     9  	"github.com/prometheus/prometheus/promql/parser"
    10  
    11  	settingsv1 "github.com/grafana/pyroscope/api/gen/proto/go/settings/v1"
    12  )
    13  
    14  type RecordingRule struct {
    15  	Matchers       []*labels.Matcher
    16  	GroupBy        []string
    17  	ExternalLabels labels.Labels
    18  	FunctionName   string
    19  }
    20  
    21  const (
    22  	metricNamePrefix = "profiles_recorded_"
    23  	RuleIDLabel      = "profiles_rule_id"
    24  )
    25  
    26  var uniqueLabels = map[string]bool{
    27  	RuleIDLabel:                     true,
    28  	prometheusmodel.MetricNameLabel: true,
    29  }
    30  
    31  func NewRecordingRule(rule *settingsv1.RecordingRule) (*RecordingRule, error) {
    32  	sb := labels.NewScratchBuilder(len(rule.ExternalLabels) + 1)
    33  	return newRecordingRuleWithBuilder(rule, &sb)
    34  }
    35  
    36  func newRecordingRuleWithBuilder(rule *settingsv1.RecordingRule, sb *labels.ScratchBuilder) (*RecordingRule, error) {
    37  	// validate metric name
    38  	if err := ValidateMetricName(rule.MetricName); err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	// ensure __profile_type__ matcher is present
    43  	matchers, err := parseMatchers(rule.Matchers)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("failed to parse matchers: %w", err)
    46  	}
    47  	var profileTypeMatcher *labels.Matcher
    48  	for _, matcher := range matchers {
    49  		if matcher.Name == LabelNameProfileType {
    50  			profileTypeMatcher = matcher
    51  			break
    52  		}
    53  	}
    54  	if profileTypeMatcher == nil {
    55  		return nil, fmt.Errorf("no __profile_type__ matcher present")
    56  	}
    57  	if profileTypeMatcher.Type != labels.MatchEqual {
    58  		return nil, fmt.Errorf("__profile_type__ matcher is not an equality")
    59  	}
    60  	var functionName string
    61  	if rule.StacktraceFilter != nil {
    62  		if rule.StacktraceFilter.FunctionName != nil {
    63  			functionName = rule.StacktraceFilter.FunctionName.FunctionName
    64  		}
    65  	}
    66  
    67  	// validate group_by label names for Prometheus compatibility
    68  	for _, labelName := range rule.GroupBy {
    69  		name := prometheusmodel.LabelName(labelName)
    70  		if !prometheusmodel.LegacyValidation.IsValidLabelName(string(name)) {
    71  			return nil, fmt.Errorf("group_by label %q must match %s", labelName, prometheusmodel.LabelNameRE.String())
    72  		}
    73  	}
    74  
    75  	sb.Reset()
    76  	for _, lbl := range rule.ExternalLabels {
    77  		// ensure no __name__ or profiles_rule_id labels already exist
    78  		if uniqueLabels[lbl.Name] {
    79  			// skip
    80  			continue
    81  		}
    82  		// validate external label names for Prometheus compatibility
    83  		name := prometheusmodel.LabelName(lbl.Name)
    84  		if !prometheusmodel.LegacyValidation.IsValidLabelName(string(name)) {
    85  			return nil, fmt.Errorf("external_labels name %q must match %s", lbl.Name, prometheusmodel.LabelNameRE.String())
    86  		}
    87  		sb.Add(lbl.Name, lbl.Value)
    88  	}
    89  
    90  	// trust rule.MetricName
    91  	sb.Add(prometheusmodel.MetricNameLabel, rule.MetricName)
    92  	// Inject recording rule Id
    93  	sb.Add(RuleIDLabel, rule.Id)
    94  
    95  	sb.Sort()
    96  
    97  	return &RecordingRule{
    98  		Matchers:       matchers,
    99  		GroupBy:        rule.GroupBy,
   100  		ExternalLabels: sb.Labels(),
   101  		FunctionName:   functionName,
   102  	}, nil
   103  }
   104  
   105  func parseMatchers(matchers []string) ([]*labels.Matcher, error) {
   106  	parsed := make([]*labels.Matcher, 0, len(matchers))
   107  	for _, m := range matchers {
   108  		s, err := parser.ParseMetricSelector(m)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  		parsed = append(parsed, s...)
   113  	}
   114  	return parsed, nil
   115  }
   116  
   117  func ValidateMetricName(name string) error {
   118  	if !prometheusmodel.LegacyValidation.IsValidMetricName(name) {
   119  		return fmt.Errorf("invalid metric name: %s", name)
   120  	}
   121  	if !strings.HasPrefix(name, metricNamePrefix) {
   122  		return fmt.Errorf("metric name must start with %s", metricNamePrefix)
   123  	}
   124  	return nil
   125  }