Skip to content

Commit 7b31fc1

Browse files
committed
fix internal link
Signed-off-by: Patrik Cyvoct <patrik@ptrk.io>
1 parent b076156 commit 7b31fc1

13 files changed

Lines changed: 390 additions & 12 deletions

pkg/quickwit/data_query.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"regexp"
66
"strconv"
7+
"strings"
78

89
es "github.com/quickwit-oss/quickwit-datasource/pkg/quickwit/client"
910
"github.com/quickwit-oss/quickwit-datasource/pkg/quickwit/simplejson"
@@ -329,13 +330,79 @@ func isRawDocumentQuery(query *Query) bool {
329330
return queryMetricType(query) == rawDocumentType
330331
}
331332

332-
func queryMetricType(query *Query) string {
333+
var (
334+
bareTraceIDPattern = regexp.MustCompile(`^[0-9a-fA-F]{32}$`)
335+
traceIDFieldPattern = regexp.MustCompile(`(?i)^(trace_id|traceID|traceId)\s*:\s*"?[0-9a-f]{32}"?\s*$`)
336+
)
337+
338+
func firstMetricType(query *Query) string {
333339
if query == nil || len(query.Metrics) == 0 {
334340
return ""
335341
}
336342
return query.Metrics[0].Type
337343
}
338344

345+
func queryMetricType(query *Query) string {
346+
return firstMetricType(query)
347+
}
348+
349+
func isBareTraceIDQuery(rawQuery string) bool {
350+
return bareTraceIDPattern.MatchString(strings.TrimSpace(rawQuery))
351+
}
352+
353+
func isTraceIDFieldQuery(rawQuery string) bool {
354+
return traceIDFieldPattern.MatchString(strings.TrimSpace(rawQuery))
355+
}
356+
357+
func canInferTraceLink(query *Query) bool {
358+
firstMetricType := firstMetricType(query)
359+
return firstMetricType == "" || firstMetricType == logsType || firstMetricType == tracesType
360+
}
361+
362+
func normalizeInternalLinkTraceQuery(query *Query) {
363+
if query == nil {
364+
return
365+
}
366+
367+
if query.QueryType != tracesType &&
368+
firstMetricType(query) != tracesType &&
369+
!(canInferTraceLink(query) && isBareTraceIDQuery(query.RawQuery)) &&
370+
!(firstMetricType(query) == logsType && isTraceIDFieldQuery(query.RawQuery)) {
371+
return
372+
}
373+
374+
rawQuery := strings.TrimSpace(query.RawQuery)
375+
if isBareTraceIDQuery(rawQuery) {
376+
query.RawQuery = "trace_id:" + rawQuery
377+
}
378+
query.QueryType = tracesType
379+
query.BucketAggs = []*BucketAgg{}
380+
381+
if len(query.Metrics) == 0 {
382+
query.Metrics = []*MetricAgg{
383+
{
384+
ID: "1",
385+
Type: tracesType,
386+
Settings: simplejson.NewFromAny(map[string]interface{}{"limit": "1000"}),
387+
Meta: simplejson.New(),
388+
},
389+
}
390+
return
391+
}
392+
393+
query.Metrics = query.Metrics[:1]
394+
query.Metrics[0].Type = tracesType
395+
if query.Metrics[0].ID == "" {
396+
query.Metrics[0].ID = "1"
397+
}
398+
if query.Metrics[0].Settings == nil {
399+
query.Metrics[0].Settings = simplejson.New()
400+
}
401+
if query.Metrics[0].Settings.Get("limit").MustString() == "" {
402+
query.Metrics[0].Settings.Set("limit", "1000")
403+
}
404+
}
405+
339406
func processLogsQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) {
340407
metric := q.Metrics[0]
341408
sort := es.SortOrderDesc

pkg/quickwit/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
// Query represents the time series query model of the datasource
1010
type Query struct {
1111
RawQuery string `json:"query"`
12+
QueryType string `json:"queryType"`
1213
BucketAggs []*BucketAgg `json:"bucketAggs"`
1314
Metrics []*MetricAgg `json:"metrics"`
1415
Alias string `json:"alias"`

pkg/quickwit/parse_query.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func parseQuery(tsdbQuery []backend.DataQuery) ([]*Query, error) {
2020
// please do not create a new field with that name, to avoid potential problems with old, persisted queries.
2121

2222
rawQuery := model.Get("query").MustString()
23+
queryType := model.Get("queryType").MustString()
2324
bucketAggs, err := parseBucketAggs(model)
2425
if err != nil {
2526
return nil, err
@@ -35,8 +36,9 @@ func parseQuery(tsdbQuery []backend.DataQuery) ([]*Query, error) {
3536
from := q.TimeRange.From.UnixNano() / int64(time.Millisecond)
3637
to := q.TimeRange.To.UnixNano() / int64(time.Millisecond)
3738

38-
queries = append(queries, &Query{
39+
query := &Query{
3940
RawQuery: rawQuery,
41+
QueryType: queryType,
4042
BucketAggs: bucketAggs,
4143
Metrics: metrics,
4244
Alias: alias,
@@ -46,7 +48,9 @@ func parseQuery(tsdbQuery []backend.DataQuery) ([]*Query, error) {
4648
MaxDataPoints: q.MaxDataPoints,
4749
RangeFrom: from,
4850
RangeTo: to,
49-
})
51+
}
52+
normalizeInternalLinkTraceQuery(query)
53+
queries = append(queries, query)
5054
}
5155

5256
return queries, nil

pkg/quickwit/parse_query_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,92 @@ func TestParseQuery(t *testing.T) {
101101
require.Equal(t, q.BucketAggs[1].Settings.Get("min_doc_count").MustInt(), 0)
102102
require.Equal(t, q.BucketAggs[1].Settings.Get("trimEdges").MustInt(), 0)
103103
})
104+
105+
t.Run("Should normalize exemplar-style bare trace id links", func(t *testing.T) {
106+
traceID := "75d7a6e5c07de26e0238cd17a281a190"
107+
body := `{
108+
"query": "` + traceID + `",
109+
"queryType": "traces",
110+
"metrics": [{ "type": "logs", "id": "3", "settings": { "limit": "100" } }],
111+
"bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }]
112+
}`
113+
dataQuery, err := newDataQuery(body)
114+
require.NoError(t, err)
115+
queries, err := parseQuery(dataQuery.Queries)
116+
require.NoError(t, err)
117+
require.Len(t, queries, 1)
118+
119+
q := queries[0]
120+
require.Equal(t, "trace_id:"+traceID, q.RawQuery)
121+
require.Equal(t, tracesType, q.QueryType)
122+
require.Len(t, q.Metrics, 1)
123+
require.Equal(t, tracesType, q.Metrics[0].Type)
124+
require.Equal(t, "3", q.Metrics[0].ID)
125+
require.Equal(t, "100", q.Metrics[0].Settings.Get("limit").MustString())
126+
require.Empty(t, q.BucketAggs)
127+
})
128+
129+
t.Run("Should create a traces metric for bare trace id links without metrics", func(t *testing.T) {
130+
traceID := "75d7a6e5c07de26e0238cd17a281a190"
131+
body := `{
132+
"query": "` + traceID + `",
133+
"metrics": [],
134+
"bucketAggs": []
135+
}`
136+
dataQuery, err := newDataQuery(body)
137+
require.NoError(t, err)
138+
queries, err := parseQuery(dataQuery.Queries)
139+
require.NoError(t, err)
140+
require.Len(t, queries, 1)
141+
142+
q := queries[0]
143+
require.Equal(t, "trace_id:"+traceID, q.RawQuery)
144+
require.Equal(t, tracesType, q.QueryType)
145+
require.Len(t, q.Metrics, 1)
146+
require.Equal(t, tracesType, q.Metrics[0].Type)
147+
require.Equal(t, "1", q.Metrics[0].ID)
148+
require.Equal(t, "1000", q.Metrics[0].Settings.Get("limit").MustString())
149+
})
150+
151+
t.Run("Should not normalize regular metric queries with bare hex filters", func(t *testing.T) {
152+
traceID := "75d7a6e5c07de26e0238cd17a281a190"
153+
body := `{
154+
"query": "` + traceID + `",
155+
"metrics": [{ "type": "count", "id": "1" }],
156+
"bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }]
157+
}`
158+
dataQuery, err := newDataQuery(body)
159+
require.NoError(t, err)
160+
queries, err := parseQuery(dataQuery.Queries)
161+
require.NoError(t, err)
162+
require.Len(t, queries, 1)
163+
164+
q := queries[0]
165+
require.Equal(t, traceID, q.RawQuery)
166+
require.Empty(t, q.QueryType)
167+
require.Len(t, q.Metrics, 1)
168+
require.Equal(t, countType, q.Metrics[0].Type)
169+
require.Len(t, q.BucketAggs, 1)
170+
})
171+
172+
t.Run("Should not normalize span-specific log correlation queries", func(t *testing.T) {
173+
traceID := "75d7a6e5c07de26e0238cd17a281a190"
174+
body := `{
175+
"query": "trace_id:` + traceID + ` AND span_id:cccccccccccccccc",
176+
"metrics": [{ "type": "logs", "id": "3", "settings": { "limit": "100" } }],
177+
"bucketAggs": []
178+
}`
179+
dataQuery, err := newDataQuery(body)
180+
require.NoError(t, err)
181+
queries, err := parseQuery(dataQuery.Queries)
182+
require.NoError(t, err)
183+
require.Len(t, queries, 1)
184+
185+
q := queries[0]
186+
require.Equal(t, "trace_id:"+traceID+" AND span_id:cccccccccccccccc", q.RawQuery)
187+
require.Empty(t, q.QueryType)
188+
require.Len(t, q.Metrics, 1)
189+
require.Equal(t, logsType, q.Metrics[0].Type)
190+
})
104191
})
105192
}

pkg/quickwit/response_parser_traces.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import (
1717
)
1818

1919
const (
20-
tracesType = "traces"
21-
traceSearchType = "trace_search"
20+
tracesType = "traces"
21+
traceSearchType = "trace_search"
22+
quickwitPluginID = "quickwit-quickwit-datasource"
2223

2324
maxTraceStackTraceBytes = 64 * 1024
2425
traceStackTraceTruncatedText = "\n... truncated"
@@ -1005,6 +1006,9 @@ func traceInternalDataLinks(title string, datasourceUID string, datasourceName s
10051006
Query: map[string]interface{}{
10061007
"refId": "A",
10071008
"query": query,
1009+
"queryType": metricType,
1010+
"datasource": map[string]string{"type": quickwitPluginID, "uid": datasourceUID},
1011+
"filters": []interface{}{},
10081012
"bucketAggs": []interface{}{},
10091013
"metrics": []map[string]interface{}{
10101014
{

pkg/quickwit/response_parser_traces_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@ func TestTraceDataLinks(t *testing.T) {
441441

442442
query := link.Internal.Query.(map[string]interface{})
443443
require.Equal(t, "trace_id:${__span.traceId} AND span_id:${__span.spanId}", query["query"])
444+
require.Equal(t, logsType, query["queryType"])
445+
require.Equal(t, map[string]string{"type": quickwitPluginID, "uid": "logs-uid"}, query["datasource"])
444446
require.Equal(t, logsType, query["metrics"].([]map[string]interface{})[0]["type"])
445447
})
446448

@@ -495,6 +497,8 @@ func TestTraceDataLinks(t *testing.T) {
495497

496498
query := link.Internal.Query.(map[string]interface{})
497499
require.Equal(t, "trace_id:${__value.raw}", query["query"])
500+
require.Equal(t, tracesType, query["queryType"])
501+
require.Equal(t, map[string]string{"type": quickwitPluginID, "uid": "traces-uid"}, query["datasource"])
498502
require.Equal(t, tracesType, query["metrics"].([]map[string]interface{})[0]["type"])
499503
})
500504
}

src/components/QueryEditor/index.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { render, screen, waitFor } from '@testing-library/react';
22
import React from 'react';
33

4+
import { CoreApp } from '@grafana/data';
5+
46
import { ElasticDatasource } from '@/datasource';
57
import { ElasticsearchQuery } from '@/types';
68

@@ -46,4 +48,36 @@ describe('QueryEditor', () => {
4648

4749
expect(screen.getByText('Group By')).toBeInTheDocument();
4850
});
51+
52+
it('normalizes exemplar-style trace links before rendering', async () => {
53+
const traceId = '75d7a6e5c07de26e0238cd17a281a190';
54+
const onChange = jest.fn();
55+
const query: ElasticsearchQuery = {
56+
refId: 'A',
57+
query: traceId,
58+
metrics: [{ id: '3', type: 'logs', settings: { limit: '100' } }],
59+
bucketAggs: [],
60+
filters: [],
61+
};
62+
63+
render(
64+
<QueryEditor
65+
query={query}
66+
datasource={{} as ElasticDatasource}
67+
onChange={onChange}
68+
onRunQuery={noop}
69+
app={CoreApp.Explore}
70+
/>
71+
);
72+
73+
await waitFor(() =>
74+
expect(onChange).toHaveBeenCalledWith(
75+
expect.objectContaining({
76+
query: `trace_id:${traceId}`,
77+
metrics: [expect.objectContaining({ id: '3', type: 'traces', settings: { limit: '100' } })],
78+
bucketAggs: [],
79+
})
80+
)
81+
);
82+
});
4983
});

src/components/QueryEditor/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { css } from '@emotion/css';
22

3-
import React, { createContext, useRef } from 'react';
3+
import React, { createContext, useEffect, useRef } from 'react';
44
import { debounceTime, throttleTime } from 'rxjs';
55
import { useObservableCallback, useSubscription } from 'observable-hooks'
66

@@ -26,20 +26,29 @@ import { getHook } from '@/utils/context';
2626
import { LuceneQueryEditor } from '@/components/LuceneQueryEditor';
2727
import { useDatasourceFields } from '@/datasource/utils';
2828
import { FilterEditor } from '@/components/QueryEditor/FilterEditor';
29+
import { normalizeInternalLinkQuery } from '@/queryModel';
2930

3031
export type ElasticQueryEditorProps = QueryEditorProps<ElasticDatasource, ElasticsearchQuery, QuickwitOptions>;
3132

3233
export const QueryEditor = ({ query, onChange, onRunQuery, datasource, range, app }: ElasticQueryEditorProps) => {
34+
const normalizedQuery = normalizeInternalLinkQuery(query);
35+
36+
useEffect(() => {
37+
if (normalizedQuery !== query) {
38+
onChange(normalizedQuery);
39+
}
40+
}, [normalizedQuery, onChange, query]);
41+
3342
return (
3443
<ElasticsearchProvider
3544
datasource={datasource}
3645
onChange={onChange}
3746
app={app || CoreApp.Unknown}
3847
onRunQuery={onRunQuery}
39-
query={query}
48+
query={normalizedQuery}
4049
range={range || getDefaultTimeRange()}
4150
>
42-
<QueryEditorForm value={query} onRunQuery={onRunQuery} />
51+
<QueryEditorForm value={normalizedQuery} onRunQuery={onRunQuery} />
4352
</ElasticsearchProvider>
4453
);
4554
};

src/datasource/base.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import ElasticsearchLanguageProvider from 'LanguageProvider';
2929
import { fieldTypeMap, hasWhiteSpace, isSimpleToken } from 'utils';
3030
import { addAddHocFilter } from 'modifyQuery';
3131
import { getQueryResponseProcessor } from 'datasource/processResponse';
32+
import { normalizeInternalLinkQuery } from '@/queryModel';
3233

3334
import { SECOND } from 'utils/time';
3435
import { GConstructor } from 'utils/mixins';
@@ -99,8 +100,12 @@ export class BaseQuickwitDataSource
99100
}
100101

101102
query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> {
102-
const queryProcessor = getQueryResponseProcessor(this, request);
103-
return super.query(request).pipe(map(queryProcessor.processResponse));
103+
const normalizedRequest = {
104+
...request,
105+
targets: request.targets.map(normalizeInternalLinkQuery),
106+
};
107+
const queryProcessor = getQueryResponseProcessor(this, normalizedRequest);
108+
return super.query(normalizedRequest).pipe(map(queryProcessor.processResponse));
104109
}
105110

106111
/**
@@ -475,7 +480,7 @@ export class BaseQuickwitDataSource
475480
};
476481

477482
const finalQuery = JSON.parse(this.templateSrv.replace(JSON.stringify(expandedQuery), scopedVars));
478-
return finalQuery;
483+
return normalizeInternalLinkQuery(finalQuery);
479484
}
480485

481486
addAdHocFilters(query: string, adhocFilters?: AdHocVariableFilter[]) {

src/datasource/processResponse.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ describe('processLogsDataFrame', () => {
260260
expect(traceIDField?.config.links?.[0].internal?.datasourceUid).toBe('traces-uid');
261261
expect(traceIDField?.config.links?.[0].internal?.datasourceName).toBe('Quickwit Traces');
262262
expect((traceIDField?.config.links?.[0].internal?.query as any).query).toBe('trace_id:${__value.raw}');
263+
expect((traceIDField?.config.links?.[0].internal?.query as any).queryType).toBe('traces');
264+
expect((traceIDField?.config.links?.[0].internal?.query as any).datasource).toEqual({
265+
type: 'quickwit-quickwit-datasource',
266+
uid: 'traces-uid',
267+
});
263268
expect((traceIDField?.config.links?.[0].internal?.query as any).metrics[0].type).toBe('traces');
264269
});
265270

0 commit comments

Comments
 (0)