github.com/grafana/pyroscope@v1.18.0/pkg/ingester/otlp/ingest_handler_test.go (about) 1 package otlp 2 3 import ( 4 "bytes" 5 "context" 6 "flag" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "sort" 11 "strings" 12 "testing" 13 14 "github.com/grafana/dskit/server" 15 "github.com/grafana/dskit/user" 16 "github.com/klauspost/compress/gzip" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/mock" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/protobuf/proto" 21 22 v1experimental2 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development" 23 v1 "go.opentelemetry.io/proto/otlp/common/v1" 24 v1experimental "go.opentelemetry.io/proto/otlp/profiles/v1development" 25 26 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 27 "github.com/grafana/pyroscope/pkg/distributor/model" 28 phlaremodel "github.com/grafana/pyroscope/pkg/model" 29 "github.com/grafana/pyroscope/pkg/og/convert/pprof/strprofile" 30 "github.com/grafana/pyroscope/pkg/tenant" 31 "github.com/grafana/pyroscope/pkg/test" 32 "github.com/grafana/pyroscope/pkg/test/mocks/mockotlp" 33 "github.com/grafana/pyroscope/pkg/util" 34 "github.com/grafana/pyroscope/pkg/validation" 35 ) 36 37 func TestGetServiceNameFromAttributes(t *testing.T) { 38 tests := []struct { 39 name string 40 attrs []*v1.KeyValue 41 expected string 42 }{ 43 { 44 name: "empty attributes", 45 attrs: []*v1.KeyValue{}, 46 expected: phlaremodel.AttrServiceNameFallback, 47 }, 48 { 49 name: "use executable name", 50 attrs: []*v1.KeyValue{ 51 { 52 Key: "process.executable.name", 53 Value: &v1.AnyValue{ 54 Value: &v1.AnyValue_StringValue{ 55 StringValue: "bash", 56 }, 57 }, 58 }, 59 }, 60 expected: phlaremodel.AttrServiceNameFallback + ":bash", 61 }, 62 { 63 name: "service name present", 64 attrs: []*v1.KeyValue{ 65 { 66 Key: "service.name", 67 Value: &v1.AnyValue{ 68 Value: &v1.AnyValue_StringValue{ 69 StringValue: "test-service", 70 }, 71 }, 72 }, 73 { 74 Key: "process.executable.name", 75 Value: &v1.AnyValue{ 76 Value: &v1.AnyValue_StringValue{ 77 StringValue: "test-executable", 78 }, 79 }, 80 }, 81 }, 82 expected: "test-service", 83 }, 84 { 85 name: "service name empty", 86 attrs: []*v1.KeyValue{ 87 { 88 Key: "service.name", 89 Value: &v1.AnyValue{ 90 Value: &v1.AnyValue_StringValue{ 91 StringValue: "", 92 }, 93 }, 94 }, 95 { 96 Key: "process.executable.name", 97 Value: &v1.AnyValue{ 98 Value: &v1.AnyValue_StringValue{ 99 StringValue: "", 100 }, 101 }, 102 }, 103 }, 104 expected: phlaremodel.AttrServiceNameFallback, 105 }, 106 { 107 name: "service name among other attributes", 108 attrs: []*v1.KeyValue{ 109 { 110 Key: "host.name", 111 Value: &v1.AnyValue{ 112 Value: &v1.AnyValue_StringValue{ 113 StringValue: "host1", 114 }, 115 }, 116 }, 117 { 118 Key: "service.name", 119 Value: &v1.AnyValue{ 120 Value: &v1.AnyValue_StringValue{ 121 StringValue: "test-service", 122 }, 123 }, 124 }, 125 }, 126 expected: "test-service", 127 }, 128 } 129 130 for _, tt := range tests { 131 t.Run(tt.name, func(t *testing.T) { 132 result := getServiceNameFromAttributes(tt.attrs) 133 assert.Equal(t, tt.expected, result) 134 }) 135 } 136 } 137 138 func TestAppendAttributesUnique(t *testing.T) { 139 tests := []struct { 140 name string 141 existingAttrs []*typesv1.LabelPair 142 newAttrs []*v1.KeyValue 143 processedKeys map[string]bool 144 expected []*typesv1.LabelPair 145 }{ 146 { 147 name: "empty attributes", 148 existingAttrs: []*typesv1.LabelPair{}, 149 newAttrs: []*v1.KeyValue{}, 150 processedKeys: make(map[string]bool), 151 expected: []*typesv1.LabelPair{}, 152 }, 153 { 154 name: "new unique attributes", 155 existingAttrs: []*typesv1.LabelPair{ 156 {Name: "existing", Value: "value"}, 157 }, 158 newAttrs: []*v1.KeyValue{ 159 { 160 Key: "new", 161 Value: &v1.AnyValue{ 162 Value: &v1.AnyValue_StringValue{ 163 StringValue: "newvalue", 164 }, 165 }, 166 }, 167 }, 168 processedKeys: map[string]bool{"existing": true}, 169 expected: []*typesv1.LabelPair{ 170 {Name: "existing", Value: "value"}, 171 {Name: "new", Value: "newvalue"}, 172 }, 173 }, 174 { 175 name: "duplicate attributes", 176 existingAttrs: []*typesv1.LabelPair{ 177 {Name: "key1", Value: "value1"}, 178 }, 179 newAttrs: []*v1.KeyValue{ 180 { 181 Key: "key1", 182 Value: &v1.AnyValue{ 183 Value: &v1.AnyValue_StringValue{ 184 StringValue: "value2", 185 }, 186 }, 187 }, 188 }, 189 processedKeys: map[string]bool{"key1": true}, 190 expected: []*typesv1.LabelPair{ 191 {Name: "key1", Value: "value1"}, 192 }, 193 }, 194 } 195 196 for _, tt := range tests { 197 t.Run(tt.name, func(t *testing.T) { 198 result := appendAttributesUnique(tt.existingAttrs, tt.newAttrs, tt.processedKeys) 199 assert.Equal(t, tt.expected, result) 200 }) 201 } 202 } 203 204 func readJSONFile(t *testing.T, filename string) string { 205 data, err := os.ReadFile(filename) 206 require.NoError(t, err, "filename: "+filename) 207 return string(data) 208 } 209 210 func TestConversion(t *testing.T) { 211 212 testdata := []struct { 213 name string 214 expectedJsonFile string 215 expectedError string 216 profile func() *otlpbuilder 217 }{ 218 { 219 name: "symbolized function names", 220 expectedJsonFile: "testdata/TestSymbolizedFunctionNames.json", 221 profile: func() *otlpbuilder { 222 b := new(otlpbuilder) 223 b.dictionary.MappingTable = []*v1experimental.Mapping{{ 224 MemoryStart: 0x1000, 225 MemoryLimit: 0x1000, 226 FilenameStrindex: b.addstr("file1.so"), 227 }} 228 b.dictionary.LocationTable = []*v1experimental.Location{{ 229 MappingIndex: 0, 230 Address: 0x1e0, 231 Lines: nil, 232 }, { 233 MappingIndex: 0, 234 Address: 0x2f0, 235 Lines: nil, 236 }} 237 b.dictionary.StackTable = []*v1experimental.Stack{{ 238 LocationIndices: []int32{0, 1}, 239 }} 240 b.profile.SampleType = &v1experimental.ValueType{ 241 TypeStrindex: b.addstr("samples"), 242 UnitStrindex: b.addstr("ms"), 243 } 244 b.profile.Samples = []*v1experimental.Sample{{ 245 StackIndex: 0, 246 Values: []int64{0xef}, 247 }} 248 return b 249 }, 250 }, 251 { 252 name: "offcpu", 253 expectedJsonFile: "testdata/TestConversionOffCpu.json", 254 profile: func() *otlpbuilder { 255 b := new(otlpbuilder) 256 b.profile.SampleType = &v1experimental.ValueType{ 257 TypeStrindex: b.addstr("events"), 258 UnitStrindex: b.addstr("nanoseconds"), 259 } 260 b.dictionary.MappingTable = []*v1experimental.Mapping{{ 261 MemoryStart: 0x1000, 262 MemoryLimit: 0x1000, 263 FilenameStrindex: b.addstr("file1.so"), 264 }} 265 b.dictionary.LocationTable = []*v1experimental.Location{{ 266 MappingIndex: 0, 267 Address: 0x1e0, 268 }, { 269 MappingIndex: 0, 270 Address: 0x2f0, 271 }, { 272 MappingIndex: 0, 273 Address: 0x3f0, 274 }} 275 b.dictionary.StackTable = []*v1experimental.Stack{{ 276 LocationIndices: []int32{0, 1}, 277 }, { 278 LocationIndices: []int32{2}, 279 }} 280 b.profile.Samples = []*v1experimental.Sample{{ 281 StackIndex: 0, 282 Values: []int64{0xef}, 283 }, { 284 StackIndex: 1, 285 Values: []int64{1, 2, 3, 4, 5, 6}, 286 }} 287 return b 288 }, 289 }, 290 { 291 name: "samples with different value sizes ", 292 expectedError: "sample values length mismatch", 293 profile: func() *otlpbuilder { 294 b := new(otlpbuilder) 295 b.profile.SampleType = &v1experimental.ValueType{ 296 TypeStrindex: b.addstr("wrote_type"), 297 UnitStrindex: b.addstr("wrong_unit"), 298 } 299 b.dictionary.MappingTable = []*v1experimental.Mapping{{ 300 MemoryStart: 0x1000, 301 MemoryLimit: 0x1000, 302 FilenameStrindex: b.addstr("file1.so"), 303 }} 304 b.dictionary.LocationTable = []*v1experimental.Location{{ 305 MappingIndex: 0, 306 Address: 0x1e0, 307 }, { 308 MappingIndex: 0, 309 Address: 0x2f0, 310 }, { 311 MappingIndex: 0, 312 Address: 0x3f0, 313 }} 314 b.dictionary.StackTable = []*v1experimental.Stack{{ 315 LocationIndices: []int32{0, 1}, 316 }, { 317 LocationIndices: []int32{2}, 318 }} 319 b.profile.PeriodType = &v1experimental.ValueType{ 320 TypeStrindex: b.addstr("period_type"), 321 UnitStrindex: b.addstr("period_unit"), 322 } 323 b.profile.Period = 100 324 b.profile.Samples = []*v1experimental.Sample{{ 325 StackIndex: 0, 326 Values: []int64{0xef}, 327 }, { 328 StackIndex: 1, 329 Values: []int64{1, 2, 3, 4, 5, 6}, // should be rejected because of that 330 }} 331 return b 332 }, 333 }, 334 } 335 336 for _, td := range testdata { 337 td := td 338 339 t.Run(td.name, func(t *testing.T) { 340 svc := mockotlp.NewMockPushService(t) 341 var profiles []*model.PushRequest 342 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 343 c := (args.Get(1)).(*model.PushRequest) 344 profiles = append(profiles, c) 345 }).Return(nil, nil).Maybe() 346 b := td.profile() 347 b.profile.TimeUnixNano = 239 348 req := &v1experimental2.ExportProfilesServiceRequest{ 349 ResourceProfiles: []*v1experimental.ResourceProfiles{{ 350 ScopeProfiles: []*v1experimental.ScopeProfiles{{ 351 Profiles: []*v1experimental.Profile{ 352 &b.profile, 353 }}}}}, 354 Dictionary: &b.dictionary} 355 logger := test.NewTestingLogger(t) 356 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 357 _, err := h.Export(user.InjectOrgID(context.Background(), tenant.DefaultTenantID), req) 358 359 if td.expectedError == "" { 360 require.NoError(t, err) 361 require.Equal(t, 1, len(profiles)) 362 363 gp := profiles[0].Series[0].Profile.Profile 364 365 jsonStr, err := strprofile.Stringify(gp, strprofile.Options{}) 366 assert.NoError(t, err) 367 expectedJSON := readJSONFile(t, td.expectedJsonFile) 368 assert.JSONEq(t, expectedJSON, jsonStr) 369 } else { 370 require.Error(t, err) 371 require.True(t, strings.Contains(err.Error(), td.expectedError)) 372 } 373 }) 374 } 375 376 } 377 378 func TestSampleAttributes(t *testing.T) { 379 // Create a profile with two samples, with different sample attributes 380 // one process=firefox, the other process=chrome 381 // expect both of them to be present in the converted pprof as labels, but not series labels 382 svc := mockotlp.NewMockPushService(t) 383 var profiles []*model.PushRequest 384 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 385 c := (args.Get(1)).(*model.PushRequest) 386 profiles = append(profiles, c) 387 }).Return(nil, nil) 388 389 otlpb := new(otlpbuilder) 390 otlpb.dictionary.MappingTable = []*v1experimental.Mapping{{ 391 MemoryStart: 0x1000, 392 MemoryLimit: 0x1000, 393 FilenameStrindex: otlpb.addstr("firefox.so"), 394 }, { 395 MemoryStart: 0x1000, 396 MemoryLimit: 0x1000, 397 FilenameStrindex: otlpb.addstr("chrome.so"), 398 }} 399 400 otlpb.dictionary.LocationTable = []*v1experimental.Location{{ 401 MappingIndex: 0, 402 Address: 0x1e, 403 }, { 404 MappingIndex: 0, 405 Address: 0x2e, 406 }, { 407 MappingIndex: 1, 408 Address: 0x3e, 409 }, { 410 MappingIndex: 1, 411 Address: 0x4e, 412 }} 413 otlpb.dictionary.StackTable = []*v1experimental.Stack{{ 414 LocationIndices: []int32{0, 1}, 415 }, { 416 LocationIndices: []int32{2, 3}, 417 }} 418 otlpb.profile.Samples = []*v1experimental.Sample{{ 419 StackIndex: 0, 420 Values: []int64{0xef}, 421 AttributeIndices: []int32{0}, 422 }, { 423 StackIndex: 1, 424 Values: []int64{0xefef}, 425 AttributeIndices: []int32{1}, 426 }} 427 otlpb.dictionary.AttributeTable = []*v1experimental.KeyValueAndUnit{{ 428 KeyStrindex: otlpb.addstr("process"), 429 Value: &v1.AnyValue{ 430 Value: &v1.AnyValue_StringValue{ 431 StringValue: "firefox", 432 }, 433 }, 434 }, { 435 KeyStrindex: otlpb.addstr("process"), 436 Value: &v1.AnyValue{ 437 Value: &v1.AnyValue_StringValue{ 438 StringValue: "chrome", 439 }, 440 }, 441 }} 442 otlpb.profile.SampleType = &v1experimental.ValueType{ 443 TypeStrindex: otlpb.addstr("samples"), 444 UnitStrindex: otlpb.addstr("ms"), 445 } 446 otlpb.profile.TimeUnixNano = 239 447 req := &v1experimental2.ExportProfilesServiceRequest{ 448 ResourceProfiles: []*v1experimental.ResourceProfiles{{ 449 ScopeProfiles: []*v1experimental.ScopeProfiles{{ 450 Profiles: []*v1experimental.Profile{ 451 &otlpb.profile, 452 }}}}}, 453 Dictionary: &otlpb.dictionary} 454 logger := test.NewTestingLogger(t) 455 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 456 _, err := h.Export(user.InjectOrgID(context.Background(), tenant.DefaultTenantID), req) 457 assert.NoError(t, err) 458 require.Equal(t, 1, len(profiles)) 459 require.Equal(t, 1, len(profiles[0].Series)) 460 461 seriesLabelsMap := make(map[string]string) 462 for _, label := range profiles[0].Series[0].Labels { 463 seriesLabelsMap[label.Name] = label.Value 464 } 465 assert.Equal(t, "", seriesLabelsMap["process"]) 466 assert.NotContains(t, seriesLabelsMap, "service.name") 467 468 gp := profiles[0].Series[0].Profile.Profile 469 470 jsonStr, err := strprofile.Stringify(gp, strprofile.Options{}) 471 assert.NoError(t, err) 472 expectedJSON := readJSONFile(t, "testdata/TestSampleAttributes.json") 473 assert.Equal(t, expectedJSON, jsonStr) 474 475 } 476 477 func TestDifferentServiceNames(t *testing.T) { 478 // Create a profile with two samples having different service.name attributes 479 // Expect them to be pushed as separate profiles 480 svc := mockotlp.NewMockPushService(t) 481 var profiles []*model.PushRequest 482 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 483 c := (args.Get(1)).(*model.PushRequest) 484 for _, series := range c.Series { 485 sort.Sort(phlaremodel.Labels(series.Labels)) 486 } 487 profiles = append(profiles, c) 488 }).Return(nil, nil) 489 490 otlpb := new(otlpbuilder) 491 otlpb.dictionary.MappingTable = []*v1experimental.Mapping{{ 492 MemoryStart: 0x1000, 493 MemoryLimit: 0x2000, 494 FilenameStrindex: otlpb.addstr("service-a.so"), 495 }, { 496 MemoryStart: 0x2000, 497 MemoryLimit: 0x3000, 498 FilenameStrindex: otlpb.addstr("service-b.so"), 499 }, { 500 MemoryStart: 0x4000, 501 MemoryLimit: 0x5000, 502 FilenameStrindex: otlpb.addstr("service-c.so"), 503 }} 504 505 otlpb.dictionary.LocationTable = []*v1experimental.Location{{ 506 MappingIndex: 0, // service-a.so 507 Address: 0x1100, 508 Lines: []*v1experimental.Line{{ 509 FunctionIndex: 0, 510 Line: 10, 511 }}, 512 }, { 513 MappingIndex: 0, // service-a.so 514 Address: 0x1200, 515 Lines: []*v1experimental.Line{{ 516 FunctionIndex: 1, 517 Line: 20, 518 }}, 519 }, { 520 MappingIndex: 1, // service-b.so 521 Address: 0x2100, 522 Lines: []*v1experimental.Line{{ 523 FunctionIndex: 2, 524 Line: 30, 525 }}, 526 }, { 527 MappingIndex: 1, // service-b.so 528 Address: 0x2200, 529 Lines: []*v1experimental.Line{{ 530 FunctionIndex: 3, 531 Line: 40, 532 }}, 533 }, { 534 MappingIndex: 2, // service-c.so 535 Address: 0xef0, 536 Lines: []*v1experimental.Line{{ 537 FunctionIndex: 4, 538 Line: 50, 539 }}, 540 }} 541 542 otlpb.dictionary.FunctionTable = []*v1experimental.Function{{ 543 NameStrindex: otlpb.addstr("serviceA_func1"), 544 SystemNameStrindex: otlpb.addstr("serviceA_func1"), 545 FilenameStrindex: otlpb.addstr("service_a.go"), 546 }, { 547 NameStrindex: otlpb.addstr("serviceA_func2"), 548 SystemNameStrindex: otlpb.addstr("serviceA_func2"), 549 FilenameStrindex: otlpb.addstr("service_a.go"), 550 }, { 551 NameStrindex: otlpb.addstr("serviceB_func1"), 552 SystemNameStrindex: otlpb.addstr("serviceB_func1"), 553 FilenameStrindex: otlpb.addstr("service_b.go"), 554 }, { 555 NameStrindex: otlpb.addstr("serviceB_func2"), 556 SystemNameStrindex: otlpb.addstr("serviceB_func2"), 557 FilenameStrindex: otlpb.addstr("service_b.go"), 558 }, { 559 NameStrindex: otlpb.addstr("serviceC_func3"), 560 SystemNameStrindex: otlpb.addstr("serviceC_func3"), 561 FilenameStrindex: otlpb.addstr("service_c.go"), 562 }} 563 564 otlpb.dictionary.StackTable = []*v1experimental.Stack{{ 565 LocationIndices: []int32{0, 1}, // Use first two locations 566 }, { 567 LocationIndices: []int32{2, 3}, 568 }, { 569 LocationIndices: []int32{4, 4}, 570 }} 571 572 otlpb.profile.Samples = []*v1experimental.Sample{{ 573 StackIndex: 0, 574 Values: []int64{100}, 575 AttributeIndices: []int32{0}, 576 }, { 577 StackIndex: 1, 578 Values: []int64{200}, 579 AttributeIndices: []int32{1}, 580 }, { 581 StackIndex: 2, 582 Values: []int64{700}, 583 AttributeIndices: []int32{}, 584 }} 585 586 otlpb.dictionary.AttributeTable = []*v1experimental.KeyValueAndUnit{{ 587 KeyStrindex: otlpb.addstr("service.name"), 588 Value: &v1.AnyValue{ 589 Value: &v1.AnyValue_StringValue{ 590 StringValue: "service-a", 591 }, 592 }, 593 }, { 594 KeyStrindex: otlpb.addstr("service.name"), 595 Value: &v1.AnyValue{ 596 Value: &v1.AnyValue_StringValue{ 597 StringValue: "service-b", 598 }, 599 }, 600 }} 601 602 otlpb.profile.SampleType = &v1experimental.ValueType{ 603 TypeStrindex: otlpb.addstr("samples"), 604 UnitStrindex: otlpb.addstr("count"), 605 } 606 otlpb.profile.PeriodType = &v1experimental.ValueType{ 607 TypeStrindex: otlpb.addstr("cpu"), 608 UnitStrindex: otlpb.addstr("nanoseconds"), 609 } 610 otlpb.profile.Period = 10000000 // 10ms 611 otlpb.profile.TimeUnixNano = 239 612 req := &v1experimental2.ExportProfilesServiceRequest{ 613 ResourceProfiles: []*v1experimental.ResourceProfiles{{ 614 ScopeProfiles: []*v1experimental.ScopeProfiles{{ 615 Profiles: []*v1experimental.Profile{ 616 &otlpb.profile, 617 }}}}}, 618 Dictionary: &otlpb.dictionary} 619 620 logger := test.NewTestingLogger(t) 621 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 622 _, err := h.Export(user.InjectOrgID(context.Background(), tenant.DefaultTenantID), req) 623 require.NoError(t, err) 624 625 require.Equal(t, 1, len(profiles)) 626 require.Equal(t, 3, len(profiles[0].Series)) 627 628 expectedProfiles := map[string]string{ 629 "{__delta__=\"false\", __name__=\"process_cpu\", __otel__=\"true\", service_name=\"service-a\"}": "testdata/TestDifferentServiceNames_service_a_profile.json", 630 "{__delta__=\"false\", __name__=\"process_cpu\", __otel__=\"true\", service_name=\"service-b\"}": "testdata/TestDifferentServiceNames_service_b_profile.json", 631 "{__delta__=\"false\", __name__=\"process_cpu\", __otel__=\"true\", service_name=\"unknown_service\"}": "testdata/TestDifferentServiceNames_unknown_profile.json", 632 } 633 634 for _, s := range profiles[0].Series { 635 series := phlaremodel.Labels(s.Labels).ToPrometheusLabels().String() 636 assert.Contains(t, expectedProfiles, series) 637 expectedJsonPath := expectedProfiles[series] 638 expectedJson := readJSONFile(t, expectedJsonPath) 639 640 gp := s.Profile.Profile 641 642 require.Equal(t, 1, len(gp.SampleType)) 643 assert.Equal(t, "cpu", gp.StringTable[gp.SampleType[0].Type]) 644 assert.Equal(t, "nanoseconds", gp.StringTable[gp.SampleType[0].Unit]) 645 646 require.NotNil(t, gp.PeriodType) 647 assert.Equal(t, "cpu", gp.StringTable[gp.PeriodType.Type]) 648 assert.Equal(t, "nanoseconds", gp.StringTable[gp.PeriodType.Unit]) 649 assert.Equal(t, int64(10000000), gp.Period) 650 651 jsonStr, err := strprofile.Stringify(gp, strprofile.Options{}) 652 assert.NoError(t, err) 653 assert.JSONEq(t, expectedJson, jsonStr) 654 assert.NotContains(t, jsonStr, "service.name") 655 656 } 657 } 658 659 type otlpbuilder struct { 660 profile v1experimental.Profile 661 dictionary v1experimental.ProfilesDictionary 662 stringmap map[string]int32 663 } 664 665 func (o *otlpbuilder) addstr(s string) int32 { 666 if o.stringmap == nil { 667 o.stringmap = make(map[string]int32) 668 } 669 if idx, ok := o.stringmap[s]; ok { 670 return idx 671 } 672 idx := int32(len(o.stringmap)) 673 o.stringmap[s] = idx 674 o.dictionary.StringTable = append(o.dictionary.StringTable, s) 675 return idx 676 } 677 678 func testConfig() server.Config { 679 cfg := server.Config{} 680 fs := flag.NewFlagSet("test", flag.PanicOnError) 681 cfg.RegisterFlags(fs) 682 return cfg 683 } 684 685 func defaultLimits() validation.MockLimits { 686 return validation.MockLimits{ 687 IngestionBodyLimitBytesValue: 1024 * 1024 * 1024, // 1GB 688 } 689 } 690 691 // createValidOTLPRequest creates a minimal valid OTLP profile export request for testing 692 func createValidOTLPRequest() *v1experimental2.ExportProfilesServiceRequest { 693 b := new(otlpbuilder) 694 b.dictionary.MappingTable = []*v1experimental.Mapping{{ 695 MemoryStart: 0x1000, 696 MemoryLimit: 0x2000, 697 FilenameStrindex: b.addstr("test.so"), 698 }} 699 b.dictionary.LocationTable = []*v1experimental.Location{{ 700 MappingIndex: 0, 701 Address: 0x1100, 702 }} 703 b.dictionary.StackTable = []*v1experimental.Stack{{ 704 LocationIndices: []int32{0}, 705 }} 706 b.profile.SampleType = &v1experimental.ValueType{ 707 TypeStrindex: b.addstr("samples"), 708 UnitStrindex: b.addstr("count"), 709 } 710 b.profile.Samples = []*v1experimental.Sample{{ 711 StackIndex: 0, 712 Values: []int64{100}, 713 }} 714 b.profile.TimeUnixNano = 1234567890 715 716 return &v1experimental2.ExportProfilesServiceRequest{ 717 ResourceProfiles: []*v1experimental.ResourceProfiles{{ 718 ScopeProfiles: []*v1experimental.ScopeProfiles{{ 719 Profiles: []*v1experimental.Profile{&b.profile}, 720 }}, 721 }}, 722 Dictionary: &b.dictionary, 723 } 724 } 725 726 func TestHTTPRequestWithJSONAndTenantAccepted(t *testing.T) { 727 svc := mockotlp.NewMockPushService(t) 728 var capturedTenantID string 729 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 730 ctx := args.Get(0).(context.Context) 731 tenantID, err := tenant.ExtractTenantIDFromContext(ctx) 732 require.NoError(t, err) 733 capturedTenantID = tenantID 734 }).Return(nil, nil) 735 736 logger := test.NewTestingLogger(t) 737 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 738 739 jsonRequest := `{ 740 "resourceProfiles": [{ 741 "scopeProfiles": [{ 742 "profiles": [{ 743 "sampleType": {"typeStrindex": 0, "unitStrindex": 1}, 744 "samples": [{"stackIndex": 0, "values": [100]}], 745 "timeUnixNano": "1234567890" 746 }] 747 }] 748 }], 749 "dictionary": { 750 "stringTable": ["samples", "count", "test.so"], 751 "mappingTable": [{"memoryStart": "4096", "memoryLimit": "8192", "filenameStrindex": 2}], 752 "locationTable": [{"mappingIndex": 0, "address": "4352"}], 753 "stackTable": [{"locationIndices": [0]}] 754 } 755 }` 756 757 httpReq := httptest.NewRequest("POST", "/otlp/v1/profiles", bytes.NewReader([]byte(jsonRequest))) 758 httpReq.Header.Set("Content-Type", "application/json") 759 httpReq.Header.Set(user.OrgIDHeaderName, "json-tenant") 760 761 w := httptest.NewRecorder() 762 util.AuthenticateUser(true).Wrap(h).ServeHTTP(w, httpReq) 763 764 assert.Equal(t, http.StatusOK, w.Code) 765 assert.Equal(t, "json-tenant", capturedTenantID) 766 } 767 768 func TestHTTPRequestWithGzipCompression(t *testing.T) { 769 svc := mockotlp.NewMockPushService(t) 770 var capturedTenantID string 771 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 772 ctx := args.Get(0).(context.Context) 773 tenantID, err := tenant.ExtractTenantIDFromContext(ctx) 774 require.NoError(t, err) 775 capturedTenantID = tenantID 776 }).Return(nil, nil) 777 778 logger := test.NewTestingLogger(t) 779 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 780 781 req := createValidOTLPRequest() 782 reqBytes, err := proto.Marshal(req) 783 require.NoError(t, err) 784 785 var gzipBuf bytes.Buffer 786 gzipWriter := gzip.NewWriter(&gzipBuf) 787 _, err = gzipWriter.Write(reqBytes) 788 require.NoError(t, err) 789 err = gzipWriter.Close() 790 require.NoError(t, err) 791 792 httpReq := httptest.NewRequest("POST", "/otlp/v1/profiles", bytes.NewReader(gzipBuf.Bytes())) 793 httpReq.Header.Set("Content-Type", "application/x-protobuf") 794 httpReq.Header.Set("Content-Encoding", "gzip") 795 796 w := httptest.NewRecorder() 797 util.AuthenticateUser(false).Wrap(h).ServeHTTP(w, httpReq) 798 799 assert.Equal(t, http.StatusOK, w.Code) 800 assert.Equal(t, tenant.DefaultTenantID, capturedTenantID) 801 } 802 803 func TestHTTPRequestWithGzipCompressionAndJSON(t *testing.T) { 804 svc := mockotlp.NewMockPushService(t) 805 var capturedTenantID string 806 svc.On("PushBatch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 807 ctx := args.Get(0).(context.Context) 808 tenantID, err := tenant.ExtractTenantIDFromContext(ctx) 809 require.NoError(t, err) 810 capturedTenantID = tenantID 811 }).Return(nil, nil) 812 813 logger := test.NewTestingLogger(t) 814 h := NewOTLPIngestHandler(testConfig(), svc, logger, defaultLimits()) 815 816 jsonRequest := `{ 817 "resourceProfiles": [{ 818 "scopeProfiles": [{ 819 "profiles": [{ 820 "sampleType": {"typeStrindex": 0, "unitStrindex": 1}, 821 "samples": [{"stackIndex": 0, "values": [100]}], 822 "timeUnixNano": "1234567890" 823 }] 824 }] 825 }], 826 "dictionary": { 827 "stringTable": ["samples", "count", "test.so"], 828 "mappingTable": [{"memoryStart": "4096", "memoryLimit": "8192", "filenameStrindex": 2}], 829 "locationTable": [{"mappingIndex": 0, "address": "4352"}], 830 "stackTable": [{"locationIndices": [0]}] 831 } 832 }` 833 834 var gzipBuf bytes.Buffer 835 gzipWriter := gzip.NewWriter(&gzipBuf) 836 _, err := gzipWriter.Write([]byte(jsonRequest)) 837 require.NoError(t, err) 838 err = gzipWriter.Close() 839 require.NoError(t, err) 840 841 httpReq := httptest.NewRequest("POST", "/otlp/v1/profiles", bytes.NewReader(gzipBuf.Bytes())) 842 httpReq.Header.Set("Content-Type", "application/json") 843 httpReq.Header.Set("Content-Encoding", "gzip") 844 httpReq.Header.Set(user.OrgIDHeaderName, "gzip-json-tenant") 845 846 w := httptest.NewRecorder() 847 util.AuthenticateUser(true).Wrap(h).ServeHTTP(w, httpReq) 848 849 assert.Equal(t, http.StatusOK, w.Code) 850 assert.Equal(t, "gzip-json-tenant", capturedTenantID) 851 }