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 }