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 }