github.com/grafana/pyroscope@v1.18.0/pkg/test/integration/ingest_otlp_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/gogo/status"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	profilesv1 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development"
    16  	commonv1 "go.opentelemetry.io/proto/otlp/common/v1"
    17  	"google.golang.org/grpc/codes"
    18  	"google.golang.org/protobuf/proto"
    19  
    20  	"github.com/grafana/pyroscope/pkg/og/convert/pprof/strprofile"
    21  )
    22  
    23  type otlpTestData struct {
    24  	name             string
    25  	profilePath      string
    26  	expectedProfiles []expectedProfile
    27  	assertMetrics    func(t *testing.T, p *PyroscopeTest)
    28  }
    29  
    30  type expectedProfile struct {
    31  	metricName       string
    32  	query            map[string]string
    33  	expectedJsonPath string
    34  }
    35  
    36  var otlpTestDatas = []otlpTestData{
    37  	{
    38  		name:        "unsymbolized profile from otel-ebpf-profiler with cpu and offcpu",
    39  		profilePath: "testdata/otel-ebpf-profile.pb.bin",
    40  		expectedProfiles: []expectedProfile{
    41  			{
    42  				"process_cpu:cpu:nanoseconds:cpu:nanoseconds",
    43  				map[string]string{"service_name": "unknown_service"},
    44  				"testdata/otel-ebpf-profile.out.json",
    45  			},
    46  		},
    47  		assertMetrics: func(t *testing.T, p *PyroscopeTest) {
    48  
    49  		},
    50  	},
    51  }
    52  
    53  func TestIngestOTLP(t *testing.T) {
    54  	for _, td := range otlpTestDatas {
    55  		t.Run(td.name, func(t *testing.T) {
    56  			EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
    57  				rb := p.NewRequestBuilder(t)
    58  				runNo := p.TempAppName()
    59  
    60  				profileBytes, err := os.ReadFile(td.profilePath)
    61  				require.NoError(t, err)
    62  				var profile = new(profilesv1.ExportProfilesServiceRequest)
    63  				err = proto.Unmarshal(profileBytes, profile)
    64  				require.NoError(t, err)
    65  
    66  				for _, rp := range profile.ResourceProfiles {
    67  					for _, sp := range rp.ScopeProfiles {
    68  						sp.Scope.Attributes = append(sp.Scope.Attributes, &commonv1.KeyValue{
    69  							Key: "test_run_no", Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: runNo}},
    70  						})
    71  					}
    72  				}
    73  
    74  				client := rb.OtelPushClient()
    75  				_, err = client.Export(context.Background(), profile)
    76  				require.NoError(t, err)
    77  
    78  				for _, metric := range td.expectedProfiles {
    79  
    80  					query := make(map[string]string)
    81  					for k, v := range metric.query {
    82  						query[k] = v
    83  					}
    84  					query["test_run_no"] = runNo
    85  
    86  					resp := rb.SelectMergeProfile(metric.metricName, query)
    87  
    88  					assert.NotEmpty(t, resp.Msg.Sample)
    89  					assert.NotEmpty(t, resp.Msg.Function)
    90  					assert.NotEmpty(t, resp.Msg.Mapping)
    91  					assert.NotEmpty(t, resp.Msg.Location)
    92  
    93  					actual := strprofile.ToCompactProfile(resp.Msg, strprofile.Options{
    94  						NoTime:     true,
    95  						NoDuration: true,
    96  					})
    97  					strprofile.SortProfileSamples(actual)
    98  					actualBytes, err := json.Marshal(actual)
    99  					assert.NoError(t, err)
   100  
   101  					pprofDumpFileName := strings.ReplaceAll(metric.expectedJsonPath, ".json", ".pprof.pb.bin") // for debugging
   102  					pprof, err := resp.Msg.MarshalVT()
   103  					assert.NoError(t, err)
   104  					err = os.WriteFile(pprofDumpFileName, pprof, 0644)
   105  					assert.NoError(t, err)
   106  
   107  					expectedBytes, err := os.ReadFile(metric.expectedJsonPath)
   108  					require.NoError(t, err)
   109  					var expected strprofile.CompactProfile
   110  					assert.NoError(t, json.Unmarshal(expectedBytes, &expected))
   111  					strprofile.SortProfileSamples(expected)
   112  					expectedBytes, err = json.Marshal(expected)
   113  					require.NoError(t, err)
   114  
   115  					assert.Equal(t, string(expectedBytes), string(actualBytes))
   116  				}
   117  				td.assertMetrics(t, p)
   118  			})
   119  		})
   120  	}
   121  }
   122  
   123  type badOtlpTestData struct {
   124  	name                 string
   125  	profilePath          string
   126  	expectedErrorMessage string
   127  }
   128  
   129  var badOtlpTestDatas = []badOtlpTestData{
   130  	{
   131  		name:                 "corrupted data (function idx out of bounds)",
   132  		profilePath:          "testdata/otel-ebpf-profile-corrupted.pb.bin",
   133  		expectedErrorMessage: "failed to convert otel profile: invalid stack index: 1000000000",
   134  	},
   135  }
   136  
   137  func TestIngestBadOTLP(t *testing.T) {
   138  	for _, td := range badOtlpTestDatas {
   139  		t.Run(td.name, func(t *testing.T) {
   140  			EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   141  				rb := p.NewRequestBuilder(t)
   142  				profileBytes, err := os.ReadFile(td.profilePath)
   143  				require.NoError(t, err)
   144  				var profile = new(profilesv1.ExportProfilesServiceRequest)
   145  				err = proto.Unmarshal(profileBytes, profile)
   146  				require.NoError(t, err)
   147  
   148  				client := rb.OtelPushClient()
   149  				_, err = client.Export(context.Background(), profile)
   150  				require.Error(t, err)
   151  				require.Equal(t, codes.InvalidArgument, status.Code(err))
   152  				if td.expectedErrorMessage != "" {
   153  					require.Contains(t, err.Error(), td.expectedErrorMessage)
   154  				}
   155  			})
   156  		})
   157  	}
   158  }
   159  
   160  // TestIngestOTLPHTTPBinary tests OTLP ingestion via HTTP with binary/protobuf content type
   161  func TestIngestOTLPHTTPBinary(t *testing.T) {
   162  	for _, td := range otlpTestDatas {
   163  		t.Run(td.name, func(t *testing.T) {
   164  			EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   165  				rb := p.NewRequestBuilder(t)
   166  				runNo := p.TempAppName()
   167  
   168  				profileBytes, err := os.ReadFile(td.profilePath)
   169  				require.NoError(t, err)
   170  				var profile = new(profilesv1.ExportProfilesServiceRequest)
   171  				err = proto.Unmarshal(profileBytes, profile)
   172  				require.NoError(t, err)
   173  
   174  				for _, rp := range profile.ResourceProfiles {
   175  					for _, sp := range rp.ScopeProfiles {
   176  						sp.Scope.Attributes = append(sp.Scope.Attributes, &commonv1.KeyValue{
   177  							Key: "test_run_no", Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: runNo}},
   178  						})
   179  					}
   180  				}
   181  
   182  				// Send via HTTP with application/x-protobuf content type
   183  				req := rb.OtelPushHTTPProtobuf(profile)
   184  				resp, err := http.DefaultClient.Do(req)
   185  				require.NoError(t, err)
   186  				defer resp.Body.Close()
   187  
   188  				body, err := io.ReadAll(resp.Body)
   189  				require.NoError(t, err)
   190  				require.Equal(t, http.StatusOK, resp.StatusCode, "Response body: %s", string(body))
   191  
   192  				for _, metric := range td.expectedProfiles {
   193  					query := make(map[string]string)
   194  					for k, v := range metric.query {
   195  						query[k] = v
   196  					}
   197  					query["test_run_no"] = runNo
   198  
   199  					resp := rb.SelectMergeProfile(metric.metricName, query)
   200  
   201  					assert.NotEmpty(t, resp.Msg.Sample)
   202  					assert.NotEmpty(t, resp.Msg.Function)
   203  					assert.NotEmpty(t, resp.Msg.Mapping)
   204  					assert.NotEmpty(t, resp.Msg.Location)
   205  
   206  					actual := strprofile.ToCompactProfile(resp.Msg, strprofile.Options{
   207  						NoTime:     true,
   208  						NoDuration: true,
   209  					})
   210  					strprofile.SortProfileSamples(actual)
   211  					actualBytes, err := json.Marshal(actual)
   212  					assert.NoError(t, err)
   213  
   214  					expectedBytes, err := os.ReadFile(metric.expectedJsonPath)
   215  					require.NoError(t, err)
   216  					var expected strprofile.CompactProfile
   217  					assert.NoError(t, json.Unmarshal(expectedBytes, &expected))
   218  					strprofile.SortProfileSamples(expected)
   219  					expectedBytes, err = json.Marshal(expected)
   220  					require.NoError(t, err)
   221  
   222  					assert.Equal(t, string(expectedBytes), string(actualBytes))
   223  				}
   224  				td.assertMetrics(t, p)
   225  			})
   226  		})
   227  	}
   228  }
   229  
   230  // TestIngestOTLPHTTPJSON tests OTLP ingestion via HTTP with JSON content type
   231  func TestIngestOTLPHTTPJSON(t *testing.T) {
   232  	for _, td := range otlpTestDatas {
   233  		t.Run(td.name, func(t *testing.T) {
   234  			EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   235  				rb := p.NewRequestBuilder(t)
   236  				runNo := p.TempAppName()
   237  
   238  				profileBytes, err := os.ReadFile(td.profilePath)
   239  				require.NoError(t, err)
   240  				var profile = new(profilesv1.ExportProfilesServiceRequest)
   241  				err = proto.Unmarshal(profileBytes, profile)
   242  				require.NoError(t, err)
   243  
   244  				for _, rp := range profile.ResourceProfiles {
   245  					for _, sp := range rp.ScopeProfiles {
   246  						sp.Scope.Attributes = append(sp.Scope.Attributes, &commonv1.KeyValue{
   247  							Key: "test_run_no", Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: runNo}},
   248  						})
   249  					}
   250  				}
   251  
   252  				// Send via HTTP with application/json content type
   253  				req := rb.OtelPushHTTPJSON(profile)
   254  				resp, err := http.DefaultClient.Do(req)
   255  				require.NoError(t, err)
   256  				defer resp.Body.Close()
   257  
   258  				body, err := io.ReadAll(resp.Body)
   259  				require.NoError(t, err)
   260  				require.Equal(t, http.StatusOK, resp.StatusCode, "Response body: %s", string(body))
   261  
   262  				for _, metric := range td.expectedProfiles {
   263  					query := make(map[string]string)
   264  					for k, v := range metric.query {
   265  						query[k] = v
   266  					}
   267  					query["test_run_no"] = runNo
   268  
   269  					resp := rb.SelectMergeProfile(metric.metricName, query)
   270  
   271  					assert.NotEmpty(t, resp.Msg.Sample)
   272  					assert.NotEmpty(t, resp.Msg.Function)
   273  					assert.NotEmpty(t, resp.Msg.Mapping)
   274  					assert.NotEmpty(t, resp.Msg.Location)
   275  
   276  					actual := strprofile.ToCompactProfile(resp.Msg, strprofile.Options{
   277  						NoTime:     true,
   278  						NoDuration: true,
   279  					})
   280  					strprofile.SortProfileSamples(actual)
   281  					actualBytes, err := json.Marshal(actual)
   282  					assert.NoError(t, err)
   283  
   284  					expectedBytes, err := os.ReadFile(metric.expectedJsonPath)
   285  					require.NoError(t, err)
   286  					var expected strprofile.CompactProfile
   287  					assert.NoError(t, json.Unmarshal(expectedBytes, &expected))
   288  					strprofile.SortProfileSamples(expected)
   289  					expectedBytes, err = json.Marshal(expected)
   290  					require.NoError(t, err)
   291  
   292  					assert.Equal(t, string(expectedBytes), string(actualBytes))
   293  				}
   294  				td.assertMetrics(t, p)
   295  			})
   296  		})
   297  	}
   298  }