github.com/grafana/pyroscope@v1.18.0/pkg/frontend/readpath/queryfrontend/query_frontend_test.go (about) 1 package queryfrontend 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "connectrpc.com/connect" 10 "github.com/go-kit/log" 11 "github.com/grafana/dskit/user" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 16 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 17 metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1" 18 querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 19 queryv1 "github.com/grafana/pyroscope/api/gen/proto/go/query/v1" 20 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 21 "github.com/grafana/pyroscope/pkg/block/metadata" 22 "github.com/grafana/pyroscope/pkg/featureflags" 23 "github.com/grafana/pyroscope/pkg/tenant" 24 "github.com/grafana/pyroscope/pkg/test/mocks/mockfrontend" 25 "github.com/grafana/pyroscope/pkg/test/mocks/mockmetastorev1" 26 "github.com/grafana/pyroscope/pkg/test/mocks/mockqueryfrontend" 27 ) 28 29 func Test_QueryFrontend_QueryMetadata(t *testing.T) { 30 for _, test := range []struct { 31 query *queryv1.QueryRequest 32 request *metastorev1.QueryMetadataRequest 33 response *metastorev1.QueryMetadataResponse 34 }{ 35 { 36 query: &queryv1.QueryRequest{LabelSelector: `{service_name="service-a"}`}, 37 request: &metastorev1.QueryMetadataRequest{ 38 TenantId: []string{"org"}, 39 Query: `{service_name="service-a"}`, 40 Labels: []string{metadata.LabelNameUnsymbolized}, 41 }, 42 response: &metastorev1.QueryMetadataResponse{ 43 Blocks: []*metastorev1.BlockMeta{{Id: "block_id_a"}}, 44 }, 45 }, 46 { 47 query: &queryv1.QueryRequest{LabelSelector: `{service_name!="service-a"}`}, 48 request: &metastorev1.QueryMetadataRequest{ 49 TenantId: []string{"org"}, 50 Query: `{__tenant_dataset__="dataset_tsdb_index"}`, 51 Labels: []string{metadata.LabelNameUnsymbolized, "__tenant_dataset__"}, 52 }, 53 response: &metastorev1.QueryMetadataResponse{ 54 Blocks: []*metastorev1.BlockMeta{{Id: "block_id_a"}}, 55 }, 56 }, 57 { 58 query: &queryv1.QueryRequest{LabelSelector: `{service_name=~".*"}`}, 59 request: &metastorev1.QueryMetadataRequest{ 60 TenantId: []string{"org"}, 61 Query: `{__tenant_dataset__="dataset_tsdb_index"}`, 62 Labels: []string{metadata.LabelNameUnsymbolized, "__tenant_dataset__"}, 63 }, 64 response: &metastorev1.QueryMetadataResponse{ 65 Blocks: []*metastorev1.BlockMeta{{Id: "block_id_c"}}, 66 }, 67 }, 68 { 69 query: &queryv1.QueryRequest{LabelSelector: `{foo="bar"}`}, 70 request: &metastorev1.QueryMetadataRequest{ 71 TenantId: []string{"org"}, 72 Query: `{__tenant_dataset__="dataset_tsdb_index"}`, 73 Labels: []string{metadata.LabelNameUnsymbolized, "__tenant_dataset__"}, 74 }, 75 response: &metastorev1.QueryMetadataResponse{ 76 Blocks: []*metastorev1.BlockMeta{{Id: "block_id_b"}}, 77 }, 78 }, 79 { 80 query: &queryv1.QueryRequest{LabelSelector: "{}"}, 81 request: &metastorev1.QueryMetadataRequest{ 82 TenantId: []string{"org"}, 83 Query: `{__tenant_dataset__="dataset_tsdb_index"}`, 84 Labels: []string{metadata.LabelNameUnsymbolized, "__tenant_dataset__"}, 85 }, 86 response: &metastorev1.QueryMetadataResponse{ 87 Blocks: []*metastorev1.BlockMeta{{Id: "block_id_d"}}, 88 }, 89 }, 90 } { 91 mockMetadataClient := new(mockmetastorev1.MockMetadataQueryServiceClient) 92 ctx := user.InjectOrgID(context.Background(), "org") 93 f := &QueryFrontend{metadataQueryClient: mockMetadataClient} 94 95 mockMetadataClient.On("QueryMetadata", mock.Anything, test.request). 96 Return(test.response, nil). 97 Once() 98 99 blocks, err := f.QueryMetadata(ctx, test.query) 100 assert.NoError(t, err) 101 assert.Equal(t, test.response.Blocks, blocks) 102 } 103 } 104 105 func TestQueryFrontendSymbolization(t *testing.T) { 106 tests := []struct { 107 name string 108 tenantID string 109 symbolizerEnabled bool 110 hasUnsymbolized bool 111 setupMocks func(*mockfrontend.MockLimits, *mockqueryfrontend.MockSymbolizer) 112 }{ 113 { 114 name: "symbolization enabled for tenant with native profiles", 115 tenantID: "tenant1", 116 symbolizerEnabled: true, 117 hasUnsymbolized: true, 118 setupMocks: func(mockLimits *mockfrontend.MockLimits, mockSymbolizer *mockqueryfrontend.MockSymbolizer) { 119 mockLimits.On("SymbolizerEnabled", "tenant1").Return(true) 120 mockLimits.On("QuerySanitizeOnMerge", "tenant1").Return(true) 121 mockSymbolizer.On("SymbolizePprof", mock.Anything, mock.Anything).Return(nil).Once() 122 }, 123 }, 124 { 125 name: "symbolization disabled for tenant", 126 tenantID: "tenant2", 127 symbolizerEnabled: false, 128 hasUnsymbolized: true, 129 setupMocks: func(mockLimits *mockfrontend.MockLimits, mockSymbolizer *mockqueryfrontend.MockSymbolizer) { 130 mockLimits.On("SymbolizerEnabled", "tenant2").Return(false) 131 mockLimits.On("QuerySanitizeOnMerge", "tenant2").Return(true) 132 mockSymbolizer.AssertNotCalled(t, "SymbolizePprof") 133 }, 134 }, 135 { 136 name: "symbolization enabled but no native profiles", 137 tenantID: "tenant3", 138 symbolizerEnabled: true, 139 hasUnsymbolized: false, 140 setupMocks: func(mockLimits *mockfrontend.MockLimits, mockSymbolizer *mockqueryfrontend.MockSymbolizer) { 141 mockLimits.On("SymbolizerEnabled", "tenant3").Return(true) 142 mockLimits.On("QuerySanitizeOnMerge", "tenant3").Return(true) 143 mockSymbolizer.AssertNotCalled(t, "SymbolizePprof") 144 }, 145 }, 146 } 147 148 for _, tt := range tests { 149 t.Run(tt.name, func(t *testing.T) { 150 mockLimits := mockfrontend.NewMockLimits(t) 151 mockSymbolizer := mockqueryfrontend.NewMockSymbolizer(t) 152 tt.setupMocks(mockLimits, mockSymbolizer) 153 154 mockQueryBackend := mockqueryfrontend.NewMockQueryBackend(t) 155 mockQueryBackend.On("Invoke", mock.Anything, mock.Anything).Return(&queryv1.InvokeResponse{ 156 Reports: []*queryv1.Report{ 157 { 158 Pprof: &queryv1.PprofReport{Pprof: createProfile(t)}, 159 }, 160 }, 161 }, nil) 162 163 mockMetadataClient := new(mockmetastorev1.MockMetadataQueryServiceClient) 164 mockMetadataClient.On("QueryMetadata", mock.Anything, mock.Anything). 165 Return(&metastorev1.QueryMetadataResponse{ 166 Blocks: []*metastorev1.BlockMeta{{ 167 Id: "block_id_d", 168 Datasets: []*metastorev1.Dataset{{ 169 Labels: []int32{1, 1, 2}, 170 }}, 171 StringTable: []string{ 172 "", // First string is always empty by convention 173 metadata.LabelNameUnsymbolized, 174 fmt.Sprintf("%v", tt.hasUnsymbolized), 175 }, 176 }}, 177 }, nil). 178 Once() 179 180 qf := NewQueryFrontend( 181 log.NewNopLogger(), 182 mockLimits, 183 mockMetadataClient, 184 nil, 185 mockQueryBackend, 186 mockSymbolizer, 187 ) 188 189 ctx := tenant.InjectTenantID(context.Background(), tt.tenantID) 190 _, err := qf.Query(ctx, &queryv1.QueryRequest{ 191 LabelSelector: `{service_name="test-service"}`, 192 Query: []*queryv1.Query{ 193 { 194 QueryType: queryv1.QueryType_QUERY_PPROF, 195 }, 196 }, 197 }) 198 199 require.NoError(t, err) 200 201 mockMetadataClient.AssertExpectations(t) 202 mockQueryBackend.AssertExpectations(t) 203 }) 204 } 205 } 206 207 func createProfile(t *testing.T) []byte { 208 t.Helper() 209 210 stringTable := []string{ 211 "", 212 "some_label", 213 "some_value", 214 } 215 216 labels := []*profilev1.Label{{ 217 Key: 1, 218 Str: 2, 219 }} 220 221 profile := &profilev1.Profile{ 222 StringTable: stringTable, 223 Sample: []*profilev1.Sample{{ 224 Label: labels, 225 }}, 226 } 227 228 bytes, err := profile.MarshalVT() 229 require.NoError(t, err) 230 return bytes 231 } 232 233 func Test_QueryFrontend_LabelNames_WithFiltering(t *testing.T) { 234 tests := []struct { 235 name string 236 allowUtf8LabelNames bool 237 setCapabilities bool 238 backendLabelNames []string 239 expectedLabelNames []string 240 }{ 241 { 242 name: "UTF8 labels allowed when enabled", 243 allowUtf8LabelNames: true, 244 setCapabilities: true, 245 backendLabelNames: []string{"foo", "bar", "世界"}, 246 expectedLabelNames: []string{"foo", "bar", "世界"}, 247 }, 248 { 249 name: "UTF8 labels filtered when disabled", 250 allowUtf8LabelNames: false, 251 setCapabilities: true, 252 backendLabelNames: []string{"foo", "bar", "世界"}, 253 expectedLabelNames: []string{"foo", "bar"}, 254 }, 255 { 256 name: "invalid labels pass through when UTF8 enabled", 257 allowUtf8LabelNames: true, 258 setCapabilities: true, 259 backendLabelNames: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 260 expectedLabelNames: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 261 }, 262 { 263 name: "invalid labels filtered when UTF8 disabled", 264 allowUtf8LabelNames: false, 265 setCapabilities: true, 266 backendLabelNames: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 267 expectedLabelNames: []string{"valid_name"}, 268 }, 269 { 270 name: "filtering enabled when no capabilities set", 271 setCapabilities: false, 272 backendLabelNames: []string{"valid_name", "123invalid", "世界"}, 273 expectedLabelNames: []string{"valid_name"}, 274 }, 275 { 276 name: "labels with dots pass through", 277 allowUtf8LabelNames: false, 278 setCapabilities: true, 279 backendLabelNames: []string{"service.name", "app.version"}, 280 expectedLabelNames: []string{"service.name", "app.version"}, 281 }, 282 } 283 284 for _, tc := range tests { 285 t.Run(tc.name, func(t *testing.T) { 286 mockQueryBackend := mockqueryfrontend.NewMockQueryBackend(t) 287 mockQueryBackend.On("Invoke", mock.Anything, mock.Anything).Return(&queryv1.InvokeResponse{ 288 Reports: []*queryv1.Report{ 289 { 290 ReportType: queryv1.ReportType_REPORT_LABEL_NAMES, 291 LabelNames: &queryv1.LabelNamesReport{ 292 LabelNames: tc.backendLabelNames, 293 }, 294 }, 295 }, 296 }, nil) 297 298 mockLimits := mockfrontend.NewMockLimits(t) 299 mockLimits.On("MaxQueryLookback", "test-tenant").Return(time.Duration(0)) 300 mockLimits.On("MaxQueryLength", "test-tenant").Return(time.Duration(0)) 301 mockLimits.On("QuerySanitizeOnMerge", "test-tenant").Return(true) 302 mockMetadataClient := new(mockmetastorev1.MockMetadataQueryServiceClient) 303 mockMetadataClient.On("QueryMetadata", mock.Anything, mock.Anything).Return(&metastorev1.QueryMetadataResponse{ 304 Blocks: []*metastorev1.BlockMeta{{Id: "test-block"}}, 305 }, nil) 306 307 qf := NewQueryFrontend( 308 log.NewNopLogger(), 309 mockLimits, 310 mockMetadataClient, 311 nil, 312 mockQueryBackend, 313 nil, 314 ) 315 316 ctx := tenant.InjectTenantID(context.Background(), "test-tenant") 317 if tc.setCapabilities { 318 ctx = featureflags.WithClientCapabilities(ctx, featureflags.ClientCapabilities{ 319 AllowUtf8LabelNames: tc.allowUtf8LabelNames, 320 }) 321 } 322 323 req := connect.NewRequest(&typesv1.LabelNamesRequest{ 324 Start: 1000, 325 End: 2000, 326 }) 327 328 resp, err := qf.LabelNames(ctx, req) 329 require.NoError(t, err) 330 require.Equal(t, tc.expectedLabelNames, resp.Msg.Names) 331 }) 332 } 333 } 334 335 func Test_QueryFrontend_Series_WithLabelNameFiltering(t *testing.T) { 336 tests := []struct { 337 name string 338 allowUtf8LabelNames bool 339 setCapabilities bool 340 requestLabelNames []string 341 backendLabelNames []string // For empty request case 342 expectedQueryRequest []string // What should be passed to backend 343 }{ 344 { 345 name: "all label names pass through when UTF8 enabled", 346 allowUtf8LabelNames: true, 347 setCapabilities: true, 348 requestLabelNames: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 349 expectedQueryRequest: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 350 }, 351 { 352 name: "invalid label names filtered when UTF8 disabled", 353 allowUtf8LabelNames: false, 354 setCapabilities: true, 355 requestLabelNames: []string{"valid_name", "123invalid", "invalid-hyphen", "世界"}, 356 expectedQueryRequest: []string{"valid_name"}, 357 }, 358 { 359 name: "UTF8 labels filtered when UTF8 disabled", 360 allowUtf8LabelNames: false, 361 setCapabilities: true, 362 requestLabelNames: []string{"foo", "bar", "世界", "日本語"}, 363 expectedQueryRequest: []string{"foo", "bar"}, 364 }, 365 { 366 name: "filtering enabled when no capabilities set", 367 setCapabilities: false, 368 requestLabelNames: []string{"foo", "123invalid", "世界"}, 369 expectedQueryRequest: []string{"foo"}, 370 }, 371 { 372 name: "all valid labels pass through", 373 allowUtf8LabelNames: false, 374 setCapabilities: true, 375 requestLabelNames: []string{"foo", "bar", "service_name"}, 376 expectedQueryRequest: []string{"foo", "bar", "service_name"}, 377 }, 378 { 379 name: "labels with dots pass through", 380 allowUtf8LabelNames: false, 381 setCapabilities: true, 382 requestLabelNames: []string{"service.name", "app.version"}, 383 expectedQueryRequest: []string{"service.name", "app.version"}, 384 }, 385 { 386 name: "empty label names with UTF8 disabled queries and filters all labels", 387 allowUtf8LabelNames: false, 388 setCapabilities: true, 389 requestLabelNames: []string{}, 390 backendLabelNames: []string{"foo", "bar", "世界"}, 391 expectedQueryRequest: []string{"foo", "bar"}, 392 }, 393 } 394 395 for _, tc := range tests { 396 t.Run(tc.name, func(t *testing.T) { 397 var capturedLabelNames []string 398 399 mockQueryBackend := mockqueryfrontend.NewMockQueryBackend(t) 400 401 // For empty label names case, we need to mock the LabelNames query first 402 if len(tc.requestLabelNames) == 0 { 403 mockQueryBackend.On("Invoke", mock.Anything, mock.MatchedBy(func(req *queryv1.InvokeRequest) bool { 404 return len(req.Query) > 0 && req.Query[0].QueryType == queryv1.QueryType_QUERY_LABEL_NAMES 405 })).Return(&queryv1.InvokeResponse{ 406 Reports: []*queryv1.Report{ 407 { 408 ReportType: queryv1.ReportType_REPORT_LABEL_NAMES, 409 LabelNames: &queryv1.LabelNamesReport{ 410 LabelNames: tc.backendLabelNames, 411 }, 412 }, 413 }, 414 }, nil).Once() 415 } 416 417 // Mock the Series query specifically 418 mockQueryBackend.On("Invoke", mock.Anything, mock.MatchedBy(func(req *queryv1.InvokeRequest) bool { 419 return len(req.Query) > 0 && req.Query[0].QueryType == queryv1.QueryType_QUERY_SERIES_LABELS 420 })).Run(func(args mock.Arguments) { 421 invReq := args.Get(1).(*queryv1.InvokeRequest) 422 if len(invReq.Query) > 0 && invReq.Query[0].SeriesLabels != nil { 423 capturedLabelNames = invReq.Query[0].SeriesLabels.LabelNames 424 if capturedLabelNames == nil { 425 capturedLabelNames = []string{} 426 } 427 } 428 }).Return(&queryv1.InvokeResponse{ 429 Reports: []*queryv1.Report{ 430 { 431 ReportType: queryv1.ReportType_REPORT_SERIES_LABELS, 432 SeriesLabels: &queryv1.SeriesLabelsReport{ 433 SeriesLabels: []*typesv1.Labels{}, 434 }, 435 }, 436 }, 437 }, nil).Once() 438 439 mockLimits := mockfrontend.NewMockLimits(t) 440 mockLimits.On("MaxQueryLookback", "test-tenant").Return(time.Duration(0)) 441 mockLimits.On("MaxQueryLength", "test-tenant").Return(time.Duration(0)) 442 mockLimits.On("QuerySanitizeOnMerge", "test-tenant").Return(true) 443 mockMetadataClient := new(mockmetastorev1.MockMetadataQueryServiceClient) 444 mockMetadataClient.On("QueryMetadata", mock.Anything, mock.Anything).Return(&metastorev1.QueryMetadataResponse{ 445 Blocks: []*metastorev1.BlockMeta{{Id: "test-block"}}, 446 }, nil) 447 448 qf := NewQueryFrontend( 449 log.NewNopLogger(), 450 mockLimits, 451 mockMetadataClient, 452 nil, 453 mockQueryBackend, 454 nil, 455 ) 456 457 ctx := tenant.InjectTenantID(context.Background(), "test-tenant") 458 if tc.setCapabilities { 459 ctx = featureflags.WithClientCapabilities(ctx, featureflags.ClientCapabilities{ 460 AllowUtf8LabelNames: tc.allowUtf8LabelNames, 461 }) 462 } 463 464 req := connect.NewRequest(&querierv1.SeriesRequest{ 465 Matchers: []string{`{service_name="test"}`}, 466 LabelNames: tc.requestLabelNames, 467 Start: 1000, 468 End: 2000, 469 }) 470 471 _, err := qf.Series(ctx, req) 472 require.NoError(t, err) 473 474 // Verify that the label names were filtered correctly before being sent to backend 475 require.Equal(t, tc.expectedQueryRequest, capturedLabelNames, 476 "Expected label names sent to backend to be %v, but got %v", tc.expectedQueryRequest, capturedLabelNames) 477 }) 478 } 479 }