Skip to content

Commit f609e27

Browse files
fix(querier): Support multi-tenant queries in Patterns API (#19809)
Co-authored-by: Trevor Whitney <[email protected]>
1 parent 87ce325 commit f609e27

File tree

3 files changed

+120
-4
lines changed

3 files changed

+120
-4
lines changed

pkg/querier/http.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,15 +471,23 @@ func (q *QuerierAPI) PatternsHandler(ctx context.Context, req *logproto.QueryPat
471471
}
472472

473473
// Query store for older data by converting to LogQL query
474-
// Only query the store if pattern persistence is enabled for this tenant
474+
// Only query the store if pattern persistence is enabled for at least one tenant
475475
if storeQueryInterval != nil && !q.cfg.QueryIngesterOnly && q.engineV1 != nil {
476-
tenantID, err := tenant.TenantID(ctx)
476+
tenantIDs, err := tenant.TenantIDs(ctx)
477477
if err != nil {
478478
return nil, err
479479
}
480480

481-
// Only query the store if pattern persistence is enabled for this tenant
482-
if q.limits.PatternPersistenceEnabled(tenantID) {
481+
// Only query the store if pattern persistence is enabled for at least one tenant
482+
patternPersistenceEnabled := false
483+
for _, tenantID := range tenantIDs {
484+
if q.limits.PatternPersistenceEnabled(tenantID) {
485+
patternPersistenceEnabled = true
486+
break
487+
}
488+
}
489+
490+
if patternPersistenceEnabled {
483491
g.Go(func() error {
484492
storeReq := *req
485493
storeReq.Start = storeQueryInterval.start

pkg/querier/multi_tenant_querier.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"sort"
77
"strings"
88

9+
"github.com/grafana/loki/v3/pkg/querier/pattern"
910
"github.com/grafana/loki/v3/pkg/querier/plan"
1011
"github.com/grafana/loki/v3/pkg/storage/detected"
1112
"github.com/grafana/loki/v3/pkg/storage/stores/index/seriesvolume"
@@ -289,6 +290,32 @@ func (q *MultiTenantQuerier) Volume(ctx context.Context, req *logproto.VolumeReq
289290
return merged, nil
290291
}
291292

293+
func (q *MultiTenantQuerier) Patterns(ctx context.Context, req *logproto.QueryPatternsRequest) (*logproto.QueryPatternsResponse, error) {
294+
tenantIDs, err := tenant.TenantIDs(ctx)
295+
if err != nil {
296+
return nil, err
297+
}
298+
299+
if len(tenantIDs) == 1 {
300+
return q.Querier.Patterns(ctx, req)
301+
}
302+
303+
responses := make([]*logproto.QueryPatternsResponse, len(tenantIDs))
304+
for i, id := range tenantIDs {
305+
singleContext := user.InjectOrgID(ctx, id)
306+
resp, err := q.Querier.Patterns(singleContext, req)
307+
if err != nil {
308+
return nil, err
309+
}
310+
311+
responses[i] = resp
312+
}
313+
314+
// Merge pattern responses from all tenants
315+
merged := pattern.MergePatternResponses(responses)
316+
return merged, nil
317+
}
318+
292319
func (q *MultiTenantQuerier) DetectedFields(ctx context.Context, req *logproto.DetectedFieldsRequest) (*logproto.DetectedFieldsResponse, error) {
293320
tenantIDs, err := tenant.TenantIDs(ctx)
294321
if err != nil {

pkg/querier/multi_tenant_querier_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,3 +708,84 @@ func TestMultiTenantQuerier_DetectedLabels(t *testing.T) {
708708
})
709709
}
710710
}
711+
712+
func TestMultiTenantQuerierPatterns(t *testing.T) {
713+
now := time.Now()
714+
715+
tests := []struct {
716+
name string
717+
orgID string
718+
expectedCallCount int // number of times underlying Patterns should be called
719+
expectedPatterns []string
720+
}{
721+
{
722+
name: "single tenant",
723+
orgID: "tenant1",
724+
expectedCallCount: 1,
725+
expectedPatterns: []string{"pattern1", "pattern2"},
726+
},
727+
{
728+
name: "multiple tenants",
729+
orgID: "tenant1|tenant2",
730+
expectedCallCount: 2,
731+
expectedPatterns: []string{"pattern1", "pattern2"}, // merged from both tenants
732+
},
733+
{
734+
name: "three tenants",
735+
orgID: "tenant1|tenant2|tenant3",
736+
expectedCallCount: 3,
737+
expectedPatterns: []string{"pattern1", "pattern2"},
738+
},
739+
}
740+
741+
for _, tc := range tests {
742+
t.Run(tc.name, func(t *testing.T) {
743+
querier := newQuerierMock()
744+
745+
// Mock the Patterns method to return a response
746+
querier.On("Patterns", mock.Anything, mock.Anything).Return(&logproto.QueryPatternsResponse{
747+
Series: []*logproto.PatternSeries{
748+
{
749+
Pattern: "pattern1",
750+
Samples: []*logproto.PatternSample{
751+
{Timestamp: 0, Value: 100},
752+
},
753+
},
754+
{
755+
Pattern: "pattern2",
756+
Samples: []*logproto.PatternSample{
757+
{Timestamp: 0, Value: 50},
758+
},
759+
},
760+
},
761+
}, nil)
762+
763+
multiTenantQuerier := NewMultiTenantQuerier(querier, log.NewNopLogger())
764+
ctx := user.InjectOrgID(context.Background(), tc.orgID)
765+
766+
req := &logproto.QueryPatternsRequest{
767+
Query: `{service_name="test"}`,
768+
Start: now.Add(-1 * time.Hour),
769+
End: now,
770+
Step: time.Minute.Milliseconds(),
771+
}
772+
773+
resp, err := multiTenantQuerier.Patterns(ctx, req)
774+
require.NoError(t, err)
775+
require.NotNil(t, resp)
776+
777+
// Verify the underlying Patterns was called the expected number of times
778+
querier.AssertNumberOfCalls(t, "Patterns", tc.expectedCallCount)
779+
780+
// Verify we got the expected patterns
781+
require.Len(t, resp.Series, len(tc.expectedPatterns))
782+
foundPatterns := make(map[string]bool)
783+
for _, series := range resp.Series {
784+
foundPatterns[series.Pattern] = true
785+
}
786+
for _, expectedPattern := range tc.expectedPatterns {
787+
require.True(t, foundPatterns[expectedPattern], "Expected pattern %s not found", expectedPattern)
788+
}
789+
})
790+
}
791+
}

0 commit comments

Comments
 (0)