github.com/grafana/pyroscope@v1.18.0/pkg/model/pprofsplit/pprof_split_by_test.go (about) 1 package pprofsplit 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 8 "github.com/prometheus/common/model" 9 "github.com/prometheus/prometheus/model/relabel" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 14 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 15 ) 16 17 type testSample struct { 18 labels string // "foo=bar,baz=qux" 19 value int64 20 } 21 22 type expectedSeries struct { 23 labels string // "foo=bar,baz=qux" 24 samples []testSample // samples with their labels 25 } 26 27 func Test_VisitSampleSeriesBy(t *testing.T) { 28 // Test cases are mostly generated by AI. 29 // Some very specific cases were added manually. 30 testCases := []struct { 31 description string 32 seriesLabels string // Series-level labels. Label order matters. 33 samples []testSample // Input samples. Label order does not matter. 34 splitBy []string // Labels to split by. 35 rules []*relabel.Config // Relabel rules to apply. 36 expected []expectedSeries 37 }{ 38 { 39 description: "split profile by group by labels", 40 seriesLabels: "__name__=profile,foo=bar", 41 samples: []testSample{ 42 {labels: "service_name=web,endpoint=/users", value: 100}, 43 {labels: "service_name=api,endpoint=/users", value: 300}, 44 }, 45 splitBy: []string{"service_name"}, 46 expected: []expectedSeries{ 47 { 48 labels: "__name__=profile,endpoint=/users,foo=bar,service_name=web", 49 samples: []testSample{ 50 {labels: "", value: 100}, 51 }, 52 }, 53 { 54 labels: "__name__=profile,endpoint=/users,foo=bar,service_name=api", 55 samples: []testSample{ 56 {labels: "", value: 300}, 57 }, 58 }, 59 }, 60 }, 61 { 62 description: "group by labels are not overridden", 63 seriesLabels: "__name__=profile,foo=bar,service_name=app", 64 samples: []testSample{ 65 {labels: "service_name=web,endpoint=/users", value: 100}, 66 {labels: "service_name=api,endpoint=/orders", value: 300}, 67 }, 68 splitBy: []string{"service_name"}, 69 expected: []expectedSeries{ 70 { 71 labels: "__name__=profile,foo=bar,service_name=app", 72 samples: []testSample{ 73 {labels: "endpoint=/users", value: 100}, 74 {labels: "endpoint=/orders", value: 300}, 75 }, 76 }, 77 }, 78 }, 79 { 80 description: "split by multiple labels", 81 seriesLabels: "__name__=profile,app=web", 82 samples: []testSample{ 83 {labels: "service_name=auth,region=us-east,endpoint=/login", value: 150}, 84 {labels: "service_name=auth,region=us-west,endpoint=/login", value: 200}, 85 {labels: "service_name=api,region=us-east,endpoint=/users", value: 250}, 86 {labels: "service_name=api,region=us-west,endpoint=/orders", value: 300}, 87 }, 88 splitBy: []string{"service_name", "region"}, 89 expected: []expectedSeries{ 90 { 91 labels: "__name__=profile,app=web,endpoint=/login,region=us-east,service_name=auth", 92 samples: []testSample{ 93 {labels: "", value: 150}, 94 }, 95 }, 96 { 97 labels: "__name__=profile,app=web,endpoint=/login,region=us-west,service_name=auth", 98 samples: []testSample{ 99 {labels: "", value: 200}, 100 }, 101 }, 102 { 103 labels: "__name__=profile,app=web,endpoint=/users,region=us-east,service_name=api", 104 samples: []testSample{ 105 {labels: "", value: 250}, 106 }, 107 }, 108 { 109 labels: "__name__=profile,app=web,endpoint=/orders,region=us-west,service_name=api", 110 samples: []testSample{ 111 {labels: "", value: 300}, 112 }, 113 }, 114 }, 115 }, 116 { 117 description: "split by non-existent label", 118 seriesLabels: "__name__=profile,app=test", 119 samples: []testSample{ 120 {labels: "service_name=web,endpoint=/users", value: 100}, 121 {labels: "service_name=api,endpoint=/orders", value: 200}, 122 }, 123 splitBy: []string{"missing_label"}, 124 expected: []expectedSeries{ 125 { 126 labels: "__name__=profile,app=test", 127 samples: []testSample{ 128 {labels: "endpoint=/users,service_name=web", value: 100}, 129 {labels: "endpoint=/orders,service_name=api", value: 200}, 130 }, 131 }, 132 }, 133 }, 134 { 135 description: "samples with no labels", 136 seriesLabels: "__name__=profile,env=prod", 137 samples: []testSample{ 138 {labels: "", value: 500}, 139 {labels: "", value: 600}, 140 }, 141 splitBy: []string{"service_name"}, 142 expected: []expectedSeries{}, 143 }, 144 { 145 description: "split by label with empty value", 146 seriesLabels: "__name__=profile,app=web", 147 samples: []testSample{ 148 {labels: "service_name=,endpoint=/health", value: 75}, 149 {labels: "service_name=api,endpoint=/health", value: 125}, 150 }, 151 splitBy: []string{"service_name"}, 152 expected: []expectedSeries{ 153 { 154 labels: "__name__=profile,app=web,endpoint=/health", 155 samples: []testSample{ 156 {labels: "", value: 75}, 157 }, 158 }, 159 { 160 labels: "__name__=profile,app=web,endpoint=/health,service_name=api", 161 samples: []testSample{ 162 {labels: "", value: 125}, 163 }, 164 }, 165 }, 166 }, 167 { 168 description: "no split by labels", 169 seriesLabels: "__name__=profile,env=test", 170 samples: []testSample{ 171 {labels: "service_name=web,endpoint=/users", value: 400}, 172 {labels: "service_name=api,endpoint=/orders", value: 500}, 173 }, 174 splitBy: []string{}, 175 expected: []expectedSeries{ 176 { 177 labels: "__name__=profile,env=test", 178 samples: []testSample{ 179 {labels: "endpoint=/users,service_name=web", value: 400}, 180 {labels: "endpoint=/orders,service_name=api", value: 500}, 181 }, 182 }, 183 }, 184 }, 185 { 186 description: "multiple samples with same split-by label value", 187 seriesLabels: "__name__=profile,version=1.0", 188 samples: []testSample{ 189 {labels: "service_name=web,method=GET", value: 100}, 190 {labels: "service_name=web,method=POST", value: 150}, 191 {labels: "service_name=api,method=GET", value: 200}, 192 }, 193 splitBy: []string{"service_name"}, 194 expected: []expectedSeries{ 195 { 196 labels: "__name__=profile,service_name=web,version=1.0", 197 samples: []testSample{ 198 {labels: "method=GET", value: 100}, 199 {labels: "method=POST", value: 150}, 200 }, 201 }, 202 { 203 labels: "__name__=profile,method=GET,service_name=api,version=1.0", 204 samples: []testSample{ 205 {labels: "", value: 200}, 206 }, 207 }, 208 }, 209 }, 210 { 211 description: "partial overlap between series and sample labels", 212 seriesLabels: "__name__=profile,service_name=main,region=us-east", 213 samples: []testSample{ 214 {labels: "service_name=web,env=prod", value: 300}, 215 {labels: "region=eu-west,env=staging", value: 400}, 216 }, 217 splitBy: []string{"env"}, 218 expected: []expectedSeries{ 219 { 220 labels: "__name__=profile,env=prod,region=us-east,service_name=main", 221 samples: []testSample{ 222 {labels: "", value: 300}, 223 }, 224 }, 225 { 226 labels: "__name__=profile,env=staging,region=us-east,service_name=main", 227 samples: []testSample{ 228 {labels: "", value: 400}, 229 }, 230 }, 231 }, 232 }, 233 { 234 description: "complex scenario with overlapping labels", 235 seriesLabels: "__name__=profile,app=frontend,env=prod", 236 samples: []testSample{ 237 {labels: "service_name=auth,env=staging,version=v1", value: 100}, 238 {labels: "service_name=auth,region=us-west,version=v2", value: 200}, 239 {labels: "service_name=api,env=dev,region=eu-central", value: 300}, 240 }, 241 splitBy: []string{"service_name", "version"}, 242 expected: []expectedSeries{ 243 { 244 labels: "__name__=profile,app=frontend,env=prod,service_name=auth,version=v1", 245 samples: []testSample{ 246 {labels: "", value: 100}, 247 }, 248 }, 249 { 250 labels: "__name__=profile,app=frontend,env=prod,region=us-west,service_name=auth,version=v2", 251 samples: []testSample{ 252 {labels: "", value: 200}, 253 }, 254 }, 255 { 256 labels: "__name__=profile,app=frontend,env=prod,region=eu-central,service_name=api", 257 samples: []testSample{ 258 {labels: "", value: 300}, 259 }, 260 }, 261 }, 262 }, 263 { 264 description: "single sample with multiple labels", 265 seriesLabels: "__name__=profile,region=us-east", 266 samples: []testSample{ 267 {labels: "service_name=web,method=GET,status=200", value: 42}, 268 }, 269 splitBy: []string{"service_name"}, 270 expected: []expectedSeries{ 271 { 272 labels: "__name__=profile,method=GET,region=us-east,service_name=web,status=200", 273 samples: []testSample{ 274 {labels: "", value: 42}, 275 }, 276 }, 277 }, 278 }, 279 { 280 description: "mixed samples - some with labels, some without", 281 seriesLabels: "__name__=profile,app=myapp", 282 samples: []testSample{ 283 {labels: "service_name=auth,endpoint=/login", value: 100}, 284 {labels: "", value: 200}, 285 {labels: "service_name=api", value: 300}, 286 }, 287 splitBy: []string{"service_name"}, 288 expected: []expectedSeries{ 289 { 290 labels: "__name__=profile,app=myapp", 291 samples: []testSample{ 292 {labels: "", value: 200}, 293 }, 294 }, 295 { 296 labels: "__name__=profile,app=myapp,endpoint=/login,service_name=auth", 297 samples: []testSample{ 298 {labels: "", value: 100}, 299 }, 300 }, 301 { 302 labels: "__name__=profile,app=myapp,service_name=api", 303 samples: []testSample{ 304 {labels: "", value: 300}, 305 }, 306 }, 307 }, 308 }, 309 { 310 description: "split by multiple labels with partial matches", 311 seriesLabels: "__name__=profile,environment=prod", 312 samples: []testSample{ 313 {labels: "service_name=web,region=us-east,tier=frontend", value: 100}, 314 {labels: "service_name=web,tier=frontend", value: 150}, 315 {labels: "service_name=api,region=us-west", value: 200}, 316 {labels: "region=eu-central", value: 250}, 317 }, 318 splitBy: []string{"service_name", "region"}, 319 expected: []expectedSeries{ 320 { 321 labels: "__name__=profile,environment=prod,region=us-east,service_name=web,tier=frontend", 322 samples: []testSample{ 323 {labels: "", value: 100}, 324 }, 325 }, 326 { 327 labels: "__name__=profile,environment=prod,service_name=web,tier=frontend", 328 samples: []testSample{ 329 {labels: "", value: 150}, 330 }, 331 }, 332 { 333 labels: "__name__=profile,environment=prod,region=us-west,service_name=api", 334 samples: []testSample{ 335 {labels: "", value: 200}, 336 }, 337 }, 338 { 339 labels: "__name__=profile,environment=prod,region=eu-central", 340 samples: []testSample{ 341 {labels: "", value: 250}, 342 }, 343 }, 344 }, 345 }, 346 { 347 description: "unicode and special characters in labels", 348 seriesLabels: "__name__=profile,app=测试应用", 349 samples: []testSample{ 350 {labels: "service_name=微服务-api,endpoint=/用户/登录", value: 100}, 351 {labels: "service_name=web-frontend,endpoint=/status", value: 200}, 352 }, 353 splitBy: []string{"service_name"}, 354 expected: []expectedSeries{ 355 { 356 labels: "__name__=profile,app=测试应用,endpoint=/用户/登录,service_name=微服务-api", 357 samples: []testSample{ 358 {labels: "", value: 100}, 359 }, 360 }, 361 { 362 labels: "__name__=profile,app=测试应用,endpoint=/status,service_name=web-frontend", 363 samples: []testSample{ 364 {labels: "", value: 200}, 365 }, 366 }, 367 }, 368 }, 369 { 370 description: "many labels with different combinations", 371 seriesLabels: "__name__=profile,cluster=prod-cluster", 372 samples: []testSample{ 373 {labels: "service=auth,method=POST,status=200,region=us-east,az=us-east-1a", value: 50}, 374 {labels: "service=auth,method=POST,status=200,region=us-east,az=us-east-1b", value: 75}, 375 {labels: "service=auth,method=GET,status=200,region=us-west,az=us-west-2a", value: 25}, 376 {labels: "service=api,method=POST,status=500,region=eu-central,az=eu-central-1a", value: 100}, 377 }, 378 splitBy: []string{"service", "method", "status"}, 379 expected: []expectedSeries{ 380 { 381 labels: "__name__=profile,cluster=prod-cluster,method=POST,region=us-east,service=auth,status=200", 382 samples: []testSample{ 383 {labels: "az=us-east-1a", value: 50}, 384 {labels: "az=us-east-1b", value: 75}, 385 }, 386 }, 387 { 388 labels: "__name__=profile,az=us-west-2a,cluster=prod-cluster,method=GET,region=us-west,service=auth,status=200", 389 samples: []testSample{ 390 {labels: "", value: 25}, 391 }, 392 }, 393 { 394 labels: "__name__=profile,az=eu-central-1a,cluster=prod-cluster,method=POST,region=eu-central,service=api,status=500", 395 samples: []testSample{ 396 {labels: "", value: 100}, 397 }, 398 }, 399 }, 400 }, 401 { 402 description: "split by labels that exist in series labels", 403 seriesLabels: "__name__=profile,service_name=main-service,region=global", 404 samples: []testSample{ 405 {labels: "service_name=auth,endpoint=/login", value: 100}, 406 {labels: "region=us-east,endpoint=/health", value: 200}, 407 {labels: "method=GET", value: 300}, 408 }, 409 splitBy: []string{"service_name", "region"}, 410 expected: []expectedSeries{ 411 { 412 labels: "__name__=profile,region=global,service_name=main-service", 413 samples: []testSample{ 414 {labels: "endpoint=/login", value: 100}, 415 {labels: "endpoint=/health", value: 200}, 416 {labels: "method=GET", value: 300}, 417 }, 418 }, 419 }, 420 }, 421 { 422 description: "empty string values in split-by labels", 423 seriesLabels: "__name__=profile,app=test-app", 424 samples: []testSample{ 425 {labels: "env=,version=v1.0,service=web", value: 100}, 426 {labels: "env=prod,version=,service=api", value: 200}, 427 {labels: "env=staging,version=v2.0,service=", value: 300}, 428 }, 429 splitBy: []string{"env", "version", "service"}, 430 expected: []expectedSeries{ 431 { 432 labels: "__name__=profile,app=test-app,service=web,version=v1.0", 433 samples: []testSample{ 434 {labels: "", value: 100}, 435 }, 436 }, 437 { 438 labels: "__name__=profile,app=test-app,env=prod,service=api", 439 samples: []testSample{ 440 {labels: "", value: 200}, 441 }, 442 }, 443 { 444 labels: "__name__=profile,app=test-app,env=staging,version=v2.0", 445 samples: []testSample{ 446 {labels: "", value: 300}, 447 }, 448 }, 449 }, 450 }, 451 { 452 description: "duplicate split-by label values across different samples", 453 seriesLabels: "__name__=profile,datacenter=dc1", 454 samples: []testSample{ 455 {labels: "service=web,tier=frontend,instance=web-1", value: 100}, 456 {labels: "service=web,tier=frontend,instance=web-2", value: 150}, 457 {labels: "service=web,tier=backend,instance=web-3", value: 200}, 458 {labels: "service=api,tier=frontend,instance=api-1", value: 250}, 459 }, 460 splitBy: []string{"service", "tier"}, 461 expected: []expectedSeries{ 462 { 463 labels: "__name__=profile,datacenter=dc1,service=web,tier=frontend", 464 samples: []testSample{ 465 {labels: "instance=web-1", value: 100}, 466 {labels: "instance=web-2", value: 150}, 467 }, 468 }, 469 { 470 labels: "__name__=profile,datacenter=dc1,instance=web-3,service=web,tier=backend", 471 samples: []testSample{ 472 {labels: "", value: 200}, 473 }, 474 }, 475 { 476 labels: "__name__=profile,datacenter=dc1,instance=api-1,service=api,tier=frontend", 477 samples: []testSample{ 478 {labels: "", value: 250}, 479 }, 480 }, 481 }, 482 }, 483 { 484 description: "relabel rules drop entire profile", 485 seriesLabels: "__name__=profile,app=test", 486 samples: []testSample{ 487 {labels: "service=auth,env=prod", value: 100}, 488 {labels: "service=api,env=staging", value: 200}, 489 }, 490 splitBy: []string{"service"}, 491 rules: []*relabel.Config{ 492 { 493 Action: relabel.Drop, 494 Regex: relabel.MustNewRegexp("test"), 495 SourceLabels: []model.LabelName{"app"}, 496 }, 497 }, 498 expected: []expectedSeries{}, 499 }, 500 { 501 description: "relabel rules drop some sample groups", 502 seriesLabels: "__name__=profile,component=backend", 503 samples: []testSample{ 504 {labels: "service=auth,env=prod", value: 100}, 505 {labels: "service=api,env=staging", value: 200}, 506 {labels: "service=web,env=prod", value: 300}, 507 }, 508 splitBy: []string{"service"}, 509 rules: []*relabel.Config{ 510 { 511 Action: relabel.Drop, 512 Regex: relabel.MustNewRegexp("staging"), 513 SourceLabels: []model.LabelName{"env"}, 514 }, 515 }, 516 expected: []expectedSeries{ 517 { 518 labels: "__name__=profile,component=backend,env=prod,service=auth", 519 samples: []testSample{ 520 {labels: "", value: 100}, 521 }, 522 }, 523 { 524 labels: "__name__=profile,component=backend,env=prod,service=web", 525 samples: []testSample{ 526 {labels: "", value: 300}, 527 }, 528 }, 529 }, 530 }, 531 { 532 description: "samples with same stack trace get merged", 533 seriesLabels: "__name__=profile,app=merger", 534 samples: []testSample{ 535 {labels: "service=auth,method=GET", value: 100}, 536 {labels: "service=auth,method=GET", value: 50}, // Same labels but different location IDs won't merge 537 {labels: "service=api,method=POST", value: 200}, 538 }, 539 splitBy: []string{"service"}, 540 expected: []expectedSeries{ 541 { 542 labels: "__name__=profile,app=merger,method=GET,service=auth", 543 samples: []testSample{ 544 {labels: "", value: 100}, 545 {labels: "", value: 50}, // Separate samples since different location IDs 546 }, 547 }, 548 { 549 labels: "__name__=profile,app=merger,method=POST,service=api", 550 samples: []testSample{ 551 {labels: "", value: 200}, 552 }, 553 }, 554 }, 555 }, 556 { 557 description: "empty profile after all groups dropped by relabel rules", 558 seriesLabels: "__name__=profile,app=filter", 559 samples: []testSample{ 560 {labels: "drop=true,service=auth", value: 100}, 561 {labels: "drop=true,service=api", value: 200}, 562 }, 563 splitBy: []string{"service"}, 564 rules: []*relabel.Config{ 565 { 566 Action: relabel.Drop, 567 Regex: relabel.MustNewRegexp("true"), 568 SourceLabels: []model.LabelName{"drop"}, 569 }, 570 }, 571 expected: []expectedSeries{}, 572 }, 573 { 574 description: "complex sample merging with multiple values", 575 seriesLabels: "__name__=profile,cluster=main", 576 samples: []testSample{ 577 {labels: "service=web,endpoint=/api", value: 100}, 578 {labels: "service=web,endpoint=/api", value: 150}, // Same labels but different LocationIds 579 {labels: "service=web,endpoint=/health", value: 50}, 580 {labels: "service=web,endpoint=/api", value: 75}, // Same labels but different LocationIds 581 }, 582 splitBy: []string{"service"}, 583 expected: []expectedSeries{ 584 { 585 labels: "__name__=profile,cluster=main,service=web", 586 samples: []testSample{ 587 {labels: "endpoint=/api", value: 100}, 588 {labels: "endpoint=/api", value: 150}, 589 {labels: "endpoint=/api", value: 75}, 590 {labels: "endpoint=/health", value: 50}, 591 }, 592 }, 593 }, 594 }, 595 { 596 description: "string table expansion with new label names and values", 597 seriesLabels: "__name__=profile,existing_label=existing_value", 598 samples: []testSample{ 599 {labels: "completely_new_label=completely_new_value", value: 100}, 600 {labels: "another_new_label=another_new_value", value: 200}, 601 }, 602 splitBy: []string{"completely_new_label"}, 603 expected: []expectedSeries{ 604 { 605 labels: "__name__=profile,completely_new_label=completely_new_value,existing_label=existing_value", 606 samples: []testSample{ 607 {labels: "", value: 100}, 608 }, 609 }, 610 { 611 labels: "__name__=profile,another_new_label=another_new_value,existing_label=existing_value", 612 samples: []testSample{ 613 {labels: "", value: 200}, 614 }, 615 }, 616 }, 617 }, 618 { 619 description: "samples with identical location IDs for merging", 620 seriesLabels: "__name__=profile,service=test", 621 samples: []testSample{ 622 {labels: "endpoint=/api,method=GET", value: 200}, 623 {labels: "endpoint=/api,method=GET", value: 300}, // Same labels but different location IDs 624 {labels: "endpoint=/health,method=GET", value: 100}, 625 }, 626 splitBy: []string{"endpoint"}, 627 expected: []expectedSeries{ 628 { 629 labels: "__name__=profile,endpoint=/api,method=GET,service=test", 630 samples: []testSample{ 631 {labels: "", value: 200}, 632 {labels: "", value: 300}, // Separate samples due to different location IDs 633 }, 634 }, 635 { 636 labels: "__name__=profile,endpoint=/health,method=GET,service=test", 637 samples: []testSample{ 638 {labels: "", value: 100}, 639 }, 640 }, 641 }, 642 }, 643 { 644 description: "fingerprint collision with different label sets", 645 seriesLabels: "__name__=profile,cluster=test", 646 samples: []testSample{ 647 // These might create hash collisions but have different actual labels 648 {labels: "service=a,region=b", value: 100}, 649 {labels: "service=c,region=d", value: 200}, 650 {labels: "service=a,region=b", value: 50}, // Same labels but different location IDs 651 }, 652 splitBy: []string{"service"}, 653 expected: []expectedSeries{ 654 { 655 labels: "__name__=profile,cluster=test,region=b,service=a", 656 samples: []testSample{ 657 {labels: "", value: 100}, 658 {labels: "", value: 50}, // Separate samples due to different location IDs 659 }, 660 }, 661 { 662 labels: "__name__=profile,cluster=test,region=d,service=c", 663 samples: []testSample{ 664 {labels: "", value: 200}, 665 }, 666 }, 667 }, 668 }, 669 { 670 description: "no samples in profile", 671 seriesLabels: "__name__=profile,app=empty", 672 samples: []testSample{}, 673 splitBy: []string{"service"}, 674 expected: []expectedSeries{}, 675 }, 676 { 677 description: "profile with only samples having empty values", 678 seriesLabels: "__name__=profile,app=zero", 679 samples: []testSample{ 680 {labels: "service=auth", value: 0}, 681 {labels: "service=api", value: 0}, 682 }, 683 splitBy: []string{"service"}, 684 expected: []expectedSeries{ 685 { 686 labels: "__name__=profile,app=zero,service=auth", 687 samples: []testSample{ 688 {labels: "", value: 0}, 689 }, 690 }, 691 { 692 labels: "__name__=profile,app=zero,service=api", 693 samples: []testSample{ 694 {labels: "", value: 0}, 695 }, 696 }, 697 }, 698 }, 699 { 700 description: "force string table expansion during sample processing", 701 seriesLabels: "__name__=profile,app=test", 702 samples: []testSample{ 703 {labels: "service=auth", value: 100}, 704 {labels: "service=api", value: 200}, 705 }, 706 splitBy: []string{"service"}, 707 expected: []expectedSeries{ 708 { 709 labels: "__name__=profile,app=test,service=auth", 710 samples: []testSample{ 711 {labels: "", value: 100}, 712 }, 713 }, 714 { 715 labels: "__name__=profile,app=test,service=api", 716 samples: []testSample{ 717 {labels: "", value: 200}, 718 }, 719 }, 720 }, 721 }, 722 { 723 description: "relabel rules drop pprof labels", 724 seriesLabels: "__name__=profile,app=test", 725 samples: []testSample{ 726 {labels: "service=auth,internal_debug=true,endpoint=/login", value: 100}, 727 {labels: "service=api,internal_debug=false,endpoint=/users", value: 200}, 728 {labels: "service=web,temp_flag=remove_me,endpoint=/health", value: 300}, 729 }, 730 splitBy: []string{"service"}, 731 rules: []*relabel.Config{ 732 { 733 Action: relabel.LabelDrop, 734 Regex: relabel.MustNewRegexp("internal_.*|temp_.*"), 735 }, 736 }, 737 expected: []expectedSeries{ 738 { 739 labels: "__name__=profile,app=test,endpoint=/login,service=auth", 740 samples: []testSample{ 741 {labels: "", value: 100}, 742 }, 743 }, 744 { 745 labels: "__name__=profile,app=test,endpoint=/users,service=api", 746 samples: []testSample{ 747 {labels: "", value: 200}, 748 }, 749 }, 750 { 751 labels: "__name__=profile,app=test,endpoint=/health,service=web", 752 samples: []testSample{ 753 {labels: "", value: 300}, 754 }, 755 }, 756 }, 757 }, 758 { 759 description: "relabel rules keep only labels without whitespace", 760 seriesLabels: "__name__=profile,app=filtertest,service=auth", 761 samples: []testSample{ 762 {labels: "bad label=value1,endpoint=/login,temp flag=debug", value: 100}, 763 {labels: "another bad=value2,endpoint=/users,good_label=keep", value: 200}, 764 {labels: "weird name=value3,endpoint=/health", value: 300}, 765 }, 766 splitBy: []string{"service"}, 767 rules: []*relabel.Config{ 768 { 769 Action: relabel.LabelKeep, 770 Regex: relabel.MustNewRegexp("^[^\\s]+$"), 771 }, 772 }, 773 expected: []expectedSeries{ 774 { 775 labels: "__name__=profile,app=filtertest,service=auth", 776 samples: []testSample{ 777 {labels: "endpoint=/login", value: 100}, 778 {labels: "endpoint=/users,good_label=keep", value: 200}, 779 {labels: "endpoint=/health", value: 300}, 780 }, 781 }, 782 }, 783 }, 784 { 785 description: "relabel rules replace dots with underscores in label values", 786 seriesLabels: "__name__=profile,app=normalizer", 787 samples: []testSample{ 788 {labels: "service=com.example.auth,endpoint=/api/v1.0,version=1.2.3", value: 100}, 789 {labels: "service=org.service.api,endpoint=/health.check,version=2.0.1", value: 200}, 790 {labels: "service=net.frontend.web,endpoint=/home.page,version=1.0.0", value: 300}, 791 }, 792 splitBy: []string{"service"}, 793 rules: []*relabel.Config{ 794 { 795 SourceLabels: []model.LabelName{"service"}, 796 Regex: relabel.MustNewRegexp("([^.]*)\\.(.*)\\.(.*)"), 797 Replacement: "${1}_${2}_${3}", 798 TargetLabel: "service", 799 Action: relabel.Replace, 800 }, 801 { 802 SourceLabels: []model.LabelName{"endpoint"}, 803 Regex: relabel.MustNewRegexp("([^.]*)\\.(.*)"), 804 Replacement: "${1}_${2}", 805 TargetLabel: "endpoint", 806 Action: relabel.Replace, 807 }, 808 { 809 SourceLabels: []model.LabelName{"version"}, 810 Regex: relabel.MustNewRegexp("([^.]*)\\.(.*)\\.(.*)"), 811 Replacement: "${1}_${2}_${3}", 812 TargetLabel: "version", 813 Action: relabel.Replace, 814 }, 815 }, 816 expected: []expectedSeries{ 817 { 818 labels: "__name__=profile,app=normalizer,endpoint=/api/v1_0,service=com_example_auth,version=1_2_3", 819 samples: []testSample{ 820 {labels: "", value: 100}, 821 }, 822 }, 823 { 824 labels: "__name__=profile,app=normalizer,endpoint=/health_check,service=org_service_api,version=2_0_1", 825 samples: []testSample{ 826 {labels: "", value: 200}, 827 }, 828 }, 829 { 830 labels: "__name__=profile,app=normalizer,endpoint=/home_page,service=net_frontend_web,version=1_0_0", 831 samples: []testSample{ 832 {labels: "", value: 300}, 833 }, 834 }, 835 }, 836 }, 837 } 838 839 for _, tc := range testCases { 840 t.Run(tc.description, func(t *testing.T) { 841 profile := &profilev1.Profile{ 842 StringTable: []string{""}, 843 Sample: make([]*profilev1.Sample, len(tc.samples)), 844 } 845 lookup := stringLookup(profile) 846 reverseLookup := stringReverseLookup(profile) 847 for i, s := range tc.samples { 848 profile.Sample[i] = &profilev1.Sample{ 849 LocationId: []uint64{uint64(i + 1)}, // unique location ID per sample 850 Value: []int64{s.value}, 851 Label: parseSampleLabels(t, lookup, s.labels), 852 } 853 } 854 855 visitor := new(mockVisitor) 856 seriesLabels := parseLabels(t, tc.seriesLabels) 857 require.NoError(t, VisitSampleSeriesBy(profile, seriesLabels, tc.rules, visitor, tc.splitBy...)) 858 require.Len(t, visitor.series, len(tc.expected)) 859 860 for i, actual := range visitor.series { 861 expected := tc.expected[i] 862 expectedLabels := parseLabels(t, expected.labels) 863 assert.Equal(t, expectedLabels, actual.labels, fmt.Sprintf("want: %s,\ngot: %s", 864 formatLabels(expectedLabels), 865 formatLabels(actual.labels))) 866 867 require.Len(t, actual.samples, len(expected.samples)) 868 for j, actualSample := range actual.samples { 869 expectedSample := expected.samples[j] 870 assert.Equal(t, expectedSample.value, actualSample.Value[0]) 871 expectedSampleLabels := parseSampleLabels(t, lookup, expectedSample.labels) 872 assert.Equal(t, expectedSampleLabels, actualSample.Label, fmt.Sprintf("want: %s, got %s", 873 formatSampleLabels(reverseLookup, expectedSampleLabels), 874 formatSampleLabels(reverseLookup, actualSample.Label))) 875 } 876 } 877 }) 878 } 879 } 880 881 func stringLookup(p *profilev1.Profile) func(string) int64 { 882 stringIndex := map[string]int64{"": 0} 883 return func(s string) int64 { 884 if idx, ok := stringIndex[s]; ok { 885 return idx 886 } 887 i := int64(len(p.StringTable)) 888 p.StringTable = append(p.StringTable, s) 889 stringIndex[s] = i 890 return i 891 } 892 } 893 894 func stringReverseLookup(p *profilev1.Profile) func(int64) string { 895 return func(i int64) string { 896 return p.StringTable[i] 897 } 898 } 899 900 func parseLabels(t *testing.T, s string) []*typesv1.LabelPair { 901 if s == "" { 902 // To simplify assertions we return an empty slice instead of nil. 903 return []*typesv1.LabelPair{} 904 } 905 var labels []*typesv1.LabelPair 906 for _, pair := range strings.Split(s, ",") { 907 parts := strings.SplitN(pair, "=", 2) 908 if len(parts) != 2 { 909 t.Fatal("invalid series labels:", s) 910 } 911 labels = append(labels, &typesv1.LabelPair{ 912 Name: parts[0], 913 Value: parts[1], 914 }) 915 } 916 return labels 917 } 918 919 func formatLabels(labels []*typesv1.LabelPair) string { 920 var b strings.Builder 921 for i, label := range labels { 922 if i > 0 { 923 b.WriteByte(',') 924 } 925 b.WriteString(label.Name + "=" + label.Value) 926 } 927 return b.String() 928 } 929 930 func parseSampleLabels(t *testing.T, str func(string) int64, s string) []*profilev1.Label { 931 if s == "" { 932 // To simplify assertions we return an empty slice instead of nil. 933 return []*profilev1.Label{} 934 } 935 var labels []*profilev1.Label 936 for _, pair := range strings.Split(s, ",") { 937 parts := strings.SplitN(pair, "=", 2) 938 if len(parts) != 2 { 939 t.Fatal("invalid sample labels:", s) 940 } 941 labels = append(labels, &profilev1.Label{ 942 Key: str(parts[0]), 943 Str: str(parts[1]), 944 }) 945 } 946 return labels 947 } 948 949 func formatSampleLabels(lookup func(int64) string, labels []*profilev1.Label) string { 950 var b strings.Builder 951 for i, label := range labels { 952 if i > 0 { 953 b.WriteByte(',') 954 } 955 b.WriteString(lookup(label.Key) + "=" + lookup(label.Str)) 956 } 957 return b.String() 958 }