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 }