github.com/grafana/pyroscope@v1.18.0/pkg/settings/recording/recording.go (about)

     1  package recording
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"errors"
     7  	"fmt"
     8  	"math/rand"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  
    14  	"connectrpc.com/connect"
    15  	"github.com/go-kit/log"
    16  	"github.com/go-kit/log/level"
    17  	"github.com/grafana/dskit/tenant"
    18  	prom "github.com/prometheus/common/model"
    19  	"github.com/prometheus/prometheus/model/labels"
    20  	"github.com/prometheus/prometheus/promql/parser"
    21  	"github.com/thanos-io/objstore"
    22  
    23  	settingsv1 "github.com/grafana/pyroscope/api/gen/proto/go/settings/v1"
    24  	"github.com/grafana/pyroscope/api/gen/proto/go/settings/v1/settingsv1connect"
    25  	"github.com/grafana/pyroscope/pkg/model"
    26  	"github.com/grafana/pyroscope/pkg/settings/store"
    27  	"github.com/grafana/pyroscope/pkg/validation"
    28  )
    29  
    30  var _ settingsv1connect.RecordingRulesServiceHandler = (*RecordingRules)(nil)
    31  
    32  func New(bucket objstore.Bucket, logger log.Logger, overrides *validation.Overrides) *RecordingRules {
    33  	return &RecordingRules{
    34  		bucket:    bucket,
    35  		logger:    logger,
    36  		stores:    make(map[store.Key]*bucketStore),
    37  		overrides: overrides,
    38  	}
    39  }
    40  
    41  // RecordingRules is a collection that gathers rules coming from config and coming from the bucket storage.
    42  // Rules coming from config work as overrides of store rules, and in case of repeated ID, config rules prevail.
    43  type RecordingRules struct {
    44  	bucket objstore.Bucket
    45  	logger log.Logger
    46  
    47  	rw     sync.RWMutex
    48  	stores map[store.Key]*bucketStore
    49  
    50  	overrides *validation.Overrides
    51  }
    52  
    53  // GetRecordingRule will return a rule of the given ID or not found.
    54  // Rules defined by config are returned over rules in the store.
    55  func (r *RecordingRules) GetRecordingRule(ctx context.Context, req *connect.Request[settingsv1.GetRecordingRuleRequest]) (*connect.Response[settingsv1.GetRecordingRuleResponse], error) {
    56  	err := validateGet(req.Msg)
    57  	if err != nil {
    58  		return nil, connect.NewError(connect.CodeInvalidArgument, err)
    59  	}
    60  
    61  	tenantID, err := r.tenantOrError(ctx)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	// look for provisioned rules
    67  	rulesFromConfig := r.recordingRulesFromOverrides(tenantID)
    68  	for _, r := range rulesFromConfig {
    69  		if r.Id == req.Msg.Id {
    70  			return connect.NewResponse(&settingsv1.GetRecordingRuleResponse{Rule: r}), nil
    71  		}
    72  	}
    73  
    74  	s := r.storeForTenant(tenantID)
    75  	rule, err := s.Get(ctx, req.Msg.Id)
    76  	if err != nil {
    77  		return nil, connect.NewError(connect.CodeInternal, err)
    78  	}
    79  	if rule == nil {
    80  		return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no rule with id='%s' found", req.Msg.Id))
    81  	}
    82  
    83  	res := &settingsv1.GetRecordingRuleResponse{
    84  		Rule: convertRuleToAPI(rule),
    85  	}
    86  	return connect.NewResponse(res), nil
    87  }
    88  
    89  // ListRecordingRules will return all the rules defined by config and in the store. Rules in the store with the same ID
    90  // as a rule in config will be filtered out.
    91  func (r *RecordingRules) ListRecordingRules(ctx context.Context, req *connect.Request[settingsv1.ListRecordingRulesRequest]) (*connect.Response[settingsv1.ListRecordingRulesResponse], error) {
    92  	tenantId, err := r.tenantOrError(ctx)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	rulesFromOverrides := r.recordingRulesFromOverrides(tenantId)
    98  	ruleIds := make(map[string]struct{}, len(rulesFromOverrides))
    99  	res := &settingsv1.ListRecordingRulesResponse{
   100  		Rules: make([]*settingsv1.RecordingRule, 0),
   101  	}
   102  	for _, r := range rulesFromOverrides {
   103  		ruleIds[r.Id] = struct{}{}
   104  		res.Rules = append(res.Rules, r)
   105  	}
   106  
   107  	s := r.storeForTenant(tenantId)
   108  	rulesFromStore, err := s.List(ctx)
   109  	if err != nil {
   110  		return nil, connect.NewError(connect.CodeInternal, err)
   111  	}
   112  	for _, rule := range rulesFromStore.Rules {
   113  		if _, overridden := ruleIds[rule.Id]; overridden {
   114  			continue
   115  		}
   116  		res.Rules = append(res.Rules, convertRuleToAPI(rule))
   117  	}
   118  
   119  	return connect.NewResponse(res), nil
   120  }
   121  
   122  // UpsertRecordingRule upserts a rule in the storage.
   123  // Operational purposes: you can upsert store rules (no matter if they exist in config)
   124  func (r *RecordingRules) UpsertRecordingRule(ctx context.Context, req *connect.Request[settingsv1.UpsertRecordingRuleRequest]) (*connect.Response[settingsv1.UpsertRecordingRuleResponse], error) {
   125  	err := validateUpsert(req.Msg)
   126  	if err != nil {
   127  		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid request: %v", err))
   128  	}
   129  
   130  	s, err := r.storeFromContext(ctx)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	newRule := &settingsv1.RecordingRuleStore{
   136  		Id:               req.Msg.Id,
   137  		MetricName:       req.Msg.MetricName,
   138  		Matchers:         req.Msg.Matchers,
   139  		GroupBy:          req.Msg.GroupBy,
   140  		ExternalLabels:   req.Msg.ExternalLabels,
   141  		Generation:       req.Msg.Generation,
   142  		StacktraceFilter: req.Msg.StacktraceFilter,
   143  	}
   144  	newRule, err = s.Upsert(ctx, newRule)
   145  	if err != nil {
   146  		var cErr *store.ErrConflictGeneration
   147  		if errors.As(err, &cErr) {
   148  			return nil, connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("conflicting update, please try again"))
   149  		}
   150  		return nil, connect.NewError(connect.CodeInternal, err)
   151  	}
   152  
   153  	res := &settingsv1.UpsertRecordingRuleResponse{
   154  		Rule: convertRuleToAPI(newRule),
   155  	}
   156  	return connect.NewResponse(res), nil
   157  }
   158  
   159  // DeleteRecordingRule deletes a store rule
   160  // Operational purposes: you can delete store rules (no matter if they exist in config)
   161  func (r *RecordingRules) DeleteRecordingRule(ctx context.Context, req *connect.Request[settingsv1.DeleteRecordingRuleRequest]) (*connect.Response[settingsv1.DeleteRecordingRuleResponse], error) {
   162  	err := validateDelete(req.Msg)
   163  	if err != nil {
   164  		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid request: %v", err))
   165  	}
   166  
   167  	s, err := r.storeFromContext(ctx)
   168  	if err != nil {
   169  		return nil, connect.NewError(connect.CodeInternal, err)
   170  	}
   171  
   172  	err = s.Delete(ctx, req.Msg.Id)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	res := &settingsv1.DeleteRecordingRuleResponse{}
   178  	return connect.NewResponse(res), nil
   179  }
   180  
   181  func (r *RecordingRules) storeFromContext(ctx context.Context) (*bucketStore, error) {
   182  	tenantID, err := r.tenantOrError(ctx)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	return r.storeForTenant(tenantID), nil
   187  }
   188  
   189  func (r *RecordingRules) storeForTenant(tenantID string) *bucketStore {
   190  	key := store.Key{TenantID: tenantID}
   191  
   192  	r.rw.RLock()
   193  	tenantStore, ok := r.stores[key]
   194  	r.rw.RUnlock()
   195  	if ok {
   196  		return tenantStore
   197  	}
   198  
   199  	r.rw.Lock()
   200  	defer r.rw.Unlock()
   201  
   202  	tenantStore, ok = r.stores[key]
   203  	if ok {
   204  		return tenantStore
   205  	}
   206  
   207  	tenantStore = newBucketStore(r.logger, r.bucket, key)
   208  	r.stores[key] = tenantStore
   209  	return tenantStore
   210  }
   211  
   212  func (r *RecordingRules) tenantOrError(ctx context.Context) (string, error) {
   213  	tenantID, err := tenant.TenantID(ctx)
   214  	if err != nil {
   215  		level.Error(r.logger).Log("error getting tenant ID", "err", err)
   216  		return "", connect.NewError(connect.CodeInternal, err)
   217  	}
   218  	return tenantID, nil
   219  }
   220  
   221  func (r *RecordingRules) recordingRulesFromOverrides(tenantID string) []*settingsv1.RecordingRule {
   222  	rules := r.overrides.RecordingRules(tenantID)
   223  	for i := range rules {
   224  		rules[i].Provisioned = true
   225  		if rules[i].Id == "" {
   226  			// for consistency, rules will be filled with an ID
   227  			rules[i].Id = idForRule(rules[i])
   228  		}
   229  	}
   230  	return rules
   231  }
   232  
   233  func validateGet(req *settingsv1.GetRecordingRuleRequest) error {
   234  	// Format fields.
   235  	req.Id = strings.TrimSpace(req.Id)
   236  
   237  	// Validate fields.
   238  	var errs []error
   239  
   240  	if req.Id == "" {
   241  		errs = append(errs, fmt.Errorf("id is required"))
   242  	}
   243  
   244  	return errors.Join(errs...)
   245  }
   246  
   247  var (
   248  	upsertIdRE = regexp.MustCompile(`^[a-zA-Z]+$`)
   249  )
   250  
   251  func validateUpsert(req *settingsv1.UpsertRecordingRuleRequest) error {
   252  	// Validate fields.
   253  	var errs []error
   254  
   255  	// Format fields.
   256  	if req.Id == "" {
   257  		req.Id = generateID(idLength)
   258  		req.Generation = 1
   259  	}
   260  	req.MetricName = strings.TrimSpace(req.MetricName)
   261  
   262  	if !upsertIdRE.MatchString(req.Id) {
   263  		errs = append(errs, fmt.Errorf("id %q must match %s", req.Id, upsertIdRE.String()))
   264  	}
   265  
   266  	if req.MetricName == "" {
   267  		errs = append(errs, fmt.Errorf("metric_name is required"))
   268  	} else if err := model.ValidateMetricName(req.MetricName); err != nil {
   269  		errs = append(errs, fmt.Errorf("metric_name %q is invalid: %v", req.MetricName, err))
   270  	}
   271  
   272  	for _, m := range req.Matchers {
   273  		_, err := parser.ParseMetricSelector(m)
   274  		if err != nil {
   275  			errs = append(errs, fmt.Errorf("matcher %q is invalid: %v", m, err))
   276  		}
   277  	}
   278  
   279  	for _, l := range req.GroupBy {
   280  		name := prom.LabelName(l)
   281  		if !prom.LegacyValidation.IsValidLabelName(string(name)) {
   282  			errs = append(errs, fmt.Errorf("group_by label %q must match %s", l, prom.LabelNameRE.String()))
   283  		}
   284  	}
   285  
   286  	for _, l := range req.ExternalLabels {
   287  		name := prom.LabelName(l.Name)
   288  		if !prom.LegacyValidation.IsValidLabelName(string(name)) {
   289  			errs = append(errs, fmt.Errorf("external_labels name %q must match %s", name, prom.LabelNameRE.String()))
   290  		}
   291  
   292  		value := prom.LabelValue(l.Value)
   293  		if !value.IsValid() {
   294  			errs = append(errs, fmt.Errorf("external_labels value %q must be a valid utf-8 string", l.Value))
   295  		}
   296  	}
   297  
   298  	if req.Generation < 0 {
   299  		errs = append(errs, fmt.Errorf("generation must be positive"))
   300  	}
   301  
   302  	return errors.Join(errs...)
   303  }
   304  
   305  func validateDelete(req *settingsv1.DeleteRecordingRuleRequest) error {
   306  	// Format fields.
   307  	req.Id = strings.TrimSpace(req.Id)
   308  
   309  	// Validate fields.
   310  	var errs []error
   311  
   312  	if req.Id == "" {
   313  		errs = append(errs, fmt.Errorf("id is required"))
   314  	}
   315  
   316  	return errors.Join(errs...)
   317  }
   318  
   319  func convertRuleToAPI(rule *settingsv1.RecordingRuleStore) *settingsv1.RecordingRule {
   320  	apiRule := &settingsv1.RecordingRule{
   321  		Id:               rule.Id,
   322  		MetricName:       rule.MetricName,
   323  		ProfileType:      "unknown",
   324  		Matchers:         rule.Matchers,
   325  		GroupBy:          rule.GroupBy,
   326  		ExternalLabels:   rule.ExternalLabels,
   327  		Generation:       rule.Generation,
   328  		StacktraceFilter: rule.StacktraceFilter,
   329  	}
   330  
   331  	// Try find the profile type from the matchers.
   332  Loop:
   333  	for _, m := range rule.Matchers {
   334  		s, err := parser.ParseMetricSelector(m)
   335  		if err != nil {
   336  			// Since this value is loaded from the tenant settings database and
   337  			// we validate selectors before saving, we should theoretically
   338  			// always have valid selectors. If there's an error parsing a
   339  			// selector, we'll just skip it.
   340  			continue
   341  		}
   342  
   343  		for _, label := range s {
   344  			if label.Name != model.LabelNameProfileType {
   345  				continue
   346  			}
   347  
   348  			if label.Type != labels.MatchEqual {
   349  				continue
   350  			}
   351  
   352  			apiRule.ProfileType = label.Value
   353  			break Loop
   354  		}
   355  	}
   356  
   357  	return apiRule
   358  }
   359  
   360  const (
   361  	alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   362  	idLength = 10
   363  )
   364  
   365  func generateID(length int) string {
   366  
   367  	if length < 1 {
   368  		return ""
   369  	}
   370  
   371  	b := make([]byte, length)
   372  	for i := range b {
   373  		b[i] = alphabet[rand.Intn(len(alphabet))]
   374  	}
   375  	return string(b)
   376  }
   377  
   378  func idForRule(rule *settingsv1.RecordingRule) string {
   379  	var b strings.Builder
   380  	b.WriteString(rule.MetricName)
   381  	b.WriteString(rule.ProfileType)
   382  	sort.Strings(rule.Matchers)
   383  	for _, m := range rule.Matchers {
   384  		b.WriteString(m)
   385  	}
   386  	sort.Strings(rule.GroupBy)
   387  	for _, g := range rule.GroupBy {
   388  		b.WriteString(g)
   389  	}
   390  	for _, l := range rule.ExternalLabels {
   391  		b.WriteString(l.Name)
   392  		b.WriteString(l.Value)
   393  	}
   394  	if rule.StacktraceFilter != nil && rule.StacktraceFilter.FunctionName != nil {
   395  		b.WriteString(rule.StacktraceFilter.FunctionName.FunctionName)
   396  	}
   397  	sum := sha256.Sum256([]byte(b.String()))
   398  	id := make([]byte, idLength)
   399  	for i := 0; i < idLength; i++ {
   400  		id[i] = alphabet[sum[i]%byte(len(alphabet))]
   401  	}
   402  	return string(id)
   403  }