Skip to content

Commit cb3f2ad

Browse files
feat: capture logs drilldown flag in query samples (#19242)
1 parent 20e04a4 commit cb3f2ad

File tree

6 files changed

+169
-19
lines changed

6 files changed

+169
-19
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- +goose Up
2+
-- Add is_logs_drilldown column to track queries from Logs Drilldown app
3+
4+
ALTER TABLE sampled_queries
5+
ADD COLUMN is_logs_drilldown BOOLEAN DEFAULT FALSE AFTER user;
6+
7+
-- Update existing rows to have FALSE instead of NULL
8+
UPDATE sampled_queries SET is_logs_drilldown = FALSE WHERE is_logs_drilldown IS NULL;
9+
10+
-- Add index for efficient filtering by logs drilldown status
11+
CREATE INDEX idx_sampled_queries_logs_drilldown ON sampled_queries(is_logs_drilldown);
12+
13+
-- +goose Down
14+
-- Remove is_logs_drilldown column and index
15+
16+
DROP INDEX idx_sampled_queries_logs_drilldown ON sampled_queries;
17+
ALTER TABLE sampled_queries DROP COLUMN is_logs_drilldown;

pkg/goldfish/storage_mysql.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func NewMySQLStorage(config StorageConfig, logger log.Logger) (*MySQLStorage, er
7878
func (s *MySQLStorage) StoreQuerySample(ctx context.Context, sample *QuerySample) error {
7979
query := `
8080
INSERT INTO sampled_queries (
81-
correlation_id, tenant_id, user, query, query_type,
81+
correlation_id, tenant_id, user, is_logs_drilldown, query, query_type,
8282
start_time, end_time, step_duration,
8383
cell_a_exec_time_ms, cell_b_exec_time_ms,
8484
cell_a_queue_time_ms, cell_b_queue_time_ms,
@@ -96,7 +96,7 @@ func (s *MySQLStorage) StoreQuerySample(ctx context.Context, sample *QuerySample
9696
cell_a_span_id, cell_b_span_id,
9797
cell_a_used_new_engine, cell_b_used_new_engine,
9898
sampled_at
99-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
99+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
100100
`
101101

102102
// Convert empty span IDs to NULL for database storage
@@ -112,6 +112,7 @@ func (s *MySQLStorage) StoreQuerySample(ctx context.Context, sample *QuerySample
112112
sample.CorrelationID,
113113
sample.TenantID,
114114
sample.User,
115+
sample.IsLogsDrilldown,
115116
sample.Query,
116117
sample.QueryType,
117118
sample.StartTime,
@@ -329,6 +330,12 @@ func buildWhereClause(filter QueryFilter) (string, []any) {
329330
args = append(args, filter.User)
330331
}
331332

333+
// Add logs drilldown filter
334+
if filter.IsLogsDrilldown != nil {
335+
conditions = append(conditions, "is_logs_drilldown = ?")
336+
args = append(args, *filter.IsLogsDrilldown)
337+
}
338+
332339
// Add new engine filter
333340
if filter.UsedNewEngine != nil {
334341
if *filter.UsedNewEngine {

pkg/goldfish/storage_mysql_test.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,32 @@ func TestQueryFilter_BuildWhereClause(t *testing.T) {
6161
expectedWhere: "WHERE cell_a_used_new_engine = 0 AND cell_b_used_new_engine = 0",
6262
expectedArgs: nil,
6363
},
64+
{
65+
name: "logs drilldown filter true",
66+
filter: QueryFilter{
67+
IsLogsDrilldown: boolPtr(true),
68+
},
69+
expectedWhere: "WHERE is_logs_drilldown = ?",
70+
expectedArgs: []any{true},
71+
},
72+
{
73+
name: "logs drilldown filter false",
74+
filter: QueryFilter{
75+
IsLogsDrilldown: boolPtr(false),
76+
},
77+
expectedWhere: "WHERE is_logs_drilldown = ?",
78+
expectedArgs: []any{false},
79+
},
6480
{
6581
name: "all filters combined",
6682
filter: QueryFilter{
67-
Tenant: "tenant-b",
68-
User: "user123",
69-
UsedNewEngine: boolPtr(true),
83+
Tenant: "tenant-b",
84+
User: "user123",
85+
IsLogsDrilldown: boolPtr(true),
86+
UsedNewEngine: boolPtr(true),
7087
},
71-
expectedWhere: "WHERE tenant_id = ? AND user = ? AND (cell_a_used_new_engine = 1 OR cell_b_used_new_engine = 1)",
72-
expectedArgs: []any{"tenant-b", "user123"},
88+
expectedWhere: "WHERE tenant_id = ? AND user = ? AND is_logs_drilldown = ? AND (cell_a_used_new_engine = 1 OR cell_b_used_new_engine = 1)",
89+
expectedArgs: []any{"tenant-b", "user123", true},
7390
},
7491
{
7592
name: "with time range specified",

pkg/goldfish/types.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import (
66

77
// QuerySample represents a sampled query with performance stats from both cells
88
type QuerySample struct {
9-
CorrelationID string `json:"correlationId"`
10-
TenantID string `json:"tenantId"`
11-
User string `json:"user"`
12-
Query string `json:"query"`
13-
QueryType string `json:"queryType"`
14-
StartTime time.Time `json:"startTime"`
15-
EndTime time.Time `json:"endTime"`
16-
Step time.Duration `json:"step"`
9+
CorrelationID string `json:"correlationId"`
10+
TenantID string `json:"tenantId"`
11+
User string `json:"user"`
12+
IsLogsDrilldown bool `json:"isLogsDrilldown"`
13+
Query string `json:"query"`
14+
QueryType string `json:"queryType"`
15+
StartTime time.Time `json:"startTime"`
16+
EndTime time.Time `json:"endTime"`
17+
Step time.Duration `json:"step"`
1718

1819
// Performance statistics instead of raw responses
1920
CellAStats QueryStats `json:"cellAStats"`
@@ -82,8 +83,9 @@ type PerformanceMetrics struct {
8283

8384
// QueryFilter contains filters for querying sampled queries
8485
type QueryFilter struct {
85-
Tenant string
86-
User string
87-
UsedNewEngine *bool // pointer to handle true/false/nil states
88-
From, To time.Time
86+
Tenant string
87+
User string
88+
IsLogsDrilldown *bool // pointer to handle true/false/nil states
89+
UsedNewEngine *bool // pointer to handle true/false/nil states
90+
From, To time.Time
8991
}

tools/querytee/goldfish/manager.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"strconv"
10+
"strings"
1011
"time"
1112

1213
"github.com/go-kit/log"
@@ -16,6 +17,7 @@ import (
1617
"github.com/prometheus/client_golang/prometheus"
1718
"github.com/prometheus/client_golang/prometheus/promauto"
1819

20+
"github.com/grafana/loki/v3/pkg/util/constants"
1921
"github.com/grafana/loki/v3/pkg/util/httpreq"
2022
)
2123

@@ -124,6 +126,7 @@ func (m *Manager) ProcessQueryPair(ctx context.Context, req *http.Request, cellA
124126
CorrelationID: correlationID,
125127
TenantID: extractTenant(req),
126128
User: extractUserFromQueryTags(req, m.logger),
129+
IsLogsDrilldown: isLogsDrilldownRequest(req),
127130
Query: req.URL.Query().Get("query"),
128131
QueryType: getQueryType(req.URL.Path),
129132
StartTime: startTime,
@@ -410,3 +413,21 @@ func extractUserFromQueryTags(req *http.Request, logger log.Logger) string {
410413
level.Debug(logger).Log("goldfish", "user-extraction", "result", unknownUser)
411414
return unknownUser
412415
}
416+
417+
// isLogsDrilldownRequest checks if the request comes from Logs Drilldown by examining the X-Query-Tags header
418+
func isLogsDrilldownRequest(req *http.Request) bool {
419+
tags := httpreq.ExtractQueryTagsFromHTTP(req)
420+
kvs := httpreq.TagsToKeyValues(tags)
421+
422+
// Iterate through key-value pairs (keys at even indices, values at odd)
423+
for i := 0; i < len(kvs); i += 2 {
424+
if i+1 < len(kvs) {
425+
key, keyOK := kvs[i].(string)
426+
value, valueOK := kvs[i+1].(string)
427+
if keyOK && valueOK && key == "source" && strings.EqualFold(value, constants.LogsDrilldownAppName) {
428+
return true
429+
}
430+
}
431+
}
432+
return false
433+
}

tools/querytee/goldfish/manager_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,89 @@ func TestExtractUserFromQueryTags(t *testing.T) {
401401
})
402402
}
403403
}
404+
405+
func TestProcessQueryPair_CapturesLogsDrilldown(t *testing.T) {
406+
tests := []struct {
407+
name string
408+
queryTags string
409+
expectedDrilldown bool
410+
}{
411+
{
412+
name: "query with logs drilldown source first",
413+
queryTags: "source=grafana-lokiexplore-app,user=john.doe",
414+
expectedDrilldown: true,
415+
},
416+
{
417+
name: "query with logs drilldown source last",
418+
queryTags: "user=john.doe,source=grafana-lokiexplore-app",
419+
expectedDrilldown: true,
420+
},
421+
{
422+
name: "query with different source",
423+
queryTags: "source=grafana,user=john.doe",
424+
expectedDrilldown: false,
425+
},
426+
{
427+
name: "query without source",
428+
queryTags: "user=john.doe",
429+
expectedDrilldown: false,
430+
},
431+
{
432+
name: "query without query tags",
433+
queryTags: "",
434+
expectedDrilldown: false,
435+
},
436+
}
437+
438+
for _, tt := range tests {
439+
t.Run(tt.name, func(t *testing.T) {
440+
config := Config{
441+
Enabled: true,
442+
SamplingConfig: SamplingConfig{
443+
DefaultRate: 1.0,
444+
},
445+
}
446+
447+
storage := &mockStorage{}
448+
manager, err := NewManager(config, storage, log.NewNopLogger(), prometheus.NewRegistry())
449+
require.NoError(t, err)
450+
451+
req, _ := http.NewRequest("GET", "/loki/api/v1/query_range?query=count_over_time({job=\"test\"}[5m])&start=1700000000&end=1700001000&step=60s", nil)
452+
req.Header.Set("X-Scope-OrgID", "tenant1")
453+
if tt.queryTags != "" {
454+
req.Header.Set("X-Query-Tags", tt.queryTags)
455+
}
456+
457+
cellAResp := &ResponseData{
458+
Body: []byte(`{"status":"success","data":{"resultType":"matrix","result":[]}}`),
459+
StatusCode: 200,
460+
Duration: 100 * time.Millisecond,
461+
Stats: goldfish.QueryStats{ExecTimeMs: 100},
462+
Hash: "hash123",
463+
Size: 150,
464+
UsedNewEngine: false,
465+
}
466+
467+
cellBResp := &ResponseData{
468+
Body: []byte(`{"status":"success","data":{"resultType":"matrix","result":[]}}`),
469+
StatusCode: 200,
470+
Duration: 120 * time.Millisecond,
471+
Stats: goldfish.QueryStats{ExecTimeMs: 120},
472+
Hash: "hash123",
473+
Size: 155,
474+
UsedNewEngine: false,
475+
}
476+
477+
ctx := context.Background()
478+
manager.ProcessQueryPair(ctx, req, cellAResp, cellBResp)
479+
480+
// Give async processing time to complete
481+
time.Sleep(100 * time.Millisecond)
482+
483+
// Verify sample was stored with correct logs drilldown flag
484+
require.Len(t, storage.samples, 1)
485+
sample := storage.samples[0]
486+
assert.Equal(t, tt.expectedDrilldown, sample.IsLogsDrilldown, "IsLogsDrilldown field should be captured from X-Query-Tags header")
487+
})
488+
}
489+
}

0 commit comments

Comments
 (0)