github.com/grafana/pyroscope@v1.18.0/cmd/profilecli/canary_exporter_probes.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  	"time"
    14  
    15  	"connectrpc.com/connect"
    16  	"github.com/go-kit/log/level"
    17  	"github.com/google/go-cmp/cmp"
    18  	gprofile "github.com/google/pprof/profile"
    19  	"github.com/google/uuid"
    20  	"github.com/pkg/errors"
    21  
    22  	profilesv1 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development"
    23  	commonv1 "go.opentelemetry.io/proto/otlp/common/v1"
    24  	otlpprofiles "go.opentelemetry.io/proto/otlp/profiles/v1development"
    25  	resourcev1 "go.opentelemetry.io/proto/otlp/resource/v1"
    26  	"google.golang.org/protobuf/encoding/protojson"
    27  	"google.golang.org/protobuf/proto"
    28  
    29  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    30  	pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1"
    31  	querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
    32  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    33  	"github.com/grafana/pyroscope/pkg/model"
    34  	"github.com/grafana/pyroscope/pkg/og/structs/flamebearer"
    35  	"github.com/grafana/pyroscope/pkg/pprof/testhelper"
    36  )
    37  
    38  const (
    39  	profileTypeID             = "deadmans_switch:made_up:profilos:made_up:profilos"
    40  	otlpProfileTypeID         = "otlp_test:otlp_test:count:otlp_test:samples"
    41  	canaryExporterServiceName = "pyroscope-canary-exporter"
    42  )
    43  
    44  func (ce *canaryExporter) testIngestProfile(ctx context.Context, now time.Time) error {
    45  	p := testhelper.NewProfileBuilder(now.UnixNano())
    46  	p.Labels = p.Labels[:0]
    47  	p.CustomProfile("deadmans_switch", "made_up", "profilos", "made_up", "profilos")
    48  	p.WithLabels(
    49  		"service_name", canaryExporterServiceName,
    50  		"job", "canary-exporter",
    51  		"instance", ce.hostname,
    52  	)
    53  	p.UUID = uuid.New()
    54  	p.ForStacktraceString("func1", "func2").AddSamples(10)
    55  	p.ForStacktraceString("func1").AddSamples(20)
    56  
    57  	// for testing the span selection
    58  	p.StringTable = append(p.StringTable, "profile_id", "00000bac2a5ab0c7")
    59  	p.Sample[1].Label = []*googlev1.Label{{Key: int64(len(p.StringTable) - 2), Str: int64(len(p.StringTable) - 1)}}
    60  
    61  	data, err := p.MarshalVT()
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	if _, err := ce.params.pusherClient().Push(ctx, connect.NewRequest(&pushv1.PushRequest{
    67  		Series: []*pushv1.RawProfileSeries{
    68  			{
    69  				Labels: p.Labels,
    70  				Samples: []*pushv1.RawSample{{
    71  					ID:         p.UUID.String(),
    72  					RawProfile: data,
    73  				}},
    74  			},
    75  		},
    76  	})); err != nil {
    77  		return err
    78  	}
    79  
    80  	level.Info(logger).Log("msg", "successfully ingested profile", "uuid", p.UUID.String())
    81  	return nil
    82  }
    83  
    84  // generateOTLPProfile creates an OTLP profile with the specified ingestion method label
    85  
    86  func (ce *canaryExporter) generateOTLPProfile(now time.Time, ingestionMethod string) *profilesv1.ExportProfilesServiceRequest {
    87  	// Sanitize the ingestion method label value by replacing "/" with "_"
    88  	sanitizedMethod := strings.ReplaceAll(ingestionMethod, "/", "_")
    89  
    90  	// Create the profile dictionary with custom profile type similar to pprof probe
    91  	dictionary := &otlpprofiles.ProfilesDictionary{
    92  		StringTable: []string{
    93  			"",                 // 0: empty string
    94  			"otlp_test",        // 1
    95  			"samples",          // 2
    96  			"count",            // 3
    97  			"func1",            // 4
    98  			"func2",            // 5
    99  			"ingestion_method", // 6
   100  			sanitizedMethod,    // 7
   101  		},
   102  		MappingTable: []*otlpprofiles.Mapping{
   103  			{}, // 0: empty mapping (required null entry)
   104  		},
   105  		FunctionTable: []*otlpprofiles.Function{
   106  			{NameStrindex: 0}, // 0: empty
   107  			{NameStrindex: 4}, // 1: func1
   108  			{NameStrindex: 5}, // 2: func2
   109  		},
   110  		LocationTable: []*otlpprofiles.Location{
   111  			{Lines: []*otlpprofiles.Line{{FunctionIndex: 1}}}, // 0: func1
   112  			{Lines: []*otlpprofiles.Line{{FunctionIndex: 2}}}, // 1: func2
   113  		},
   114  		StackTable: []*otlpprofiles.Stack{
   115  			{LocationIndices: []int32{}},     // 0: empty (required null entry)
   116  			{LocationIndices: []int32{1, 0}}, // 1: func2, func1 stack
   117  			{LocationIndices: []int32{0}},    // 2: func1 stack
   118  		},
   119  	}
   120  
   121  	// Create profile with two samples matching the original pprof profile
   122  	profile := &otlpprofiles.Profile{
   123  		TimeUnixNano: uint64(now.UnixNano()),
   124  		DurationNano: 0,
   125  		Period:       1,
   126  		SampleType: &otlpprofiles.ValueType{
   127  			TypeStrindex: 1, // "otlp_test"
   128  			UnitStrindex: 3, // "count"
   129  		},
   130  		PeriodType: &otlpprofiles.ValueType{
   131  			TypeStrindex: 1, // "otlp_test"
   132  			UnitStrindex: 2, // "samples"
   133  		},
   134  		Samples: []*otlpprofiles.Sample{
   135  			{
   136  				// func1>func2 with value 10
   137  				StackIndex: 1, // stack_table[1]
   138  				Values:     []int64{10},
   139  			},
   140  			{
   141  				// func1 with value 20
   142  				StackIndex: 2, // stack_table[2]
   143  				Values:     []int64{20},
   144  			},
   145  		},
   146  	}
   147  
   148  	// Create the resource attributes
   149  	resourceAttrs := []*commonv1.KeyValue{
   150  		{
   151  			Key:   "service.name",
   152  			Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: canaryExporterServiceName}},
   153  		},
   154  		{
   155  			Key:   "job",
   156  			Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: "canary-exporter"}},
   157  		},
   158  		{
   159  			Key:   "instance",
   160  			Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: ce.hostname}},
   161  		},
   162  		{
   163  			Key:   "ingestion_method",
   164  			Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: sanitizedMethod}},
   165  		},
   166  	}
   167  
   168  	// Create the OTLP request
   169  	req := &profilesv1.ExportProfilesServiceRequest{
   170  		Dictionary: dictionary,
   171  		ResourceProfiles: []*otlpprofiles.ResourceProfiles{
   172  			{
   173  				Resource: &resourcev1.Resource{
   174  					Attributes: resourceAttrs,
   175  				},
   176  				ScopeProfiles: []*otlpprofiles.ScopeProfiles{
   177  					{
   178  						Scope: &commonv1.InstrumentationScope{
   179  							Name: "pyroscope-canary-exporter",
   180  						},
   181  						Profiles: []*otlpprofiles.Profile{profile},
   182  					},
   183  				},
   184  			},
   185  		},
   186  	}
   187  
   188  	return req
   189  }
   190  
   191  /*
   192  	 func (ce *canaryExporter) testIngestOTLPGrpc(ctx context.Context, now time.Time) error {
   193  		// Generate the OTLP profile with the appropriate ingestion method label
   194  		req := ce.generateOTLPProfile(now, "otlp/grpc")
   195  
   196  		// Parse URL to extract host and port
   197  		parsedURL, err := url.Parse(ce.params.URL)
   198  		if err != nil {
   199  			return fmt.Errorf("failed to parse URL: %w", err)
   200  		}
   201  		port := parsedURL.Port()
   202  		if port == "" && parsedURL.Scheme == "http" {
   203  			port = "80"
   204  		} else if port == "" && parsedURL.Scheme == "https" {
   205  			port = "443"
   206  		} else {
   207  			port = "4317" // default OTLP gRPC port
   208  		}
   209  		grpcAddr := fmt.Sprintf("%s:%s", parsedURL.Hostname(), port)
   210  
   211  		// Create gRPC connection
   212  		conn, err := grpc.NewClient(grpcAddr,
   213  			grpc.WithTransportCredentials(insecure.NewCredentials()))
   214  		if err != nil {
   215  			return fmt.Errorf("failed to connect to gRPC server: %w", err)
   216  		}
   217  		defer conn.Close()
   218  
   219  		// Create OTLP profiles service client
   220  		client := profilesv1.NewProfilesServiceClient(conn)
   221  
   222  		// Send the profile
   223  		_, err = client.Export(ctx, req)
   224  		if err != nil {
   225  			return fmt.Errorf("failed to export OTLP profile via gRPC: %w", err)
   226  		}
   227  
   228  		level.Info(logger).Log("msg", "successfully ingested OTLP profile via gRPC")
   229  		return nil
   230  	}
   231  */
   232  
   233  func (ce *canaryExporter) testIngestOTLPHttpJson(ctx context.Context, now time.Time) error {
   234  	// Generate the OTLP profile with the appropriate ingestion method label
   235  	req := ce.generateOTLPProfile(now, "otlp/http/json")
   236  
   237  	// Marshal to JSON
   238  	jsonData, err := protojson.Marshal(req)
   239  	if err != nil {
   240  		return fmt.Errorf("failed to marshal OTLP profile to JSON: %w", err)
   241  	}
   242  
   243  	// Create HTTP request using the instrumented client
   244  	url := ce.params.URL + "/v1development/profiles"
   245  	httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
   246  	if err != nil {
   247  		return fmt.Errorf("failed to create HTTP request: %w", err)
   248  	}
   249  	httpReq.Header.Set("Content-Type", "application/json")
   250  
   251  	// Send the request using the instrumented client (ce.params.client is set by doTrace)
   252  	resp, err := ce.params.client.Do(httpReq)
   253  	if err != nil {
   254  		return fmt.Errorf("failed to send HTTP request: %w", err)
   255  	}
   256  	defer resp.Body.Close()
   257  
   258  	// Read the body to ensure the transport is fully traced
   259  	body, err := io.ReadAll(resp.Body)
   260  	if err != nil {
   261  		return fmt.Errorf("failed to read response body: %w", err)
   262  	}
   263  
   264  	// Check response status
   265  	if resp.StatusCode != http.StatusOK {
   266  		return fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
   267  	}
   268  
   269  	level.Info(logger).Log("msg", "successfully ingested OTLP profile via HTTP/JSON")
   270  	return nil
   271  }
   272  
   273  func (ce *canaryExporter) testIngestOTLPHttpProtobuf(ctx context.Context, now time.Time) error {
   274  	// Generate the OTLP profile with the appropriate ingestion method label
   275  	req := ce.generateOTLPProfile(now, "otlp/http/protobuf")
   276  
   277  	// Marshal to protobuf
   278  	protoData, err := proto.Marshal(req)
   279  	if err != nil {
   280  		return fmt.Errorf("failed to marshal OTLP profile to protobuf: %w", err)
   281  	}
   282  
   283  	// Create HTTP request using the instrumented client
   284  	url := ce.params.URL + "/v1development/profiles"
   285  	httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(protoData))
   286  	if err != nil {
   287  		return fmt.Errorf("failed to create HTTP request: %w", err)
   288  	}
   289  	httpReq.Header.Set("Content-Type", "application/x-protobuf")
   290  
   291  	// Send the request using the instrumented client (ce.params.client is set by doTrace)
   292  	resp, err := ce.params.client.Do(httpReq)
   293  	if err != nil {
   294  		return fmt.Errorf("failed to send HTTP request: %w", err)
   295  	}
   296  	defer resp.Body.Close()
   297  
   298  	// Read the body to ensure the transport is fully traced
   299  	body, err := io.ReadAll(resp.Body)
   300  	if err != nil {
   301  		return fmt.Errorf("failed to read response body: %w", err)
   302  	}
   303  
   304  	// Check response status
   305  	if resp.StatusCode != http.StatusOK {
   306  		return fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
   307  	}
   308  
   309  	level.Info(logger).Log("msg", "successfully ingested OTLP profile via HTTP/Protobuf")
   310  	return nil
   311  }
   312  func (ce *canaryExporter) testSelectMergeProfile(ctx context.Context, now time.Time) error {
   313  	respQuery, err := ce.params.queryClient().SelectMergeProfile(ctx, connect.NewRequest(&querierv1.SelectMergeProfileRequest{
   314  		Start:         now.UnixMilli(),
   315  		End:           now.Add(5 * time.Second).UnixMilli(),
   316  		LabelSelector: ce.createLabelSelector(),
   317  		ProfileTypeID: profileTypeID,
   318  	}))
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	buf, err := respQuery.Msg.MarshalVT()
   324  	if err != nil {
   325  		return errors.Wrap(err, "failed to marshal protobuf")
   326  	}
   327  
   328  	gp, err := gprofile.Parse(bytes.NewReader(buf))
   329  	if err != nil {
   330  		return errors.Wrap(err, "failed to parse profile")
   331  	}
   332  
   333  	expected := map[string]int64{
   334  		"func1>func2": 10,
   335  		"func1":       20,
   336  	}
   337  	actual := make(map[string]int64)
   338  
   339  	var sb strings.Builder
   340  	for _, s := range gp.Sample {
   341  		sb.Reset()
   342  		for _, loc := range s.Location {
   343  			if sb.Len() != 0 {
   344  				_, err := sb.WriteRune('>')
   345  				if err != nil {
   346  					return err
   347  				}
   348  			}
   349  			for _, line := range loc.Line {
   350  				_, err := sb.WriteString(line.Function.Name)
   351  				if err != nil {
   352  					return err
   353  				}
   354  			}
   355  		}
   356  		actual[sb.String()] = actual[sb.String()] + s.Value[0]
   357  	}
   358  
   359  	if diff := cmp.Diff(expected, actual); diff != "" {
   360  		return fmt.Errorf("query mismatch (-expected, +actual):\n%s", diff)
   361  	}
   362  
   363  	return nil
   364  }
   365  
   366  func (ce *canaryExporter) testSelectMergeOTLPProfile(ctx context.Context, now time.Time) error {
   367  	// Query specifically for OTLP gRPC ingested profiles using the custom profile type
   368  	//labelSelector := fmt.Sprintf(`{service_name="%s", job="canary-exporter", instance="%s"}`, canaryExporterServiceName, ce.hostname)
   369  
   370  	respQuery, err := ce.params.queryClient().SelectMergeProfile(ctx, connect.NewRequest(&querierv1.SelectMergeProfileRequest{
   371  		Start:         now.UnixMilli(),
   372  		End:           now.Add(5 * time.Second).UnixMilli(),
   373  		LabelSelector: ce.createLabelSelector(),
   374  		ProfileTypeID: otlpProfileTypeID,
   375  	}))
   376  	if err != nil {
   377  		return fmt.Errorf("failed to query OTLP profile: %w", err)
   378  	}
   379  
   380  	buf, err := respQuery.Msg.MarshalVT()
   381  	if err != nil {
   382  		return errors.Wrap(err, "failed to marshal protobuf")
   383  	}
   384  
   385  	gp, err := gprofile.Parse(bytes.NewReader(buf))
   386  	if err != nil {
   387  		return errors.Wrap(err, "failed to parse profile")
   388  	}
   389  
   390  	// Verify the expected stacktraces from the OTLP profile
   391  	expected := map[string]int64{
   392  		"func2>func1": 20, // 10 samples from each of the 2 ingestion methods
   393  		"func1":       40, // 20 samples * 2
   394  	}
   395  	actual := make(map[string]int64)
   396  
   397  	var sb strings.Builder
   398  	for _, s := range gp.Sample {
   399  		sb.Reset()
   400  		for _, loc := range s.Location {
   401  			if sb.Len() != 0 {
   402  				_, err := sb.WriteRune('>')
   403  				if err != nil {
   404  					return err
   405  				}
   406  			}
   407  			for _, line := range loc.Line {
   408  				_, err := sb.WriteString(line.Function.Name)
   409  				if err != nil {
   410  					return err
   411  				}
   412  			}
   413  		}
   414  		actual[sb.String()] = actual[sb.String()] + s.Value[0]
   415  	}
   416  
   417  	if diff := cmp.Diff(expected, actual); diff != "" {
   418  		return fmt.Errorf("OTLP profile query mismatch (-expected, +actual):\n%s", diff)
   419  	}
   420  
   421  	level.Info(logger).Log("msg", "successfully queried OTLP profile via gRPC")
   422  	return nil
   423  }
   424  
   425  func (ce *canaryExporter) testProfileTypes(ctx context.Context, now time.Time) error {
   426  	respQuery, err := ce.params.queryClient().ProfileTypes(ctx, connect.NewRequest(&querierv1.ProfileTypesRequest{
   427  		Start: now.UnixMilli(),
   428  		End:   now.Add(5 * time.Second).UnixMilli(),
   429  	}))
   430  	if err != nil {
   431  		return err
   432  	}
   433  
   434  	for _, pt := range respQuery.Msg.ProfileTypes {
   435  		if pt.ID == profileTypeID {
   436  			level.Info(logger).Log("msg", "found expected profile type", "id", pt.ID)
   437  			return nil
   438  		}
   439  	}
   440  
   441  	return fmt.Errorf("expected profile type %s not found", profileTypeID)
   442  }
   443  
   444  func (ce *canaryExporter) testSeries(ctx context.Context, now time.Time) error {
   445  	respQuery, err := ce.params.queryClient().Series(ctx, connect.NewRequest(&querierv1.SeriesRequest{
   446  		Start:      now.UnixMilli(),
   447  		End:        now.Add(5 * time.Second).UnixMilli(),
   448  		LabelNames: []string{model.LabelNameServiceName, model.LabelNameProfileType},
   449  	}))
   450  
   451  	if err != nil {
   452  		return err
   453  	}
   454  	labelSets := respQuery.Msg.LabelsSet
   455  
   456  	if len(labelSets) < 1 {
   457  		return fmt.Errorf("expected at least 1 label set, got %d", len(labelSets))
   458  	}
   459  
   460  	for _, ls := range labelSets {
   461  		labels := model.Labels(ls.Labels)
   462  
   463  		serviceName := labels.Get(model.LabelNameServiceName)
   464  		if serviceName == "" {
   465  			return fmt.Errorf("expected service_name label to be set")
   466  		}
   467  		if serviceName != canaryExporterServiceName {
   468  			continue
   469  		}
   470  		profileType := labels.Get(model.LabelNameProfileType)
   471  		if profileType == "" {
   472  			return fmt.Errorf("expected profile_type label to be set")
   473  		}
   474  		if profileType != profileTypeID {
   475  			continue
   476  		}
   477  		return nil // found the expected series
   478  	}
   479  
   480  	return fmt.Errorf("expected series with service_name=%s and profile_type=%s not found", canaryExporterServiceName, profileTypeID)
   481  }
   482  
   483  func (ce *canaryExporter) testLabelNames(ctx context.Context, now time.Time) error {
   484  	respQuery, err := ce.params.queryClient().LabelNames(ctx, connect.NewRequest(&typesv1.LabelNamesRequest{
   485  		Start: now.UnixMilli(),
   486  		End:   now.Add(5 * time.Second).UnixMilli(),
   487  		// we have to pass this matcher to skip the tenant-wide index in v2 which is not ready until after compaction
   488  		Matchers: []string{fmt.Sprintf(`{service_name="%s"}`, canaryExporterServiceName)},
   489  	}))
   490  
   491  	if err != nil {
   492  		return err
   493  	}
   494  
   495  	labelNames := respQuery.Msg.Names
   496  
   497  	expectedLabelNames := []string{
   498  		model.LabelNameProfileName,
   499  		model.LabelNamePeriodType,
   500  		model.LabelNamePeriodUnit,
   501  		model.LabelNameProfileType,
   502  		model.LabelNameServiceNamePrivate,
   503  		model.LabelNameType,
   504  		model.LabelNameUnit,
   505  		//"service.name", // todo: return once write path is ready for utf8 labels
   506  		model.LabelNameServiceName,
   507  	}
   508  
   509  	// Use map as set for O(1) lookups
   510  	labelNamesSet := make(map[string]struct{}, len(labelNames))
   511  	for _, label := range labelNames {
   512  		labelNamesSet[label] = struct{}{}
   513  	}
   514  
   515  	missingLabels := []string{}
   516  	for _, expectedLabel := range expectedLabelNames {
   517  		if _, exists := labelNamesSet[expectedLabel]; !exists {
   518  			missingLabels = append(missingLabels, expectedLabel)
   519  		}
   520  	}
   521  	if len(missingLabels) > 0 {
   522  		return fmt.Errorf("missing expected labels: %s", missingLabels)
   523  	}
   524  
   525  	return nil
   526  }
   527  
   528  func (ce *canaryExporter) testLabelValues(ctx context.Context, now time.Time) error {
   529  	respQuery, err := ce.params.queryClient().LabelValues(ctx, connect.NewRequest(&typesv1.LabelValuesRequest{
   530  		Start: now.UnixMilli(),
   531  		End:   now.Add(5 * time.Second).UnixMilli(),
   532  		Name:  model.LabelNameServiceName,
   533  		// we have to pass this matcher to skip the tenant-wide index in v2 which is not ready until after compaction
   534  		Matchers: []string{fmt.Sprintf(`{service_name="%s"}`, canaryExporterServiceName)},
   535  	}))
   536  
   537  	if err != nil {
   538  		return err
   539  	}
   540  
   541  	if len(respQuery.Msg.Names) != 1 {
   542  		return fmt.Errorf("expected 1 label value, got %d", len(respQuery.Msg.Names))
   543  	}
   544  
   545  	serviceName := respQuery.Msg.Names[0]
   546  
   547  	if serviceName != canaryExporterServiceName {
   548  		return fmt.Errorf("expected service_name label to be %s, got %s", canaryExporterServiceName, serviceName)
   549  	}
   550  
   551  	return nil
   552  }
   553  
   554  func (ce *canaryExporter) testSelectSeries(ctx context.Context, now time.Time) error {
   555  	respQuery, err := ce.params.queryClient().SelectSeries(ctx, connect.NewRequest(&querierv1.SelectSeriesRequest{
   556  		Start:         now.UnixMilli(),
   557  		End:           now.Add(5 * time.Second).UnixMilli(),
   558  		Step:          1000,
   559  		LabelSelector: ce.createLabelSelector(),
   560  		ProfileTypeID: profileTypeID,
   561  		GroupBy:       []string{model.LabelNameServiceName},
   562  	}))
   563  
   564  	if err != nil {
   565  		return err
   566  	}
   567  
   568  	if len(respQuery.Msg.Series) != 1 {
   569  		return fmt.Errorf("expected 1 series, got %d", len(respQuery.Msg.Series))
   570  	}
   571  
   572  	series := respQuery.Msg.Series[0]
   573  
   574  	if len(series.Points) != 1 {
   575  		return fmt.Errorf("expected 2 points, got %d", len(series.Points))
   576  	}
   577  
   578  	labels := model.Labels(series.Labels)
   579  
   580  	if len(labels) != 1 {
   581  		return fmt.Errorf("expected 1 labels, got %d", len(labels))
   582  	}
   583  
   584  	serviceName := labels.Get(model.LabelNameServiceName)
   585  	if serviceName == "" {
   586  		return fmt.Errorf("expected service_name label to be set")
   587  	}
   588  	if serviceName != canaryExporterServiceName {
   589  		return fmt.Errorf("expected service_name label to be %s, got %s", canaryExporterServiceName, serviceName)
   590  	}
   591  
   592  	return nil
   593  }
   594  
   595  func (ce *canaryExporter) testSelectMergeStacktraces(ctx context.Context, now time.Time) error {
   596  	respQuery, err := ce.params.queryClient().SelectMergeStacktraces(ctx, connect.NewRequest(&querierv1.SelectMergeStacktracesRequest{
   597  		Start:         now.UnixMilli(),
   598  		End:           now.Add(5 * time.Second).UnixMilli(),
   599  		LabelSelector: ce.createLabelSelector(),
   600  		ProfileTypeID: profileTypeID,
   601  	}))
   602  
   603  	if err != nil {
   604  		return err
   605  	}
   606  
   607  	flamegraph := respQuery.Msg.Flamegraph
   608  
   609  	if len(flamegraph.Names) != 3 {
   610  		return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamegraph.Names))
   611  	}
   612  
   613  	if len(flamegraph.Levels) != 3 {
   614  		return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamegraph.Levels))
   615  	}
   616  
   617  	return nil
   618  }
   619  
   620  func (ce *canaryExporter) testSelectMergeSpanProfile(ctx context.Context, now time.Time) error {
   621  	respQuery, err := ce.params.queryClient().SelectMergeSpanProfile(ctx, connect.NewRequest(&querierv1.SelectMergeSpanProfileRequest{
   622  		Start:         now.UnixMilli(),
   623  		End:           now.Add(5 * time.Second).UnixMilli(),
   624  		LabelSelector: ce.createLabelSelector(),
   625  		ProfileTypeID: profileTypeID,
   626  		SpanSelector:  []string{"00000bac2a5ab0c7"},
   627  	}))
   628  
   629  	if err != nil {
   630  		return err
   631  	}
   632  
   633  	flamegraph := respQuery.Msg.Flamegraph
   634  
   635  	if flamegraph == nil {
   636  		return fmt.Errorf("expected flamegraph to be set")
   637  	}
   638  
   639  	if len(flamegraph.Names) != 2 {
   640  		return fmt.Errorf("expected 2 names in flamegraph, got %d", len(flamegraph.Names))
   641  	}
   642  
   643  	if len(flamegraph.Levels) != 2 {
   644  		return fmt.Errorf("expected 2 levels in flamegraph, got %d", len(flamegraph.Levels))
   645  	}
   646  
   647  	return nil
   648  }
   649  
   650  func (ce *canaryExporter) testRender(ctx context.Context, now time.Time) error {
   651  	query := profileTypeID + ce.createLabelSelector()
   652  	startTime := now.UnixMilli()
   653  	endTime := now.Add(5 * time.Second).UnixMilli()
   654  
   655  	baseURL, err := url.Parse(ce.params.URL)
   656  	if err != nil {
   657  		return err
   658  	}
   659  	baseURL.Path = "/pyroscope/render"
   660  
   661  	params := url.Values{}
   662  	params.Add("query", query)
   663  	params.Add("from", fmt.Sprintf("%d", startTime))
   664  	params.Add("until", fmt.Sprintf("%d", endTime))
   665  
   666  	baseURL.RawQuery = params.Encode()
   667  	reqURL := baseURL.String()
   668  	level.Debug(logger).Log("msg", "requesting render", "url", reqURL)
   669  
   670  	req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
   671  	if err != nil {
   672  		return err
   673  	}
   674  
   675  	resp, err := ce.params.httpClient().Do(req)
   676  	if err != nil {
   677  		return err
   678  	}
   679  	defer resp.Body.Close()
   680  
   681  	if resp.StatusCode != http.StatusOK {
   682  		body, _ := io.ReadAll(resp.Body)
   683  		return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
   684  	}
   685  
   686  	var flamebearerProfile flamebearer.FlamebearerProfile
   687  	if err := json.NewDecoder(resp.Body).Decode(&flamebearerProfile); err != nil {
   688  		return err
   689  	}
   690  
   691  	if len(flamebearerProfile.Flamebearer.Names) != 3 {
   692  		return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Names))
   693  	}
   694  
   695  	if len(flamebearerProfile.Flamebearer.Levels) != 3 {
   696  		return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Levels))
   697  	}
   698  
   699  	return nil
   700  }
   701  
   702  func (ce *canaryExporter) testRenderDiff(ctx context.Context, now time.Time) error {
   703  	query := profileTypeID + ce.createLabelSelector()
   704  	startTime := now.UnixMilli()
   705  	endTime := now.Add(5 * time.Second).UnixMilli()
   706  
   707  	baseURL, err := url.Parse(ce.params.URL)
   708  	if err != nil {
   709  		return err
   710  	}
   711  	baseURL.Path = "/pyroscope/render-diff"
   712  
   713  	params := url.Values{}
   714  	params.Add("leftQuery", query)
   715  	params.Add("leftFrom", fmt.Sprintf("%d", startTime))
   716  	params.Add("leftUntil", fmt.Sprintf("%d", endTime))
   717  	params.Add("rightQuery", query)
   718  	params.Add("rightFrom", fmt.Sprintf("%d", startTime))
   719  	params.Add("rightUntil", fmt.Sprintf("%d", endTime))
   720  
   721  	baseURL.RawQuery = params.Encode()
   722  	reqURL := baseURL.String()
   723  	level.Debug(logger).Log("msg", "requesting diff", "url", reqURL)
   724  
   725  	req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
   726  	if err != nil {
   727  		return err
   728  	}
   729  
   730  	resp, err := ce.params.httpClient().Do(req)
   731  	if err != nil {
   732  		return err
   733  	}
   734  	defer resp.Body.Close()
   735  
   736  	if resp.StatusCode != http.StatusOK {
   737  		body, _ := io.ReadAll(resp.Body)
   738  		return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
   739  	}
   740  
   741  	var flamebearerProfile flamebearer.FlamebearerProfile
   742  	if err := json.NewDecoder(resp.Body).Decode(&flamebearerProfile); err != nil {
   743  		return err
   744  	}
   745  
   746  	if len(flamebearerProfile.Flamebearer.Names) != 3 {
   747  		return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Names))
   748  	}
   749  
   750  	if len(flamebearerProfile.Flamebearer.Levels) != 3 {
   751  		return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Levels))
   752  	}
   753  
   754  	return nil
   755  }
   756  
   757  func (ce *canaryExporter) testGetProfileStats(ctx context.Context, now time.Time) error {
   758  	resp, err := ce.params.queryClient().GetProfileStats(ctx, connect.NewRequest(&typesv1.GetProfileStatsRequest{}))
   759  
   760  	if err != nil {
   761  		return err
   762  	}
   763  
   764  	if !resp.Msg.DataIngested {
   765  		return fmt.Errorf("expected data to be ingested")
   766  	}
   767  
   768  	if resp.Msg.OldestProfileTime == math.MinInt64 {
   769  		return fmt.Errorf("expected oldest profile time to be set")
   770  	}
   771  
   772  	if resp.Msg.NewestProfileTime == math.MaxInt64 {
   773  		return fmt.Errorf("expected newest profile time to be set")
   774  	}
   775  
   776  	return nil
   777  }
   778  
   779  func (ce *canaryExporter) createLabelSelector() string {
   780  	return fmt.Sprintf(`{service_name="%s", job="canary-exporter", instance="%s"}`, canaryExporterServiceName, ce.hostname)
   781  }