github.com/grafana/pyroscope@v1.18.0/pkg/test/integration/helper.go (about) 1 package integration 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "flag" 9 "fmt" 10 "io" 11 "math/rand" 12 "mime/multipart" 13 "net/http" 14 "os" 15 "strings" 16 "sync" 17 "testing" 18 "time" 19 20 "connectrpc.com/connect" 21 "github.com/prometheus/client_golang/prometheus" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 25 "google.golang.org/grpc" 26 "google.golang.org/grpc/credentials/insecure" 27 "google.golang.org/protobuf/encoding/protojson" 28 "google.golang.org/protobuf/proto" 29 30 "github.com/prometheus/common/expfmt" 31 32 profilesv1 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development" 33 34 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 35 pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1" 36 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1/pushv1connect" 37 querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 38 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1/querierv1connect" 39 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 40 connectapi "github.com/grafana/pyroscope/pkg/api/connect" 41 "github.com/grafana/pyroscope/pkg/cfg" 42 "github.com/grafana/pyroscope/pkg/og/structs/flamebearer" 43 "github.com/grafana/pyroscope/pkg/pprof" 44 "github.com/grafana/pyroscope/pkg/pyroscope" 45 "github.com/grafana/pyroscope/pkg/util/connectgrpc" 46 ) 47 48 func EachPyroscopeTest(t *testing.T, f func(p *PyroscopeTest, t *testing.T)) { 49 tests := []struct { 50 name string 51 f func(t *testing.T) *PyroscopeTest 52 }{ 53 { 54 "v1", 55 func(t *testing.T) *PyroscopeTest { 56 return new(PyroscopeTest).Configure(t, false) 57 }, 58 }, 59 { 60 "v2", 61 func(t *testing.T) *PyroscopeTest { 62 return new(PyroscopeTest).Configure(t, true) 63 }, 64 }, 65 } 66 for _, pt := range tests { 67 t.Run(pt.name, func(t *testing.T) { 68 p := pt.f(t) 69 p.start(t) 70 t.Cleanup(func() { 71 p.stop() 72 }) 73 f(p, t) 74 }) 75 } 76 } 77 78 type PyroscopeTest struct { 79 config pyroscope.Config 80 it *pyroscope.Pyroscope 81 wg sync.WaitGroup 82 prevReg prometheus.Registerer 83 reg *prometheus.Registry 84 httpPort int 85 memberlistPort int 86 grpcPort int 87 raftPort int 88 } 89 90 const address = "127.0.0.1" 91 const storeInMemory = "inmemory" 92 93 func (p *PyroscopeTest) start(t *testing.T) { 94 var err error 95 96 p.it, err = pyroscope.New(p.config) 97 98 require.NoError(t, err) 99 100 p.wg.Add(1) 101 go func() { 102 defer p.wg.Done() 103 err := p.it.Run() 104 require.NoError(t, err) 105 }() 106 require.Eventually(t, func() bool { 107 return p.ringActive() && p.ready() 108 }, 30*time.Second, 100*time.Millisecond) 109 } 110 111 func (p *PyroscopeTest) Configure(t *testing.T, v2 bool) *PyroscopeTest { 112 ports, err := GetFreePorts(4) 113 require.NoError(t, err) 114 p.httpPort = ports[0] 115 p.memberlistPort = ports[1] 116 p.grpcPort = ports[2] 117 p.raftPort = ports[3] 118 t.Logf("ports: http %d memberlist %d grpc %d raft %d", p.httpPort, p.memberlistPort, p.grpcPort, p.raftPort) 119 120 p.prevReg = prometheus.DefaultRegisterer 121 p.reg = prometheus.NewRegistry() 122 prometheus.DefaultRegisterer = p.reg 123 124 p.config.V2 = v2 125 err = cfg.DynamicUnmarshal(&p.config, []string{"pyroscope"}, flag.NewFlagSet("pyroscope", flag.ContinueOnError)) 126 require.NoError(t, err) 127 128 // set addresses and ports 129 p.config.Server.HTTPListenAddress = address 130 p.config.Server.HTTPListenPort = p.httpPort 131 p.config.Server.GRPCListenAddress = address 132 p.config.Server.GRPCListenPort = p.grpcPort 133 p.config.Worker.SchedulerAddress = address 134 p.config.MemberlistKV.AdvertisePort = p.memberlistPort 135 p.config.MemberlistKV.TCPTransport.BindPort = p.memberlistPort 136 p.config.Ingester.LifecyclerConfig.Addr = address 137 p.config.Ingester.LifecyclerConfig.MinReadyDuration = 0 138 p.config.QueryScheduler.ServiceDiscovery.SchedulerRing.InstanceAddr = address 139 p.config.Frontend.Addr = address 140 141 // heartbeat more often 142 p.config.Distributor.DistributorRing.HeartbeatPeriod = time.Second 143 p.config.Ingester.LifecyclerConfig.HeartbeatPeriod = time.Second 144 p.config.OverridesExporter.Ring.Ring.HeartbeatPeriod = time.Second 145 p.config.QueryScheduler.ServiceDiscovery.SchedulerRing.HeartbeatPeriod = time.Second 146 147 // do not use memberlist 148 p.config.Distributor.DistributorRing.KVStore.Store = storeInMemory 149 p.config.Ingester.LifecyclerConfig.RingConfig.KVStore.Store = storeInMemory 150 p.config.OverridesExporter.Ring.Ring.KVStore.Store = storeInMemory 151 p.config.QueryScheduler.ServiceDiscovery.SchedulerRing.KVStore.Store = storeInMemory 152 153 p.config.SelfProfiling.DisablePush = true 154 p.config.Analytics.Enabled = false // usage-stats terminating slow as hell 155 p.config.LimitsConfig.MaxQueryLength = 0 156 p.config.LimitsConfig.MaxQueryLookback = 0 157 p.config.LimitsConfig.RejectOlderThan = 0 158 _ = p.config.Server.LogLevel.Set("debug") 159 160 if v2 { 161 p.config.Storage.Bucket.Filesystem.Directory = t.TempDir() 162 p.config.Storage.Bucket.Backend = "filesystem" 163 p.config.LimitsConfig.WritePathOverrides.WritePath = "segment-writer" 164 p.config.LimitsConfig.ReadPathOverrides.EnableQueryBackend = true 165 p.config.SegmentWriter.LifecyclerConfig.MinReadyDuration = 0 * time.Second 166 p.config.SegmentWriter.LifecyclerConfig.Addr = address 167 p.config.SegmentWriter.MetadataUpdateTimeout = 0 * time.Second 168 p.config.Metastore.MinReadyDuration = 0 * time.Second 169 p.config.QueryBackend.Address = fmt.Sprintf("%s:%d", address, p.grpcPort) 170 p.config.Metastore.Address = fmt.Sprintf("%s:%d", address, p.grpcPort) 171 p.config.Metastore.Raft.ServerID = fmt.Sprintf("%s:%d", address, p.raftPort) 172 p.config.Metastore.Raft.BindAddress = fmt.Sprintf("%s:%d", address, p.raftPort) 173 p.config.Metastore.Raft.AdvertiseAddress = fmt.Sprintf("%s:%d", address, p.raftPort) 174 p.config.Metastore.Raft.Dir = t.TempDir() 175 p.config.Metastore.Raft.SnapshotsDir = t.TempDir() 176 p.config.Metastore.FSM.DataDir = t.TempDir() 177 } 178 return p 179 } 180 181 func (p *PyroscopeTest) stop() { 182 defer func() { 183 prometheus.DefaultRegisterer = p.prevReg 184 }() 185 p.it.SignalHandler.Stop() 186 p.wg.Wait() 187 } 188 189 func (p *PyroscopeTest) ready() bool { 190 return httpBodyContains(p.URL()+"/ready", "ready") 191 } 192 func (p *PyroscopeTest) ringActive() bool { 193 return httpBodyContains(p.URL()+"/ring", "ACTIVE") 194 } 195 func (p *PyroscopeTest) URL() string { 196 return fmt.Sprintf("http://%s:%d", address, p.httpPort) 197 } 198 199 func (p *PyroscopeTest) Metrics(t testing.TB, keep func(string) bool) string { 200 dto, err := p.reg.Gather() 201 require.NoError(t, err) 202 gotBuf := bytes.NewBuffer(nil) 203 enc := expfmt.NewEncoder(gotBuf, expfmt.NewFormat(expfmt.TypeTextPlain)) 204 for _, mf := range dto { 205 if err := enc.Encode(mf); err != nil { 206 require.NoError(t, err) 207 } 208 } 209 split := strings.Split(gotBuf.String(), "\n") 210 res := []string{} 211 for _, line := range split { 212 if keep(line) { 213 res = append(res, line) 214 } 215 } 216 return strings.Join(res, "\n") 217 } 218 219 func httpBodyContains(url string, needle string) bool { 220 fmt.Println("httpBodyContains", url, needle) 221 res, err := http.Get(url) 222 if err != nil { 223 return false 224 } 225 if res.StatusCode != 200 || res.Body == nil { 226 return false 227 } 228 body := bytes.NewBuffer(nil) 229 _, err = io.Copy(body, res.Body) 230 if err != nil { 231 return false 232 } 233 234 return strings.Contains(body.String(), needle) 235 } 236 237 func (p *PyroscopeTest) NewRequestBuilder(t *testing.T) *RequestBuilder { 238 return &RequestBuilder{ 239 t: t, 240 url: p.URL(), 241 AppName: p.TempAppName(), 242 spy: "foo239", 243 } 244 } 245 246 func (p *PyroscopeTest) TempAppName() string { 247 return fmt.Sprintf("pprof-integration-%d", 248 rand.Uint64()) 249 } 250 251 func createRenderQuery(metric, app string) string { 252 return metric + "{service_name=\"" + app + "\"}" 253 } 254 255 type RequestBuilder struct { 256 AppName string 257 url string 258 spy string 259 t *testing.T 260 } 261 262 func (b *RequestBuilder) Spy(spy string) *RequestBuilder { 263 b.spy = spy 264 return b 265 } 266 267 func (b *RequestBuilder) IngestPPROFRequest(profilePath, prevProfilePath, sampleTypeConfigPath string) *http.Request { 268 var ( 269 profile, prevProfile, sampleTypeConfig []byte 270 err error 271 ) 272 profile, err = os.ReadFile(profilePath) 273 assert.NoError(b.t, err) 274 if prevProfilePath != "" { 275 prevProfile, err = os.ReadFile(prevProfilePath) 276 assert.NoError(b.t, err) 277 } 278 if sampleTypeConfigPath != "" { 279 sampleTypeConfig, err = os.ReadFile(sampleTypeConfigPath) 280 assert.NoError(b.t, err) 281 } 282 283 const ( 284 formFieldProfile = "profile" 285 formFieldPreviousProfile = "prev_profile" 286 formFieldSampleTypeConfig = "sample_type_config" 287 ) 288 289 var bb bytes.Buffer 290 w := multipart.NewWriter(&bb) 291 292 profileW, err := w.CreateFormFile(formFieldProfile, "not used") 293 require.NoError(b.t, err) 294 _, err = profileW.Write(profile) 295 require.NoError(b.t, err) 296 297 if sampleTypeConfig != nil { 298 299 sampleTypeConfigW, err := w.CreateFormFile(formFieldSampleTypeConfig, "not used") 300 require.NoError(b.t, err) 301 _, err = sampleTypeConfigW.Write(sampleTypeConfig) 302 require.NoError(b.t, err) 303 } 304 305 if prevProfile != nil { 306 prevProfileW, err := w.CreateFormFile(formFieldPreviousProfile, "not used") 307 require.NoError(b.t, err) 308 _, err = prevProfileW.Write(prevProfile) 309 require.NoError(b.t, err) 310 } 311 err = w.Close() 312 require.NoError(b.t, err) 313 314 bs := bb.Bytes() 315 ct := w.FormDataContentType() 316 317 url := b.url + "/ingest?name=" + b.AppName + "&spyName=" + b.spy 318 req, err := http.NewRequest("POST", url, bytes.NewReader(bs)) 319 require.NoError(b.t, err) 320 req.Header.Set("Content-Type", ct) 321 return req 322 } 323 324 func (b *RequestBuilder) IngestJFRRequestFiles(jfrPath, labelsPath string) *http.Request { 325 var ( 326 jfr, labels []byte 327 err error 328 ) 329 jfr, err = os.ReadFile(jfrPath) 330 assert.NoError(b.t, err) 331 if labelsPath != "" { 332 labels, err = os.ReadFile(labelsPath) 333 assert.NoError(b.t, err) 334 } 335 336 return b.IngestJFRRequestBody(jfr, labels) 337 } 338 339 func (b *RequestBuilder) IngestJFRRequestBody(jfr []byte, labels []byte) *http.Request { 340 var bb bytes.Buffer 341 w := multipart.NewWriter(&bb) 342 jfrw, err := w.CreateFormFile("jfr", "jfr") 343 require.NoError(b.t, err) 344 _, err = jfrw.Write(jfr) 345 require.NoError(b.t, err) 346 if labels != nil { 347 labelsw, err := w.CreateFormFile("labels", "labels") 348 require.NoError(b.t, err) 349 _, err = labelsw.Write(labels) 350 require.NoError(b.t, err) 351 } 352 err = w.Close() 353 require.NoError(b.t, err) 354 ct := w.FormDataContentType() 355 bs := bb.Bytes() 356 357 url := b.url + "/ingest?name=" + b.AppName + "&spyName=" + b.spy + "&format=jfr" 358 req, err := http.NewRequest("POST", url, bytes.NewReader(bs)) 359 require.NoError(b.t, err) 360 req.Header.Set("Content-Type", ct) 361 362 return req 363 } 364 365 func (b *RequestBuilder) IngestSpeedscopeRequest(speedscopePath string) *http.Request { 366 speedscopeData, err := os.ReadFile(speedscopePath) 367 require.NoError(b.t, err) 368 369 url := b.url + "/ingest?name=" + b.AppName + "&format=speedscope" 370 req, err := http.NewRequest("POST", url, bytes.NewReader(speedscopeData)) 371 require.NoError(b.t, err) 372 req.Header.Set("Content-Type", "application/json") 373 374 return req 375 } 376 377 func (b *RequestBuilder) Render(metric string) *flamebearer.FlamebearerProfile { 378 queryURL := b.url + "/pyroscope/render?query=" + createRenderQuery(metric, b.AppName) + "&from=946656000&until=now&format=collapsed" 379 fmt.Println(queryURL) 380 queryRes, err := http.Get(queryURL) 381 require.NoError(b.t, err) 382 body := bytes.NewBuffer(nil) 383 _, err = io.Copy(body, queryRes.Body) 384 assert.NoError(b.t, err) 385 fb := new(flamebearer.FlamebearerProfile) 386 err = json.Unmarshal(body.Bytes(), fb) 387 assert.NoError(b.t, err, body.String(), queryURL) 388 assert.Greater(b.t, len(fb.Flamebearer.Names), 1, body.String(), queryRes) 389 assert.Greater(b.t, fb.Flamebearer.NumTicks, 1, body.String(), queryRes) 390 // todo check actual stacktrace contents 391 return fb 392 } 393 394 func (b *RequestBuilder) PushPPROFRequestFromFile(file string, metric string) *connect.Request[pushv1.PushRequest] { 395 updateTimestamp := func(rawProfile []byte) []byte { 396 expectedProfile, err := pprof.RawFromBytes(rawProfile) 397 require.NoError(b.t, err) 398 expectedProfile.TimeNanos = time.Now().Add(-time.Minute).UnixNano() 399 buf := bytes.NewBuffer(nil) 400 _, err = expectedProfile.WriteTo(buf) 401 require.NoError(b.t, err) 402 rawProfile = buf.Bytes() 403 return rawProfile 404 } 405 406 rawProfile, err := os.ReadFile(file) 407 require.NoError(b.t, err) 408 409 rawProfile = updateTimestamp(rawProfile) 410 411 metricName := strings.Split(metric, ":")[0] 412 413 req := connect.NewRequest(&pushv1.PushRequest{ 414 Series: []*pushv1.RawProfileSeries{{ 415 Labels: []*typesv1.LabelPair{ 416 {Name: "__name__", Value: metricName}, 417 {Name: "__delta__", Value: "false"}, 418 {Name: "service_name", Value: b.AppName}, 419 }, 420 Samples: []*pushv1.RawSample{{RawProfile: rawProfile}}, 421 }}, 422 }) 423 return req 424 } 425 426 func (b *RequestBuilder) PushPPROFRequestFromBytes(rawProfile []byte, name string) *connect.Request[pushv1.PushRequest] { 427 req := connect.NewRequest(&pushv1.PushRequest{ 428 Series: []*pushv1.RawProfileSeries{{ 429 Labels: []*typesv1.LabelPair{ 430 {Name: "__name__", Value: name}, 431 {Name: "service_name", Value: b.AppName}, 432 }, 433 Samples: []*pushv1.RawSample{{RawProfile: rawProfile}}, 434 }}, 435 }) 436 return req 437 } 438 439 func (b *RequestBuilder) QueryClient() querierv1connect.QuerierServiceClient { 440 return querierv1connect.NewQuerierServiceClient( 441 http.DefaultClient, 442 b.url, 443 connectapi.DefaultClientOptions()..., 444 ) 445 } 446 447 func (b *RequestBuilder) PushClient() pushv1connect.PusherServiceClient { 448 return pushv1connect.NewPusherServiceClient( 449 http.DefaultClient, 450 b.url, 451 connectapi.DefaultClientOptions()..., 452 ) 453 } 454 455 func (p *PyroscopeTest) Ingest(t *testing.T, req *http.Request, expectStatus int) { 456 res, err := http.DefaultClient.Do(req) 457 require.NoError(t, err) 458 require.Equal(t, expectStatus, res.StatusCode) 459 } 460 461 func (b *RequestBuilder) Push(request *connect.Request[pushv1.PushRequest], expectStatus int, expectedError string) { 462 cl := b.PushClient() 463 _, err := cl.Push(context.TODO(), request) 464 if expectStatus == 200 { 465 assert.NoError(b.t, err) 466 } else { 467 assert.Error(b.t, err) 468 var connectErr *connect.Error 469 if ok := errors.As(err, &connectErr); ok { 470 toHTTP := connectgrpc.CodeToHTTP(connectErr.Code()) 471 assert.Equal(b.t, expectStatus, int(toHTTP)) 472 if expectedError != "" { 473 assert.Contains(b.t, connectErr.Error(), expectedError) 474 } 475 } else { 476 assert.Fail(b.t, "unexpected error type", err) 477 } 478 } 479 } 480 481 func (b *RequestBuilder) SelectMergeProfile(metric string, query map[string]string) *connect.Response[profilev1.Profile] { 482 483 cnt := 0 484 selector := strings.Builder{} 485 add := func(k, v string) { 486 if cnt > 0 { 487 selector.WriteString(", ") 488 } 489 selector.WriteString(k) 490 selector.WriteString("=") 491 selector.WriteString("\"") 492 selector.WriteString(v) 493 selector.WriteString("\"") 494 cnt++ 495 } 496 selector.WriteString("{") 497 if query["service_name"] == "" { 498 add("service_name", b.AppName) 499 } 500 501 for k, v := range query { 502 add(k, v) 503 } 504 selector.WriteString("}") 505 qc := b.QueryClient() 506 resp, err := qc.SelectMergeProfile(context.Background(), connect.NewRequest(&querierv1.SelectMergeProfileRequest{ 507 ProfileTypeID: metric, 508 Start: time.Unix(1, 0).UnixMilli(), 509 End: time.Now().UnixMilli(), 510 LabelSelector: selector.String(), 511 })) 512 require.NoError(b.t, err) 513 return resp 514 } 515 516 func (b *RequestBuilder) OtelPushClient() profilesv1.ProfilesServiceClient { 517 grpcAddr := strings.TrimPrefix(b.url, "http://") 518 519 conn, err := grpc.NewClient(grpcAddr, 520 grpc.WithTransportCredentials(insecure.NewCredentials())) 521 require.NoError(b.t, err) 522 523 return profilesv1.NewProfilesServiceClient(conn) 524 } 525 526 // OtelPushHTTPProtobuf creates an HTTP request for OTLP ingestion with binary/protobuf content type 527 func (b *RequestBuilder) OtelPushHTTPProtobuf(profile *profilesv1.ExportProfilesServiceRequest) *http.Request { 528 profileBytes, err := proto.Marshal(profile) 529 require.NoError(b.t, err) 530 531 url := b.url + "/v1development/profiles" 532 req, err := http.NewRequest("POST", url, bytes.NewReader(profileBytes)) 533 require.NoError(b.t, err) 534 req.Header.Set("Content-Type", "application/x-protobuf") 535 return req 536 } 537 538 // OtelPushHTTPJSON creates an HTTP request for OTLP ingestion with JSON content type 539 func (b *RequestBuilder) OtelPushHTTPJSON(profile *profilesv1.ExportProfilesServiceRequest) *http.Request { 540 profileBytes, err := protojson.Marshal(profile) 541 require.NoError(b.t, err) 542 543 url := b.url + "/v1development/profiles" 544 req, err := http.NewRequest("POST", url, bytes.NewReader(profileBytes)) 545 require.NoError(b.t, err) 546 req.Header.Set("Content-Type", "application/json") 547 return req 548 }