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  }