github.com/grafana/pyroscope@v1.18.0/pkg/querybackend/block_reader_test.go (about)

     1  package querybackend
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"os"
     8  	"path/filepath"
     9  	"slices"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/suite"
    14  
    15  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    16  	metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1"
    17  	queryv1 "github.com/grafana/pyroscope/api/gen/proto/go/query/v1"
    18  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    19  	"github.com/grafana/pyroscope/pkg/block"
    20  	"github.com/grafana/pyroscope/pkg/block/metadata"
    21  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    22  	"github.com/grafana/pyroscope/pkg/objstore"
    23  	"github.com/grafana/pyroscope/pkg/objstore/providers/memory"
    24  	"github.com/grafana/pyroscope/pkg/pprof"
    25  	"github.com/grafana/pyroscope/pkg/querybackend/queryplan"
    26  	"github.com/grafana/pyroscope/pkg/test"
    27  )
    28  
    29  type testSuite struct {
    30  	suite.Suite
    31  	dir string
    32  
    33  	ctx    context.Context
    34  	logger *test.TestingLogger
    35  	bucket *memory.InMemBucket
    36  	blocks []*metastorev1.BlockMeta
    37  
    38  	reader *BlockReader
    39  	meta   []*metastorev1.BlockMeta
    40  	plan   *queryv1.QueryPlan
    41  	tenant []string
    42  }
    43  
    44  func (s *testSuite) SetupSuite() {
    45  	s.bucket = memory.NewInMemBucket()
    46  	s.loadFromDir(s.dir)
    47  }
    48  
    49  func (s *testSuite) SetupTest() {
    50  	s.ctx = context.Background()
    51  	s.logger = test.NewTestingLogger(s.T())
    52  	s.reader = NewBlockReader(s.logger, &objstore.ReaderAtBucket{Bucket: s.bucket}, nil)
    53  	s.meta = make([]*metastorev1.BlockMeta, len(s.blocks))
    54  	for i, b := range s.blocks {
    55  		s.meta[i] = b.CloneVT()
    56  	}
    57  	s.sanitizeMetadata()
    58  	s.plan = queryplan.Build(s.meta, 10, 10)
    59  	s.tenant = make([]string, 0)
    60  	for _, b := range s.plan.Root.Blocks {
    61  		for _, d := range b.Datasets {
    62  			s.tenant = append(s.tenant, b.StringTable[d.Tenant])
    63  		}
    64  	}
    65  }
    66  
    67  func (s *testSuite) loadFromDir(dir string) {
    68  	s.Require().NoError(filepath.WalkDir(dir, s.visitPath))
    69  }
    70  
    71  func (s *testSuite) visitPath(path string, e os.DirEntry, err error) error {
    72  	if err != nil || e.IsDir() {
    73  		return err
    74  	}
    75  	b, err := os.ReadFile(path)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	var md metastorev1.BlockMeta
    80  	if err = metadata.Decode(b, &md); err != nil {
    81  		return err
    82  	}
    83  	md.Size = uint64(len(b))
    84  	s.blocks = append(s.blocks, &md)
    85  	return s.bucket.Upload(context.Background(), block.ObjectPath(&md), bytes.NewReader(b))
    86  }
    87  
    88  func (s *testSuite) sanitizeMetadata() {
    89  	// We read the whole block metadata, including all the datasets.
    90  	// In practice, this is never the case – metadata queries either
    91  	// return the datasets to be read or the dataset index.
    92  	hasIndex := 0
    93  	total := 0
    94  	for _, m := range s.meta {
    95  		for _, d := range m.Datasets {
    96  			total++
    97  			if block.DatasetFormat(d.Format) == block.DatasetFormat1 {
    98  				m.Datasets = slices.DeleteFunc(m.Datasets, func(x *metastorev1.Dataset) bool {
    99  					return x.Format == 0
   100  				})
   101  				hasIndex++
   102  				break
   103  			}
   104  		}
   105  	}
   106  	// We ensure that there are both cases.
   107  	s.Assert().NotZero(total)
   108  	s.Assert().NotZero(hasIndex)
   109  }
   110  
   111  func (s *testSuite) BeforeTest(_, _ string) {}
   112  
   113  func (s *testSuite) AfterTest(_, _ string) {}
   114  
   115  func TestSuite(t *testing.T) { suite.Run(t, &testSuite{dir: "testdata/samples"}) }
   116  
   117  func (s *testSuite) Test_QueryTree_All() {
   118  
   119  	expected, err := os.ReadFile("testdata/fixtures/tree_16.txt")
   120  	s.Require().NoError(err)
   121  
   122  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   123  		EndTime:       time.Now().UnixMilli(),
   124  		LabelSelector: "{}",
   125  		QueryPlan:     s.plan,
   126  		Query: []*queryv1.Query{{
   127  			QueryType: queryv1.QueryType_QUERY_TREE,
   128  			Tree:      &queryv1.TreeQuery{MaxNodes: 16},
   129  		}},
   130  		Tenant: s.tenant,
   131  	})
   132  
   133  	s.Require().NoError(err)
   134  	s.Require().NotNil(resp)
   135  	s.Require().Len(resp.Reports, 1)
   136  	tree, err := phlaremodel.UnmarshalTree(resp.Reports[0].Tree.Tree)
   137  	s.Require().NoError(err)
   138  
   139  	s.Assert().Equal(string(expected), tree.String())
   140  }
   141  
   142  func (s *testSuite) Test_QueryTree_Filter() {
   143  	expected, err := os.ReadFile("testdata/fixtures/tree_16_slow.txt")
   144  	s.Require().NoError(err)
   145  
   146  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   147  		EndTime:       time.Now().UnixMilli(),
   148  		LabelSelector: `{service_name="test-app",function="slow"}`,
   149  		QueryPlan:     s.plan,
   150  		Query: []*queryv1.Query{{
   151  			QueryType: queryv1.QueryType_QUERY_TREE,
   152  			Tree:      &queryv1.TreeQuery{MaxNodes: 16},
   153  		}},
   154  		Tenant: s.tenant,
   155  	})
   156  
   157  	s.Require().NoError(err)
   158  	s.Require().NotNil(resp)
   159  	s.Require().Len(resp.Reports, 1)
   160  	tree, err := phlaremodel.UnmarshalTree(resp.Reports[0].Tree.Tree)
   161  	s.Require().NoError(err)
   162  
   163  	s.Assert().Equal(string(expected), tree.String())
   164  }
   165  
   166  func (s *testSuite) Test_QueryPprof_Metadata() {
   167  	selector := `{service_name="test-app",__profile_type__="process_cpu:cpu:nanoseconds:cpu:nanoseconds"}`
   168  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   169  		EndTime:       time.Now().UnixMilli(),
   170  		LabelSelector: selector,
   171  		QueryPlan:     s.plan,
   172  		Query: []*queryv1.Query{{
   173  			QueryType: queryv1.QueryType_QUERY_PPROF,
   174  			Pprof:     &queryv1.PprofQuery{},
   175  		}},
   176  		Tenant: s.tenant,
   177  	})
   178  
   179  	s.Require().NoError(err)
   180  	s.Require().NotNil(resp)
   181  	s.Require().Len(resp.Reports, 1)
   182  
   183  	var p profilev1.Profile
   184  	s.Require().NoError(pprof.Unmarshal(resp.Reports[0].Pprof.Pprof, &p))
   185  
   186  	s.Assert().Len(p.SampleType, 1)
   187  	s.Assert().Equal("cpu", p.StringTable[p.SampleType[0].Type])
   188  	s.Assert().Equal("nanoseconds", p.StringTable[p.SampleType[0].Unit])
   189  
   190  	s.Assert().NotNil(p.PeriodType)
   191  	s.Assert().Equal("cpu", p.StringTable[p.PeriodType.Type])
   192  	s.Assert().Equal("nanoseconds", p.StringTable[p.PeriodType.Unit])
   193  }
   194  
   195  func (s *testSuite) Test_DatasetIndex_SeriesLabels_GroupBy() {
   196  	selector := `{service_repository="https://github.com/grafana/pyroscope"}`
   197  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   198  		EndTime:       time.Now().UnixMilli(),
   199  		LabelSelector: selector,
   200  		QueryPlan:     s.plan,
   201  		Query: []*queryv1.Query{{
   202  			QueryType: queryv1.QueryType_QUERY_SERIES_LABELS,
   203  			SeriesLabels: &queryv1.SeriesLabelsQuery{
   204  				LabelNames: []string{"service_name", "__profile_type__"},
   205  			},
   206  		}},
   207  		Tenant: s.tenant,
   208  	})
   209  
   210  	s.Require().NoError(err)
   211  	s.Require().NotNil(resp)
   212  	s.Require().Len(resp.Reports, 1)
   213  
   214  	expected, err := os.ReadFile("testdata/fixtures/series_labels_by.json")
   215  	s.Require().NoError(err)
   216  	actual, _ := json.Marshal(resp.Reports[0].SeriesLabels)
   217  	s.Assert().JSONEq(string(expected), string(actual))
   218  }
   219  
   220  func (s *testSuite) Test_SeriesLabels() {
   221  	selector := `{service_name="pyroscope"}`
   222  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   223  		EndTime:       time.Now().UnixMilli(),
   224  		LabelSelector: selector,
   225  		QueryPlan:     s.plan,
   226  		Query: []*queryv1.Query{{
   227  			QueryType:    queryv1.QueryType_QUERY_SERIES_LABELS,
   228  			SeriesLabels: &queryv1.SeriesLabelsQuery{},
   229  		}},
   230  		Tenant: s.tenant,
   231  	})
   232  
   233  	s.Require().NoError(err)
   234  	s.Require().NotNil(resp)
   235  	s.Require().Len(resp.Reports, 1)
   236  
   237  	expected, err := os.ReadFile("testdata/fixtures/series_labels.json")
   238  	s.Require().NoError(err)
   239  	actual, _ := json.Marshal(resp.Reports[0].SeriesLabels)
   240  	s.Assert().JSONEq(string(expected), string(actual))
   241  }
   242  
   243  var startTime = time.Unix(1739263329, 0)
   244  
   245  func (s *testSuite) Test_QueryTimeSeries() {
   246  	query := &queryv1.Query{
   247  		QueryType: queryv1.QueryType_QUERY_TIME_SERIES,
   248  		TimeSeries: &queryv1.TimeSeriesQuery{
   249  			GroupBy: []string{"service_name"},
   250  			Step:    30.0,
   251  		},
   252  	}
   253  
   254  	req := &queryv1.InvokeRequest{
   255  		StartTime:     startTime.UnixMilli(),
   256  		EndTime:       startTime.Add(time.Hour).UnixMilli(),
   257  		Query:         []*queryv1.Query{query},
   258  		QueryPlan:     s.plan,
   259  		LabelSelector: "{}",
   260  		Tenant:        s.tenant,
   261  	}
   262  
   263  	resp, err := s.reader.Invoke(s.ctx, req)
   264  	s.Require().NoError(err)
   265  	s.Require().NotNil(resp)
   266  	s.Require().Len(resp.Reports, 1)
   267  	s.Require().NotNil(resp.Reports[0].TimeSeries)
   268  
   269  	actual, _ := json.Marshal(resp.Reports[0].TimeSeries.TimeSeries)
   270  	expected, err := os.ReadFile("testdata/fixtures/time_series.json")
   271  	s.Require().NoError(err)
   272  	s.Assert().JSONEq(string(expected), string(actual))
   273  }
   274  
   275  // When there is only one report we don't run the aggregate method. This check ensures that the timeseries, is still correctly formatted.
   276  func (s *testSuite) Test_QueryTimeSeriesOneReport() {
   277  	query := &queryv1.Query{
   278  		QueryType: queryv1.QueryType_QUERY_TIME_SERIES,
   279  		TimeSeries: &queryv1.TimeSeriesQuery{
   280  			GroupBy: []string{"service_name"},
   281  			Step:    30.0,
   282  		},
   283  	}
   284  
   285  	// shorten plan so there is only one report
   286  	shorterPlan := s.plan.CloneVT()
   287  	shorterPlan.Root = s.plan.Root.CloneVT()
   288  	shorterPlan.Root.Blocks = s.plan.Root.Blocks[:1]
   289  
   290  	req := &queryv1.InvokeRequest{
   291  		StartTime:     startTime.UnixMilli(),
   292  		EndTime:       startTime.Add(time.Hour).UnixMilli(),
   293  		Query:         []*queryv1.Query{query},
   294  		QueryPlan:     shorterPlan,
   295  		LabelSelector: "{}",
   296  		Tenant:        s.tenant,
   297  	}
   298  
   299  	resp, err := s.reader.Invoke(s.ctx, req)
   300  	s.Require().NoError(err)
   301  	s.Require().NotNil(resp)
   302  	s.Require().Len(resp.Reports, 1)
   303  	s.Require().NotNil(resp.Reports[0].TimeSeries)
   304  
   305  	actual, _ := json.Marshal(resp.Reports[0].TimeSeries.TimeSeries)
   306  	expected, err := os.ReadFile("testdata/fixtures/time_series_first_block.json")
   307  	s.Require().NoError(err)
   308  	s.Assert().JSONEq(string(expected), string(actual))
   309  }
   310  
   311  func (s *testSuite) Test_QueryTree_All_Tenant_Isolation() {
   312  	queryTenant := "some-tenant"
   313  
   314  	s.Require().NotContains(s.tenant, queryTenant)
   315  
   316  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   317  		EndTime:       time.Now().UnixMilli(),
   318  		LabelSelector: "{}",
   319  		QueryPlan:     s.plan,
   320  		Query: []*queryv1.Query{{
   321  			QueryType: queryv1.QueryType_QUERY_TREE,
   322  			Tree:      &queryv1.TreeQuery{MaxNodes: 16},
   323  		}},
   324  		Tenant: []string{queryTenant},
   325  	})
   326  
   327  	s.Require().NoError(err)
   328  	s.Require().NotNil(resp)
   329  	s.Require().Len(resp.Reports, 0)
   330  }
   331  
   332  func (s *testSuite) Test_ProfileIDSelector() {
   333  	// Get a real profile ID for valid test case
   334  	validProfileID := s.getProfileIDFromExemplars(s.T())
   335  
   336  	// Load baseline fixture for tree comparison
   337  	baselineTree, err := os.ReadFile("testdata/fixtures/tree_16.txt")
   338  	s.Require().NoError(err)
   339  
   340  	// Get baseline tree for comparison
   341  	allTreeResp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   342  		EndTime:       time.Now().UnixMilli(),
   343  		LabelSelector: "{}",
   344  		QueryPlan:     s.plan,
   345  		Query: []*queryv1.Query{{
   346  			QueryType: queryv1.QueryType_QUERY_TREE,
   347  			Tree:      &queryv1.TreeQuery{MaxNodes: 16},
   348  		}},
   349  		Tenant: s.tenant,
   350  	})
   351  	s.Require().NoError(err)
   352  	allTree, err := phlaremodel.UnmarshalTree(allTreeResp.Reports[0].Tree.Tree)
   353  	s.Require().NoError(err)
   354  
   355  	// Get baseline pprof for comparison
   356  	allPprofResp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   357  		StartTime:     startTime.UnixMilli(),
   358  		EndTime:       startTime.Add(5 * time.Minute).UnixMilli(),
   359  		LabelSelector: "{}",
   360  		QueryPlan:     s.plan,
   361  		Query: []*queryv1.Query{{
   362  			QueryType: queryv1.QueryType_QUERY_PPROF,
   363  			Pprof:     &queryv1.PprofQuery{},
   364  		}},
   365  		Tenant: s.tenant,
   366  	})
   367  	s.Require().NoError(err)
   368  	var allProfile profilev1.Profile
   369  	err = pprof.Unmarshal(allPprofResp.Reports[0].Pprof.Pprof, &allProfile)
   370  	s.Require().NoError(err)
   371  
   372  	tests := []struct {
   373  		queryType         queryv1.QueryType
   374  		name              string
   375  		profileIDSelector []string
   376  		wantErr           bool
   377  		expectBaseline    bool
   378  		expectFiltered    bool
   379  		expectEmpty       bool
   380  	}{
   381  		// Tree query tests
   382  		{queryv1.QueryType_QUERY_TREE, "tree/invalid UUID returns error", []string{"invalid-uuid"}, true, false, false, false},
   383  		{queryv1.QueryType_QUERY_TREE, "tree/empty selector returns baseline", []string{}, false, true, false, false},
   384  		{queryv1.QueryType_QUERY_TREE, "tree/nil selector returns baseline", nil, false, true, false, false},
   385  		{queryv1.QueryType_QUERY_TREE, "tree/non-existent UUID returns empty result", []string{"00000000-0000-0000-0000-000000000000"}, false, false, false, true},
   386  		{queryv1.QueryType_QUERY_TREE, "tree/valid UUID filters to single profile", []string{validProfileID}, false, false, true, false},
   387  
   388  		// Pprof query tests
   389  		{queryv1.QueryType_QUERY_PPROF, "pprof/invalid UUID returns error", []string{"not-a-uuid"}, true, false, false, false},
   390  		{queryv1.QueryType_QUERY_PPROF, "pprof/empty selector returns baseline", []string{}, false, true, false, false},
   391  		{queryv1.QueryType_QUERY_PPROF, "pprof/nil selector returns baseline", nil, false, true, false, false},
   392  		{queryv1.QueryType_QUERY_PPROF, "pprof/non-existent UUID returns empty result", []string{"00000000-0000-0000-0000-000000000000"}, false, false, false, true},
   393  		{queryv1.QueryType_QUERY_PPROF, "pprof/valid UUID filters to single profile", []string{validProfileID}, false, false, true, false},
   394  	}
   395  
   396  	for _, tt := range tests {
   397  		s.Run(tt.name, func() {
   398  			var query *queryv1.Query
   399  			var reqStartTime, reqEndTime int64
   400  
   401  			if tt.queryType == queryv1.QueryType_QUERY_TREE {
   402  				reqEndTime = time.Now().UnixMilli()
   403  				query = &queryv1.Query{
   404  					QueryType: queryv1.QueryType_QUERY_TREE,
   405  					Tree: &queryv1.TreeQuery{
   406  						MaxNodes:          16,
   407  						ProfileIdSelector: tt.profileIDSelector,
   408  					},
   409  				}
   410  			} else {
   411  				reqStartTime = startTime.UnixMilli()
   412  				reqEndTime = startTime.Add(5 * time.Minute).UnixMilli()
   413  				query = &queryv1.Query{
   414  					QueryType: queryv1.QueryType_QUERY_PPROF,
   415  					Pprof: &queryv1.PprofQuery{
   416  						ProfileIdSelector: tt.profileIDSelector,
   417  					},
   418  				}
   419  			}
   420  
   421  			resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   422  				StartTime:     reqStartTime,
   423  				EndTime:       reqEndTime,
   424  				LabelSelector: "{}",
   425  				QueryPlan:     s.plan,
   426  				Query:         []*queryv1.Query{query},
   427  				Tenant:        s.tenant,
   428  			})
   429  
   430  			if tt.wantErr {
   431  				s.Require().Error(err)
   432  				s.Require().Nil(resp)
   433  				return
   434  			}
   435  
   436  			s.Require().NoError(err)
   437  			s.Require().NotNil(resp)
   438  			s.Require().Len(resp.Reports, 1)
   439  
   440  			if tt.queryType == queryv1.QueryType_QUERY_TREE {
   441  				tree, err := phlaremodel.UnmarshalTree(resp.Reports[0].Tree.Tree)
   442  				s.Require().NoError(err)
   443  
   444  				if tt.expectBaseline {
   445  					s.Assert().Equal(string(baselineTree), tree.String())
   446  				}
   447  				if tt.expectEmpty {
   448  					s.Assert().Zero(tree.Total())
   449  				}
   450  				if tt.expectFiltered {
   451  					s.Assert().Less(tree.Total(), allTree.Total())
   452  					s.Assert().NotZero(tree.Total())
   453  				}
   454  			} else {
   455  				var profile profilev1.Profile
   456  				err = pprof.Unmarshal(resp.Reports[0].Pprof.Pprof, &profile)
   457  				s.Require().NoError(err)
   458  
   459  				if tt.expectBaseline {
   460  					s.Assert().Equal(len(allProfile.Sample), len(profile.Sample))
   461  				}
   462  				if tt.expectEmpty {
   463  					s.Assert().Zero(len(profile.Sample))
   464  				}
   465  				if tt.expectFiltered {
   466  					s.Assert().Less(len(profile.Sample), len(allProfile.Sample))
   467  					s.Assert().NotZero(len(profile.Sample))
   468  				}
   469  			}
   470  		})
   471  	}
   472  }
   473  
   474  func (s *testSuite) getProfileIDFromExemplars(t *testing.T) string {
   475  	t.Helper()
   476  
   477  	resp, err := s.reader.Invoke(s.ctx, &queryv1.InvokeRequest{
   478  		StartTime:     startTime.UnixMilli(),
   479  		EndTime:       startTime.Add(5 * time.Minute).UnixMilli(),
   480  		LabelSelector: "{}",
   481  		QueryPlan:     s.plan,
   482  		Query: []*queryv1.Query{{
   483  			QueryType: queryv1.QueryType_QUERY_TIME_SERIES,
   484  			TimeSeries: &queryv1.TimeSeriesQuery{
   485  				Step:         30.0,
   486  				ExemplarType: typesv1.ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL,
   487  			},
   488  		}},
   489  		Tenant: s.tenant,
   490  	})
   491  	s.Require().NoError(err)
   492  	s.Require().NotNil(resp)
   493  
   494  	// Find first exemplar with a profile ID
   495  	for _, serie := range resp.Reports[0].TimeSeries.TimeSeries {
   496  		for _, point := range serie.Points {
   497  			if len(point.Exemplars) > 0 && point.Exemplars[0].ProfileId != "" {
   498  				return point.Exemplars[0].ProfileId
   499  			}
   500  		}
   501  	}
   502  	s.Require().FailNow("no profile ID found in exemplars")
   503  	return ""
   504  }