github.com/grafana/pyroscope@v1.18.0/pkg/validation/usage_groups_test.go (about) 1 package validation 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "slices" 7 "testing" 8 9 "github.com/prometheus/client_golang/prometheus/testutil" 10 "github.com/prometheus/prometheus/model/labels" 11 "github.com/prometheus/prometheus/promql/parser" 12 "github.com/stretchr/testify/require" 13 "gopkg.in/yaml.v3" 14 15 phlaremodel "github.com/grafana/pyroscope/pkg/model" 16 "github.com/grafana/pyroscope/pkg/util" 17 ) 18 19 func TestUsageGroupConfig_GetUsageGroups(t *testing.T) { 20 tests := []struct { 21 Name string 22 TenantID string 23 Config map[string]string 24 Labels phlaremodel.Labels 25 WantedNames []string 26 }{ 27 { 28 Name: "single_usage_group_match", 29 TenantID: "tenant1", 30 Config: map[string]string{ 31 "app/foo": `{service_name="foo"}`, 32 }, 33 Labels: phlaremodel.Labels{ 34 {Name: "service_name", Value: "foo"}, 35 }, 36 WantedNames: []string{"app/foo"}, 37 }, 38 { 39 Name: "multiple_usage_group_matches", 40 TenantID: "tenant1", 41 Config: map[string]string{ 42 "app/foo": `{service_name="foo"}`, 43 "app/foo2": `{service_name="foo", namespace=~"bar.*"}`, 44 }, 45 Labels: phlaremodel.Labels{ 46 {Name: "service_name", Value: "foo"}, 47 {Name: "namespace", Value: "barbaz"}, 48 }, 49 WantedNames: []string{ 50 "app/foo", 51 "app/foo2", 52 }, 53 }, 54 { 55 Name: "no_usage_group_matches", 56 TenantID: "tenant1", 57 Config: map[string]string{ 58 "app/foo": `{service_name="notfound"}`, 59 }, 60 Labels: phlaremodel.Labels{ 61 {Name: "service_name", Value: "foo"}, 62 }, 63 WantedNames: []string{}, 64 }, 65 { 66 Name: "wildcard_matcher", 67 TenantID: "tenant1", 68 Config: map[string]string{ 69 "app/foo": `{}`, 70 }, 71 Labels: phlaremodel.Labels{ 72 {Name: "service_name", Value: "foo"}, 73 }, 74 WantedNames: []string{"app/foo"}, 75 }, 76 { 77 Name: "no_labels", 78 TenantID: "tenant1", 79 Config: map[string]string{ 80 "app/foo": `{service_name="foo"}`, 81 }, 82 Labels: phlaremodel.Labels{}, 83 WantedNames: []string{}, 84 }, 85 { 86 Name: "disjoint_labels_do_not_match", 87 TenantID: "tenant1", 88 Config: map[string]string{ 89 "app/foo": `{namespace="foo", container="bar"}`, 90 }, 91 Labels: phlaremodel.Labels{ 92 {Name: "service_name", Value: "foo"}, 93 }, 94 WantedNames: []string{}, 95 }, 96 { 97 Name: "dynamic_usage_group_names", 98 TenantID: "tenant1", 99 Config: map[string]string{ 100 "app/${labels.service_name}": `{service_name=~"(.*)"}`, 101 }, 102 Labels: phlaremodel.Labels{ 103 {Name: "service_name", Value: "foo"}, 104 }, 105 WantedNames: []string{ 106 "app/foo", 107 }, 108 }, 109 { 110 Name: "dynamic_usage_group_names_missing_label", 111 TenantID: "tenant1", 112 Config: map[string]string{ 113 "app/${labels.service_name}/${labels.env}": `{service_name=~"(.*)"}`, 114 }, 115 Labels: phlaremodel.Labels{ 116 {Name: "service_name", Value: "foo"}, 117 }, 118 WantedNames: []string{}, 119 }, 120 { 121 Name: "dynamic_usage_group_names_empty_label", 122 TenantID: "tenant1", 123 Config: map[string]string{ 124 "app/${labels.service_name}": `{service_name=~"(.*)"}`, 125 }, 126 Labels: phlaremodel.Labels{ 127 {Name: "service_name", Value: ""}, 128 }, 129 WantedNames: []string{}, 130 }, 131 } 132 133 for _, tt := range tests { 134 t.Run(tt.Name, func(t *testing.T) { 135 config, err := NewUsageGroupConfig(tt.Config) 136 require.NoError(t, err) 137 138 evaluator := NewUsageGroupEvaluator(util.Logger) 139 got := evaluator.GetMatch(tt.TenantID, config, tt.Labels) 140 141 gotNames := make([]string, len(got.names)) 142 for i, name := range got.names { 143 gotNames[i] = name.ResolvedName 144 } 145 slices.Sort(gotNames) 146 slices.Sort(tt.WantedNames) 147 require.Equal(t, tt.WantedNames, gotNames) 148 }) 149 } 150 } 151 152 func TestUsageGroupMatch_CountReceivedBytes(t *testing.T) { 153 tests := []struct { 154 Name string 155 Match UsageGroupMatch 156 Count int64 157 WantCounts map[string]float64 158 }{ 159 { 160 Name: "single_usage_group_match", 161 Match: UsageGroupMatch{ 162 tenantID: "tenant1", 163 names: []UsageGroupMatchName{{ResolvedName: "app/foo"}}, 164 }, 165 Count: 100, 166 WantCounts: map[string]float64{ 167 "app/foo": 100, 168 "app/foo2": 0, 169 "other": 0, 170 }, 171 }, 172 { 173 Name: "multiple_usage_group_matches", 174 Match: UsageGroupMatch{ 175 tenantID: "tenant1", 176 names: []UsageGroupMatchName{ 177 {ResolvedName: "app/foo"}, 178 {ResolvedName: "app/foo2"}, 179 }, 180 }, 181 Count: 100, 182 WantCounts: map[string]float64{ 183 "app/foo": 100, 184 "app/foo2": 100, 185 "other": 0, 186 }, 187 }, 188 { 189 Name: "no_usage_group_matches", 190 Match: UsageGroupMatch{ 191 tenantID: "tenant1", 192 names: []UsageGroupMatchName{}, 193 }, 194 Count: 100, 195 WantCounts: map[string]float64{ 196 "app/foo": 0, 197 "app/foo2": 0, 198 "other": 100, 199 }, 200 }, 201 } 202 203 for _, tt := range tests { 204 t.Run(tt.Name, func(t *testing.T) { 205 const profileType = "cpu" 206 usageGroupReceivedDecompressedBytes.Reset() 207 208 tt.Match.CountReceivedBytes(profileType, tt.Count) 209 210 for name, want := range tt.WantCounts { 211 collector := usageGroupReceivedDecompressedBytes.WithLabelValues( 212 profileType, 213 tt.Match.tenantID, 214 name, 215 ) 216 217 got := testutil.ToFloat64(collector) 218 require.Equal(t, got, want, "usage group %s has incorrect metric value", name) 219 } 220 }) 221 } 222 } 223 224 func TestUsageGroupMatch_CountDiscardedBytes(t *testing.T) { 225 tests := []struct { 226 Name string 227 Match UsageGroupMatch 228 Count int64 229 WantCounts map[string]float64 230 }{ 231 { 232 Name: "single_usage_group_match", 233 Match: UsageGroupMatch{ 234 tenantID: "tenant1", 235 names: []UsageGroupMatchName{{ResolvedName: "app/foo"}}, 236 }, 237 Count: 100, 238 WantCounts: map[string]float64{ 239 "app/foo": 100, 240 "app/foo2": 0, 241 "other": 0, 242 }, 243 }, 244 { 245 Name: "multiple_usage_group_matches", 246 Match: UsageGroupMatch{ 247 tenantID: "tenant1", 248 names: []UsageGroupMatchName{ 249 {ResolvedName: "app/foo"}, 250 {ResolvedName: "app/foo2"}, 251 }, 252 }, 253 Count: 100, 254 WantCounts: map[string]float64{ 255 "app/foo": 100, 256 "app/foo2": 100, 257 "other": 0, 258 }, 259 }, 260 { 261 Name: "no_usage_group_matches", 262 Match: UsageGroupMatch{ 263 tenantID: "tenant1", 264 names: []UsageGroupMatchName{}, 265 }, 266 Count: 100, 267 WantCounts: map[string]float64{ 268 "app/foo": 0, 269 "app/foo2": 0, 270 "other": 100, 271 }, 272 }, 273 } 274 275 for _, tt := range tests { 276 t.Run(tt.Name, func(t *testing.T) { 277 const reason = "no_reason" 278 usageGroupDiscardedBytes.Reset() 279 280 tt.Match.CountDiscardedBytes(reason, tt.Count) 281 282 for name, want := range tt.WantCounts { 283 collector := usageGroupDiscardedBytes.WithLabelValues( 284 reason, 285 tt.Match.tenantID, 286 name, 287 ) 288 289 got := testutil.ToFloat64(collector) 290 require.Equal(t, got, want, "usage group %q has incorrect metric value", name) 291 } 292 }) 293 } 294 } 295 296 func (c *UsageGroupConfig) valuesMap() map[string][]string { 297 m := make(map[string][]string) 298 for k, v := range c.config { 299 for _, matcher := range v { 300 m[k] = append(m[k], matcher.String()) 301 } 302 } 303 return m 304 } 305 306 func TestNewUsageGroupConfig(t *testing.T) { 307 tests := []struct { 308 Name string 309 ConfigMap map[string]string 310 Want *UsageGroupConfig 311 WantErr string 312 }{ 313 { 314 Name: "single_usage_group", 315 ConfigMap: map[string]string{ 316 "app/foo": `{service_name="foo"}`, 317 }, 318 Want: &UsageGroupConfig{ 319 config: map[string][]*labels.Matcher{ 320 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 321 }, 322 }, 323 }, 324 { 325 Name: "multiple_usage_groups", 326 ConfigMap: map[string]string{ 327 "app/foo": `{service_name="foo"}`, 328 "app/foo2": `{service_name="foo", namespace=~"bar.*"}`, 329 }, 330 Want: &UsageGroupConfig{ 331 config: map[string][]*labels.Matcher{ 332 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 333 "app/foo2": testMustParseMatcher(t, `{service_name="foo", namespace=~"bar.*"}`), 334 }, 335 }, 336 }, 337 { 338 Name: "no_usage_groups", 339 ConfigMap: map[string]string{}, 340 Want: &UsageGroupConfig{ 341 config: map[string][]*labels.Matcher{}, 342 }, 343 }, 344 { 345 Name: "wildcard_matcher", 346 ConfigMap: map[string]string{ 347 "app/foo": `{}`, 348 }, 349 Want: &UsageGroupConfig{ 350 config: map[string][]*labels.Matcher{ 351 "app/foo": testMustParseMatcher(t, `{}`), 352 }, 353 }, 354 }, 355 { 356 Name: "too_many_usage_groups", 357 ConfigMap: func() map[string]string { 358 m := make(map[string]string) 359 for i := 0; i < maxUsageGroups+1; i++ { 360 m[fmt.Sprintf("app/foo%d", i)] = `{service_name="foo"}` 361 } 362 return m 363 }(), 364 WantErr: fmt.Sprintf("maximum number of usage groups is %d, got %d", maxUsageGroups, maxUsageGroups+1), 365 }, 366 { 367 Name: "invalid_matcher", 368 ConfigMap: map[string]string{ 369 "app/foo": `????`, 370 }, 371 WantErr: `failed to parse matchers for usage group "app/foo": 1:1: parse error: unexpected character: '?'`, 372 }, 373 { 374 Name: "empty_matcher", 375 ConfigMap: map[string]string{ 376 "app/foo": ``, 377 }, 378 WantErr: `failed to parse matchers for usage group "app/foo": unknown position: parse error: unexpected end of input`, 379 }, 380 { 381 Name: "empty_name", 382 ConfigMap: map[string]string{ 383 "": `{service_name="foo"}`, 384 }, 385 WantErr: "usage group name cannot be empty", 386 }, 387 { 388 Name: "whitespace_name", 389 ConfigMap: map[string]string{ 390 " app/foo ": `{service_name="foo"}`, 391 }, 392 Want: &UsageGroupConfig{ 393 config: map[string][]*labels.Matcher{ 394 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 395 }, 396 }, 397 }, 398 { 399 Name: "reserved_name", 400 ConfigMap: map[string]string{ 401 noMatchName: `{service_name="foo"}`, 402 }, 403 WantErr: fmt.Sprintf("usage group name %q is reserved", noMatchName), 404 }, 405 { 406 Name: "invalid_utf8_name", 407 ConfigMap: map[string]string{ 408 "app/\x80foo": `{service_name="foo"}`, 409 }, 410 WantErr: `usage group name "app/\x80foo" is not valid UTF-8`, 411 }, 412 } 413 414 for _, tt := range tests { 415 t.Run(tt.Name, func(t *testing.T) { 416 got, err := NewUsageGroupConfig(tt.ConfigMap) 417 if tt.WantErr != "" { 418 require.EqualError(t, err, tt.WantErr) 419 } else { 420 require.NoError(t, err) 421 require.Equal(t, tt.Want.valuesMap(), got.valuesMap()) 422 } 423 }) 424 } 425 } 426 427 func TestUsageGroupConfig_UnmarshalYAML(t *testing.T) { 428 type Object struct { 429 UsageGroups UsageGroupConfig `yaml:"usage_groups"` 430 } 431 432 tests := []struct { 433 Name string 434 YAML string 435 Want *UsageGroupConfig 436 WantErr string 437 }{ 438 { 439 Name: "single_usage_group", 440 YAML: ` 441 usage_groups: 442 app/foo: '{service_name="foo"}'`, 443 Want: &UsageGroupConfig{ 444 config: map[string][]*labels.Matcher{ 445 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 446 }, 447 }, 448 }, 449 { 450 Name: "multiple_usage_groups", 451 YAML: ` 452 usage_groups: 453 app/foo: '{service_name="foo"}' 454 app/foo2: '{service_name="foo", namespace=~"bar.*"}'`, 455 Want: &UsageGroupConfig{ 456 config: map[string][]*labels.Matcher{ 457 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 458 "app/foo2": testMustParseMatcher(t, `{service_name="foo", namespace=~"bar.*"}`), 459 }, 460 }, 461 }, 462 { 463 Name: "empty_usage_groups", 464 YAML: ` 465 usage_groups: {}`, 466 Want: &UsageGroupConfig{ 467 config: map[string][]*labels.Matcher{}, 468 }, 469 }, 470 { 471 Name: "invalid_yaml", 472 YAML: `usage_groups: ?????`, 473 WantErr: "malformed usage group config: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `?????` into map[string]string", 474 }, 475 { 476 Name: "invalid_matcher", 477 YAML: ` 478 usage_groups: 479 app/foo: ?????`, 480 WantErr: `failed to parse matchers for usage group "app/foo": 1:1: parse error: unexpected character: '?'`, 481 }, 482 { 483 Name: "missing_usage_groups_key_in_config", 484 YAML: ` 485 some_other_config: 486 foo: bar`, 487 Want: &UsageGroupConfig{}, 488 }, 489 } 490 491 for _, tt := range tests { 492 t.Run(tt.Name, func(t *testing.T) { 493 got := Object{} 494 err := yaml.Unmarshal([]byte(tt.YAML), &got) 495 if tt.WantErr != "" { 496 require.EqualError(t, err, tt.WantErr) 497 } else { 498 require.NoError(t, err) 499 require.Equal(t, tt.Want.valuesMap(), got.UsageGroups.valuesMap()) 500 } 501 }) 502 } 503 } 504 505 func TestUsageGroupConfig_UnmarshalJSON(t *testing.T) { 506 type Object struct { 507 UsageGroups UsageGroupConfig `json:"usage_groups"` 508 } 509 510 tests := []struct { 511 Name string 512 JSON string 513 Want *UsageGroupConfig 514 WantErr string 515 }{ 516 { 517 Name: "single_usage_group", 518 JSON: `{ 519 "usage_groups": { 520 "app/foo": "{service_name=\"foo\"}" 521 } 522 }`, 523 Want: &UsageGroupConfig{ 524 config: map[string][]*labels.Matcher{ 525 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 526 }, 527 }, 528 }, 529 { 530 Name: "multiple_usage_groups", 531 JSON: `{ 532 "usage_groups": { 533 "app/foo": "{service_name=\"foo\"}", 534 "app/foo2": "{service_name=\"foo\", namespace=~\"bar.*\"}" 535 } 536 }`, 537 Want: &UsageGroupConfig{ 538 config: map[string][]*labels.Matcher{ 539 "app/foo": testMustParseMatcher(t, `{service_name="foo"}`), 540 "app/foo2": testMustParseMatcher(t, `{service_name="foo", namespace=~"bar.*"}`), 541 }, 542 }, 543 }, 544 { 545 Name: "empty_usage_groups", 546 JSON: `{"usage_groups": {}}`, 547 Want: &UsageGroupConfig{ 548 config: map[string][]*labels.Matcher{}, 549 }, 550 }, 551 { 552 Name: "invalid_json", 553 JSON: `{"usage_groups": "?????"}`, 554 WantErr: "malformed usage group config: json: cannot unmarshal string into Go value of type map[string]string", 555 }, 556 { 557 Name: "invalid_matcher", 558 JSON: `{"usage_groups": {"app/foo": "?????"}}`, 559 WantErr: `failed to parse matchers for usage group "app/foo": 1:1: parse error: unexpected character: '?'`, 560 }, 561 { 562 Name: "missing_usage_groups_key_in_config", 563 JSON: `{"some_other_key": {"foo": "bar"}}`, 564 Want: &UsageGroupConfig{}, 565 }, 566 } 567 568 for _, tt := range tests { 569 t.Run(tt.Name, func(t *testing.T) { 570 got := Object{} 571 err := json.Unmarshal([]byte(tt.JSON), &got) 572 if tt.WantErr != "" { 573 require.EqualError(t, err, tt.WantErr) 574 } else { 575 require.NoError(t, err) 576 require.Equal(t, tt.Want.valuesMap(), got.UsageGroups.valuesMap()) 577 } 578 }) 579 } 580 } 581 582 func testMustParseMatcher(t *testing.T, s string) []*labels.Matcher { 583 m, err := parser.ParseMetricSelector(s) 584 require.NoError(t, err) 585 return m 586 } 587 588 func TestUsageGroupMatchName_IsMoreSpecificThan(t *testing.T) { 589 tests := []struct { 590 Name string 591 Match UsageGroupMatchName 592 Other UsageGroupMatchName 593 Want bool 594 }{ 595 { 596 Name: "same name", 597 Match: UsageGroupMatchName{ConfiguredName: "app/foo"}, 598 Other: UsageGroupMatchName{ConfiguredName: "app/foo"}, 599 Want: false, 600 }, 601 { 602 Name: "less specific name", 603 Match: UsageGroupMatchName{ConfiguredName: "${labels.service_name}"}, 604 Other: UsageGroupMatchName{ConfiguredName: "test-service"}, 605 Want: false, 606 }, 607 { 608 Name: "more specific name", 609 Match: UsageGroupMatchName{ConfiguredName: "test-service"}, 610 Other: UsageGroupMatchName{ConfiguredName: "${labels.service_name}"}, 611 Want: true, 612 }, 613 { 614 Name: "more specific name with prefix", 615 Match: UsageGroupMatchName{ConfiguredName: "test-service"}, 616 Other: UsageGroupMatchName{ConfiguredName: "service/${labels.service_name}"}, 617 Want: true, 618 }, 619 } 620 for _, tt := range tests { 621 t.Run(tt.Name, func(t *testing.T) { 622 require.Equal(t, tt.Want, tt.Match.IsMoreSpecificThan(&tt.Other)) 623 }) 624 } 625 }