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 }