github.com/grafana/pyroscope@v1.18.0/pkg/settings/recording/recording_test.go (about) 1 package recording 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "testing" 8 "time" 9 10 "connectrpc.com/connect" 11 "github.com/go-kit/log" 12 "github.com/grafana/dskit/user" 13 "github.com/stretchr/testify/require" 14 "golang.org/x/exp/rand" 15 16 settingsv1 "github.com/grafana/pyroscope/api/gen/proto/go/settings/v1" 17 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 18 "github.com/grafana/pyroscope/pkg/objstore/providers/filesystem" 19 "github.com/grafana/pyroscope/pkg/validation" 20 ) 21 22 func Test_validateGet(t *testing.T) { 23 tests := []struct { 24 Name string 25 Req *settingsv1.GetRecordingRuleRequest 26 WantErr string 27 }{ 28 { 29 Name: "valid", 30 Req: &settingsv1.GetRecordingRuleRequest{ 31 Id: "random", 32 }, 33 WantErr: "", 34 }, 35 { 36 Name: "valid_with_formatted_fields", 37 Req: &settingsv1.GetRecordingRuleRequest{ 38 Id: " random ", 39 }, 40 WantErr: "", 41 }, 42 { 43 Name: "empty_id", 44 Req: &settingsv1.GetRecordingRuleRequest{ 45 Id: "", 46 }, 47 WantErr: "id is required", 48 }, 49 } 50 51 for _, tt := range tests { 52 t.Run(tt.Name, func(t *testing.T) { 53 err := validateGet(tt.Req) 54 if tt.WantErr != "" { 55 require.Error(t, err) 56 require.EqualError(t, err, tt.WantErr) 57 } else { 58 require.NoError(t, err) 59 } 60 }) 61 } 62 } 63 64 func Test_validateUpsert(t *testing.T) { 65 tests := []struct { 66 Name string 67 Req *settingsv1.UpsertRecordingRuleRequest 68 WantErr string 69 }{ 70 { 71 Name: "valid", 72 Req: &settingsv1.UpsertRecordingRuleRequest{ 73 Id: "abcdef", 74 MetricName: "profiles_recorded_my_metric", 75 Matchers: []string{ 76 `{ label_a = "A" }`, 77 `{ label_b =~ "B" }`, 78 }, 79 GroupBy: []string{ 80 "label_c", 81 }, 82 ExternalLabels: []*typesv1.LabelPair{ 83 {Name: "label_a", Value: "A"}, 84 {Name: "label_b", Value: "B"}, 85 }, 86 Generation: 1, 87 }, 88 WantErr: "", 89 }, 90 { 91 Name: "minimal_valid", 92 Req: &settingsv1.UpsertRecordingRuleRequest{ 93 Id: "", 94 MetricName: "profiles_recorded_my_metric", 95 Matchers: []string{}, 96 GroupBy: []string{}, 97 ExternalLabels: []*typesv1.LabelPair{}, 98 }, 99 WantErr: "", 100 }, 101 { 102 Name: "valid_with_formatted_fields", 103 Req: &settingsv1.UpsertRecordingRuleRequest{ 104 Id: "abcdef", 105 MetricName: " profiles_recorded_my_metric ", 106 Matchers: []string{}, 107 GroupBy: []string{}, 108 ExternalLabels: []*typesv1.LabelPair{}, 109 }, 110 WantErr: "", 111 }, 112 { 113 Name: "empty_id", 114 Req: &settingsv1.UpsertRecordingRuleRequest{ 115 Id: "", 116 MetricName: "profiles_recorded_my_metric", 117 Matchers: []string{}, 118 GroupBy: []string{}, 119 ExternalLabels: []*typesv1.LabelPair{}, 120 }, 121 WantErr: "", 122 }, 123 { 124 Name: "whitespace_only_id", 125 Req: &settingsv1.UpsertRecordingRuleRequest{ 126 Id: " ", 127 MetricName: "profiles_recorded_my_metric", 128 Matchers: []string{}, 129 GroupBy: []string{}, 130 ExternalLabels: []*typesv1.LabelPair{}, 131 }, 132 WantErr: `id " " must match ^[a-zA-Z]+$`, 133 }, 134 { 135 Name: "invalid_id", 136 Req: &settingsv1.UpsertRecordingRuleRequest{ 137 Id: "?", 138 MetricName: "profiles_recorded_my_metric", 139 Matchers: []string{}, 140 GroupBy: []string{}, 141 ExternalLabels: []*typesv1.LabelPair{}, 142 }, 143 WantErr: `id "?" must match ^[a-zA-Z]+$`, 144 }, 145 { 146 Name: "empty_metric_name", 147 Req: &settingsv1.UpsertRecordingRuleRequest{ 148 MetricName: "", 149 Matchers: []string{}, 150 GroupBy: []string{}, 151 ExternalLabels: []*typesv1.LabelPair{}, 152 }, 153 WantErr: "metric_name is required", 154 }, 155 { 156 Name: "invalid_metric_name", 157 Req: &settingsv1.UpsertRecordingRuleRequest{ 158 MetricName: string([]byte{0xC0, 0xAF}), // invalid utf-8 159 Matchers: []string{}, 160 GroupBy: []string{}, 161 ExternalLabels: []*typesv1.LabelPair{}, 162 }, 163 WantErr: "metric_name \"\\xc0\\xaf\" is invalid: invalid metric name: \xc0\xaf", 164 }, 165 { 166 Name: "invalid_matchers", 167 Req: &settingsv1.UpsertRecordingRuleRequest{ 168 MetricName: "profiles_recorded_my_metric", 169 Matchers: []string{ 170 "", 171 }, 172 GroupBy: []string{}, 173 ExternalLabels: []*typesv1.LabelPair{}, 174 }, 175 WantErr: `matcher "" is invalid: unknown position: parse error: unexpected end of input`, 176 }, 177 { 178 Name: "invalid_group_by_empty", 179 Req: &settingsv1.UpsertRecordingRuleRequest{ 180 MetricName: "profiles_recorded_my_metric", 181 Matchers: []string{}, 182 GroupBy: []string{ 183 "", 184 }, 185 ExternalLabels: []*typesv1.LabelPair{}, 186 }, 187 WantErr: `group_by label "" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 188 }, 189 { 190 Name: "invalid_group_by_with_dot", 191 Req: &settingsv1.UpsertRecordingRuleRequest{ 192 MetricName: "profiles_recorded_my_metric", 193 Matchers: []string{}, 194 GroupBy: []string{ 195 "service.name", 196 }, 197 ExternalLabels: []*typesv1.LabelPair{}, 198 }, 199 WantErr: `group_by label "service.name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 200 }, 201 { 202 Name: "invalid_group_by_with_utf8", 203 Req: &settingsv1.UpsertRecordingRuleRequest{ 204 MetricName: "profiles_recorded_my_metric", 205 Matchers: []string{}, 206 GroupBy: []string{ 207 "世界", 208 }, 209 ExternalLabels: []*typesv1.LabelPair{}, 210 }, 211 WantErr: `group_by label "世界" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 212 }, 213 { 214 Name: "invalid_group_by_starts_with_number", 215 Req: &settingsv1.UpsertRecordingRuleRequest{ 216 MetricName: "profiles_recorded_my_metric", 217 Matchers: []string{}, 218 GroupBy: []string{ 219 "123invalid", 220 }, 221 ExternalLabels: []*typesv1.LabelPair{}, 222 }, 223 WantErr: `group_by label "123invalid" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 224 }, 225 { 226 Name: "invalid_external_label_utf8", 227 Req: &settingsv1.UpsertRecordingRuleRequest{ 228 MetricName: "profiles_recorded_my_metric", 229 Matchers: []string{}, 230 GroupBy: []string{}, 231 ExternalLabels: []*typesv1.LabelPair{ 232 { 233 Name: string([]byte{0xC0, 0xAF}), // invalid utf-8 234 Value: string([]byte{0xC0, 0xAF}), // invalid utf-8 235 }, 236 }, 237 }, 238 WantErr: `external_labels name "\xc0\xaf" must match ^[a-zA-Z_][a-zA-Z0-9_]*$ 239 external_labels value "\xc0\xaf" must be a valid utf-8 string`, 240 }, 241 { 242 Name: "invalid_external_label_with_dot", 243 Req: &settingsv1.UpsertRecordingRuleRequest{ 244 MetricName: "profiles_recorded_my_metric", 245 Matchers: []string{}, 246 GroupBy: []string{}, 247 ExternalLabels: []*typesv1.LabelPair{ 248 {Name: "service.name", Value: "foo"}, 249 }, 250 }, 251 WantErr: `external_labels name "service.name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 252 }, 253 { 254 Name: "invalid_external_label_with_utf8_name", 255 Req: &settingsv1.UpsertRecordingRuleRequest{ 256 MetricName: "profiles_recorded_my_metric", 257 Matchers: []string{}, 258 GroupBy: []string{}, 259 ExternalLabels: []*typesv1.LabelPair{ 260 {Name: "世界", Value: "value"}, 261 }, 262 }, 263 WantErr: `external_labels name "世界" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 264 }, 265 { 266 Name: "invalid_external_label_starts_with_number", 267 Req: &settingsv1.UpsertRecordingRuleRequest{ 268 MetricName: "profiles_recorded_my_metric", 269 Matchers: []string{}, 270 GroupBy: []string{}, 271 ExternalLabels: []*typesv1.LabelPair{ 272 {Name: "123invalid", Value: "value"}, 273 }, 274 }, 275 WantErr: `external_labels name "123invalid" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 276 }, 277 { 278 Name: "invalid_generation", 279 Req: &settingsv1.UpsertRecordingRuleRequest{ 280 Id: "abcdef", 281 MetricName: "profiles_recorded_my_metric", 282 Matchers: []string{}, 283 GroupBy: []string{}, 284 ExternalLabels: []*typesv1.LabelPair{}, 285 Generation: -1, 286 }, 287 WantErr: "generation must be positive", 288 }, 289 { 290 Name: "multiple_errors", 291 Req: &settingsv1.UpsertRecordingRuleRequest{ 292 MetricName: "", 293 Matchers: []string{ 294 "", 295 }, 296 GroupBy: []string{}, 297 ExternalLabels: []*typesv1.LabelPair{}, 298 }, 299 WantErr: "metric_name is required\nmatcher \"\" is invalid: unknown position: parse error: unexpected end of input", 300 }, 301 } 302 303 for _, tt := range tests { 304 t.Run(tt.Name, func(t *testing.T) { 305 err := validateUpsert(tt.Req) 306 if tt.WantErr != "" { 307 require.Error(t, err) 308 require.EqualError(t, err, tt.WantErr) 309 } else { 310 require.NoError(t, err) 311 } 312 }) 313 } 314 } 315 316 func Test_validateDelete(t *testing.T) { 317 tests := []struct { 318 Name string 319 Req *settingsv1.DeleteRecordingRuleRequest 320 WantErr string 321 }{ 322 { 323 Name: "valid", 324 Req: &settingsv1.DeleteRecordingRuleRequest{ 325 Id: "random", 326 }, 327 WantErr: "", 328 }, 329 { 330 Name: "valid_with_formatted_fields", 331 Req: &settingsv1.DeleteRecordingRuleRequest{ 332 Id: " random ", 333 }, 334 WantErr: "", 335 }, 336 { 337 Name: "empty_id", 338 Req: &settingsv1.DeleteRecordingRuleRequest{ 339 Id: "", 340 }, 341 WantErr: "id is required", 342 }, 343 } 344 345 for _, tt := range tests { 346 t.Run(tt.Name, func(t *testing.T) { 347 err := validateDelete(tt.Req) 348 if tt.WantErr != "" { 349 require.Error(t, err) 350 require.EqualError(t, err, tt.WantErr) 351 } else { 352 require.NoError(t, err) 353 } 354 }) 355 } 356 } 357 358 func Test_idForRule(t *testing.T) { 359 tests := []struct { 360 name string 361 rule *settingsv1.RecordingRule 362 expectedId string 363 }{ 364 { 365 name: "some-rule", 366 expectedId: "veouCnOZTo", 367 rule: &settingsv1.RecordingRule{ 368 MetricName: "metric1", 369 ProfileType: "cpu", 370 Matchers: []string{ 371 `{ label_a = "A" }`, 372 `{ label_b =~ "B" }`, 373 }, 374 GroupBy: []string{"label_c", "label_d"}, 375 ExternalLabels: []*typesv1.LabelPair{ 376 {Name: "label_e", Value: "E"}, 377 {Name: "label_f", Value: "F"}, 378 }, 379 StacktraceFilter: &settingsv1.StacktraceFilter{ 380 FunctionName: &settingsv1.StacktraceFilterFunctionName{ 381 FunctionName: "function_name", 382 }, 383 }, 384 }, 385 }, 386 { 387 name: "some-other-rule", 388 expectedId: "XMMpSpeTom", 389 rule: &settingsv1.RecordingRule{ 390 MetricName: "metric1", 391 ProfileType: "cpu", 392 Matchers: []string{ 393 `{ label_a = "A" }`, 394 `{ label_b =~ "B" }`, 395 }, 396 GroupBy: []string{"label_c", "label_d"}, 397 ExternalLabels: []*typesv1.LabelPair{ 398 {Name: "label_e", Value: "E"}, 399 {Name: "label_f", Value: "F"}, 400 }, 401 StacktraceFilter: &settingsv1.StacktraceFilter{ 402 FunctionName: &settingsv1.StacktraceFilterFunctionName{ 403 FunctionName: "another_function_name", 404 }, 405 }, 406 }, 407 }, 408 } 409 for _, tt := range tests { 410 t.Run(tt.name, func(t *testing.T) { 411 result := idForRule(tt.rule) 412 require.Equal(t, tt.expectedId, result) 413 }) 414 } 415 } 416 417 type testRecordingRules struct { 418 *RecordingRules 419 bucketPath string 420 } 421 422 func newTestRecordingRules(t *testing.T, overrides *validation.Overrides) *testRecordingRules { 423 logger := log.NewNopLogger() 424 if testing.Verbose() { 425 logger = log.NewLogfmtLogger(os.Stderr) 426 } 427 bucketPath := t.TempDir() 428 bucket, err := filesystem.NewBucket(bucketPath) 429 require.NoError(t, err) 430 431 return &testRecordingRules{ 432 RecordingRules: New(bucket, logger, overrides), 433 bucketPath: bucketPath, 434 } 435 } 436 437 func TestRecordingRules_Get(t *testing.T) { 438 testUser := "user1" 439 storeRule1 := RandomRule() 440 storeRule2 := RandomRule() 441 configRule1 := RandomRule() 442 configRule1.Id = storeRule2.Id // configRule1 overrides storeRule2 443 configRule2 := RandomRule() 444 configRule2.Id = "" // this rule doesn't override any rule 445 446 r := newTestRecordingRules(t, validation.MockOverrides(func(defaults *validation.Limits, tenantLimits map[string]*validation.Limits) { 447 user1 := validation.MockDefaultLimits() 448 user1.RecordingRules = validation.RecordingRules{ 449 configRule1, 450 configRule2, 451 } 452 tenantLimits[testUser] = user1 453 })) 454 455 ctx := user.InjectOrgID(context.Background(), testUser) 456 457 t.Run("Get not found", func(t *testing.T) { 458 _, err := r.GetRecordingRule(ctx, connect.NewRequest(&settingsv1.GetRecordingRuleRequest{Id: storeRule1.Id})) 459 require.EqualError(t, err, fmt.Sprintf("not_found: no rule with id='%s' found", storeRule1.Id)) 460 }) 461 462 t.Run("List rules empty for other user", func(t *testing.T) { 463 ctx2 := user.InjectOrgID(context.Background(), "user2") 464 resp, err := r.ListRecordingRules(ctx2, connect.NewRequest(&settingsv1.ListRecordingRulesRequest{})) 465 require.NoError(t, err) 466 require.Empty(t, resp.Msg.Rules) 467 }) 468 469 t.Run("Get config rule from autogenerated ID", func(t *testing.T) { 470 idForConfigRule2 := idForRule(configRule2) 471 resp, err := r.GetRecordingRule(ctx, connect.NewRequest(&settingsv1.GetRecordingRuleRequest{Id: idForConfigRule2})) 472 require.NoError(t, err) 473 require.Equal(t, configRule2, resp.Msg.Rule) 474 }) 475 476 t.Run("Insert store rules", func(t *testing.T) { 477 rule, err := r.UpsertRecordingRule(ctx, connect.NewRequest(&settingsv1.UpsertRecordingRuleRequest{ 478 Id: storeRule1.Id, 479 MetricName: storeRule1.MetricName, 480 Matchers: storeRule1.Matchers, 481 GroupBy: storeRule1.GroupBy, 482 Generation: storeRule1.Generation, 483 ExternalLabels: storeRule1.ExternalLabels, 484 StacktraceFilter: storeRule1.StacktraceFilter, 485 })) 486 require.NoError(t, err) 487 require.Equal(t, storeRule1, rule.Msg.Rule) 488 rule, err = r.UpsertRecordingRule(ctx, connect.NewRequest(&settingsv1.UpsertRecordingRuleRequest{ 489 Id: storeRule2.Id, 490 MetricName: storeRule2.MetricName, 491 Matchers: storeRule2.Matchers, 492 GroupBy: storeRule2.GroupBy, 493 Generation: storeRule2.Generation, 494 ExternalLabels: storeRule2.ExternalLabels, 495 StacktraceFilter: storeRule2.StacktraceFilter, 496 })) 497 require.NoError(t, err) 498 require.Equal(t, storeRule2, rule.Msg.Rule) 499 }) 500 501 t.Run("Get overridden rule from config", func(t *testing.T) { 502 resp, err := r.GetRecordingRule(ctx, connect.NewRequest(&settingsv1.GetRecordingRuleRequest{Id: storeRule2.Id})) 503 require.NoError(t, err) 504 require.NotEqual(t, storeRule2, configRule1) 505 require.Equal(t, configRule1, resp.Msg.Rule) 506 }) 507 508 t.Run("List rules with overrides", func(t *testing.T) { 509 resp, err := r.ListRecordingRules(ctx, connect.NewRequest(&settingsv1.ListRecordingRulesRequest{})) 510 require.NoError(t, err) 511 require.EqualValues(t, resp.Msg.Rules, []*settingsv1.RecordingRule{ 512 configRule1, 513 configRule2, 514 storeRule1, 515 // No storeRule2 as it's overridden by configRule1 516 }) 517 }) 518 519 t.Run("Upsert overridden rule just changes the original", func(t *testing.T) { 520 upsertedRule, err := r.UpsertRecordingRule(ctx, connect.NewRequest(&settingsv1.UpsertRecordingRuleRequest{ 521 Id: storeRule2.Id, 522 MetricName: storeRule2.MetricName, 523 Matchers: storeRule2.Matchers, 524 GroupBy: storeRule2.GroupBy, 525 Generation: storeRule2.Generation, 526 ExternalLabels: storeRule2.ExternalLabels, 527 StacktraceFilter: storeRule2.StacktraceFilter, 528 })) 529 storeRule2.Generation++ 530 require.NoError(t, err) 531 require.Equal(t, storeRule2, upsertedRule.Msg.Rule) 532 533 rule, err := r.GetRecordingRule(ctx, connect.NewRequest(&settingsv1.GetRecordingRuleRequest{Id: storeRule2.Id})) 534 require.NoError(t, err) 535 require.Equal(t, configRule1, rule.Msg.Rule) 536 }) 537 538 t.Run("Delete store rules", func(t *testing.T) { 539 _, err := r.DeleteRecordingRule(ctx, connect.NewRequest(&settingsv1.DeleteRecordingRuleRequest{Id: storeRule1.Id})) 540 require.NoError(t, err) 541 _, err = r.DeleteRecordingRule(ctx, connect.NewRequest(&settingsv1.DeleteRecordingRuleRequest{Id: storeRule2.Id})) 542 require.NoError(t, err) 543 resp, err := r.ListRecordingRules(ctx, connect.NewRequest(&settingsv1.ListRecordingRulesRequest{})) 544 require.NoError(t, err) 545 require.EqualValues(t, resp.Msg.Rules, []*settingsv1.RecordingRule{ 546 configRule1, 547 configRule2, 548 }) 549 }) 550 551 t.Run("Can't delete config rules", func(t *testing.T) { 552 _, err := r.DeleteRecordingRule(ctx, connect.NewRequest(&settingsv1.DeleteRecordingRuleRequest{Id: configRule1.Id})) 553 554 require.EqualError(t, err, fmt.Sprintf("not_found: no rule with ID='%s' found", configRule1.Id)) 555 }) 556 557 } 558 559 func init() { 560 rand.Seed(uint64(time.Now().UnixNano())) 561 } 562 563 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 564 565 func RandomString(n int) string { 566 b := make([]byte, n) 567 for i := range b { 568 b[i] = letters[rand.Intn(len(letters))] 569 } 570 return string(b) 571 } 572 573 func RandomRule() *settingsv1.RecordingRule { 574 profileType := RandomString(5) 575 matchers := make([]string, rand.Intn(3)) 576 for i := range matchers { 577 matchers[i] = fmt.Sprintf(`{ %s = "%s" }`, RandomString(5), RandomString(5)) 578 } 579 matchers = append(matchers, fmt.Sprintf(`{ __profile_type__ = "%s" }`, profileType)) 580 groupBy := make([]string, rand.Intn(2)+1) 581 for i := range groupBy { 582 groupBy[i] = RandomString(5) 583 } 584 externalLabels := make([]*typesv1.LabelPair, rand.Intn(2)+1) 585 for i := range externalLabels { 586 externalLabels[i] = &typesv1.LabelPair{ 587 Name: RandomString(5), 588 Value: RandomString(5), 589 } 590 } 591 var functionFilter *settingsv1.StacktraceFilter 592 if rand.Intn(2) == 1 { 593 functionFilter = &settingsv1.StacktraceFilter{ 594 FunctionName: &settingsv1.StacktraceFilterFunctionName{ 595 FunctionName: RandomString(5), 596 }, 597 } 598 } 599 return &settingsv1.RecordingRule{ 600 Id: RandomString(10), 601 MetricName: "profiles_recorded_" + RandomString(5), 602 ProfileType: profileType, 603 Matchers: matchers, 604 GroupBy: groupBy, 605 Generation: 1, 606 ExternalLabels: externalLabels, 607 StacktraceFilter: functionFilter, 608 } 609 }