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  }