-
-
-
-
-
-
-
+ {datasource.search?.filters?.map((f) => (
+
+
+
+ ))}
+
',
valueType: 'duration',
@@ -134,7 +154,6 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
filter={
findFilter('max-duration') || {
id: 'max-duration',
- type: 'static',
tag: 'duration',
operator: '<',
valueType: 'duration',
@@ -147,12 +166,12 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts
index e0c48e7a8e8..603cd113f10 100644
--- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts
+++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts
@@ -8,51 +8,49 @@ describe('generateQueryFromFilters generates the correct query for', () => {
});
it('a field without value', () => {
- expect(generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', operator: '=' }])).toBe('{}');
+ expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', operator: '=' }])).toBe('{}');
});
it('a field with value but without tag', () => {
- expect(generateQueryFromFilters([{ id: 'foo', type: 'static', value: 'foovalue', operator: '=' }])).toBe('{}');
+ expect(generateQueryFromFilters([{ id: 'foo', value: 'foovalue', operator: '=' }])).toBe('{}');
});
it('a field with value and tag but without operator', () => {
- expect(generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', value: 'foovalue' }])).toBe('{}');
+ expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}');
});
it('a field with tag, operator and tag', () => {
- expect(
- generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', value: 'foovalue', operator: '=' }])
- ).toBe('{.footag="foovalue"}');
+ expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe(
+ '{.footag="foovalue"}'
+ );
});
it('a field with valueType as integer', () => {
expect(
- generateQueryFromFilters([
- { id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' },
- ])
+ generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }])
).toBe('{.footag>1234}');
});
it('two fields with everything filled in', () => {
expect(
generateQueryFromFilters([
- { id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
- { id: 'bar', type: 'dynamic', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' },
+ { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
+ { id: 'bar', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' },
])
).toBe('{.footag>=1234 && .bartag="barvalue"}');
});
it('two fields but one is missing a value', () => {
expect(
generateQueryFromFilters([
- { id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
- { id: 'bar', type: 'dynamic', tag: 'bartag', operator: '=', valueType: 'string' },
+ { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
+ { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' },
])
).toBe('{.footag>=1234}');
});
it('two fields but one is missing a value and the other a tag', () => {
expect(
generateQueryFromFilters([
- { id: 'foo', type: 'static', value: '1234', operator: '>=', valueType: 'integer' },
- { id: 'bar', type: 'dynamic', tag: 'bartag', operator: '=', valueType: 'string' },
+ { id: 'foo', value: '1234', operator: '>=', valueType: 'integer' },
+ { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' },
])
).toBe('{}');
});
@@ -61,7 +59,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
generateQueryFromFilters([
{
id: 'foo',
- type: 'static',
tag: 'footag',
value: '1234',
operator: '>=',
@@ -76,7 +73,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
generateQueryFromFilters([
{
id: 'foo',
- type: 'static',
tag: 'footag',
value: '1234',
operator: '>=',
@@ -91,7 +87,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
generateQueryFromFilters([
{
id: 'foo',
- type: 'static',
tag: 'footag',
value: '1234',
operator: '>=',
diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts
index 22cc5333e25..a0fcd660123 100644
--- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts
+++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts
@@ -1,3 +1,5 @@
+import { startCase } from 'lodash';
+
import { SelectableValue } from '@grafana/data';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
@@ -19,7 +21,7 @@ const valueHelper = (f: TraceqlFilter) => {
}
return f.value;
};
-export const scopeHelper = (f: TraceqlFilter) => {
+const scopeHelper = (f: TraceqlFilter) => {
// Intrinsic fields don't have a scope
if (CompletionProvider.intrinsics.find((t) => t === f.tag)) {
return '';
@@ -29,6 +31,18 @@ export const scopeHelper = (f: TraceqlFilter) => {
);
};
+export const filterScopedTag = (f: TraceqlFilter) => {
+ return scopeHelper(f) + f.tag;
+};
+
+export const filterTitle = (f: TraceqlFilter) => {
+ // Special case for the intrinsic "name" since a label called "Name" isn't explicit
+ if (f.tag === 'name') {
+ return 'Span Name';
+ }
+ return startCase(filterScopedTag(f));
+};
+
export function replaceAt(array: T[], index: number, value: T) {
const ret = array.slice(0);
ret[index] = value;
diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
index 91622f12bbf..ee127a012b7 100644
--- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
@@ -12,6 +12,7 @@ import { LokiSearchSettings } from './LokiSearchSettings';
import { QuerySettings } from './QuerySettings';
import { SearchSettings } from './SearchSettings';
import { ServiceGraphSettings } from './ServiceGraphSettings';
+import { TraceQLSearchSettings } from './TraceQLSearchSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@@ -48,7 +49,11 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => {
-
+ {config.featureToggles.traceqlSearch ? (
+
+ ) : (
+
+ )}
diff --git a/public/app/plugins/datasource/tempo/configuration/TraceQLSearchSettings.tsx b/public/app/plugins/datasource/tempo/configuration/TraceQLSearchSettings.tsx
new file mode 100644
index 00000000000..0a2ab3cea5f
--- /dev/null
+++ b/public/app/plugins/datasource/tempo/configuration/TraceQLSearchSettings.tsx
@@ -0,0 +1,59 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import useAsync from 'react-use/lib/useAsync';
+
+import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
+import { getDataSourceSrv } from '@grafana/runtime';
+import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui';
+
+import { TempoDatasource } from '../datasource';
+import { TempoJsonData } from '../types';
+
+import { TraceQLSearchTags } from './TraceQLSearchTags';
+
+interface Props extends DataSourcePluginOptionsEditorProps
{}
+
+export function TraceQLSearchSettings({ options, onOptionsChange }: Props) {
+ const dataSourceSrv = getDataSourceSrv();
+ const fetchDatasource = async () => {
+ return (await dataSourceSrv.get({ type: options.type, uid: options.uid })) as TempoDatasource;
+ };
+
+ const { value: datasource } = useAsync(fetchDatasource, [dataSourceSrv, options]);
+
+ return (
+
+
Tempo search
+
+
+ ) =>
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
+ ...options.jsonData.search,
+ hide: event.currentTarget.checked,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = {
+ container: css`
+ label: container;
+ width: 100%;
+ `,
+ row: css`
+ label: row;
+ align-items: baseline;
+ `,
+};
diff --git a/public/app/plugins/datasource/tempo/configuration/TraceQLSearchTags.tsx b/public/app/plugins/datasource/tempo/configuration/TraceQLSearchTags.tsx
new file mode 100644
index 00000000000..b90e61a2783
--- /dev/null
+++ b/public/app/plugins/datasource/tempo/configuration/TraceQLSearchTags.tsx
@@ -0,0 +1,111 @@
+import React, { useCallback, useEffect } from 'react';
+import useAsync from 'react-use/lib/useAsync';
+
+import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
+import { Alert } from '@grafana/ui';
+
+import TagsInput from '../SearchTraceQLEditor/TagsInput';
+import { replaceAt } from '../SearchTraceQLEditor/utils';
+import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
+import { TempoDatasource } from '../datasource';
+import { CompletionProvider } from '../traceql/autocomplete';
+import { TempoJsonData } from '../types';
+
+interface Props extends DataSourcePluginOptionsEditorProps {
+ datasource?: TempoDatasource;
+}
+
+export function TraceQLSearchTags({ options, onOptionsChange, datasource }: Props) {
+ const fetchTags = async () => {
+ if (!datasource) {
+ throw new Error('Unable to retrieve datasource');
+ }
+
+ try {
+ await datasource.languageProvider.start();
+ const tags = datasource.languageProvider.getTags();
+
+ if (tags) {
+ // This is needed because the /api/v2/search/tag/${tag}/values API expects "status" and the v1 API expects "status.code"
+ // so Tempo doesn't send anything and we inject it here for the autocomplete
+ if (!tags.find((t) => t === 'status')) {
+ tags.push('status');
+ }
+ return tags;
+ }
+ } catch (e) {
+ // @ts-ignore
+ throw new Error(`${e.statusText}: ${e.data.error}`);
+ }
+ return [];
+ };
+
+ const { error, loading, value: tags } = useAsync(fetchTags, [datasource, options]);
+
+ const updateFilter = useCallback(
+ (s: TraceqlFilter) => {
+ let copy = options.jsonData.search?.filters;
+ copy ||= [];
+ const indexOfFilter = copy.findIndex((f) => f.id === s.id);
+ if (indexOfFilter >= 0) {
+ // update in place if the filter already exists, for consistency and to avoid UI bugs
+ copy = replaceAt(copy, indexOfFilter, s);
+ } else {
+ copy.push(s);
+ }
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
+ ...options.jsonData.search,
+ filters: copy,
+ });
+ },
+ [onOptionsChange, options]
+ );
+
+ const deleteFilter = (s: TraceqlFilter) => {
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
+ ...options.jsonData.search,
+ filters: options.jsonData.search?.filters?.filter((f) => f.id !== s.id),
+ });
+ };
+
+ useEffect(() => {
+ if (!options.jsonData.search?.filters) {
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
+ ...options.jsonData.search,
+ filters: [
+ {
+ id: 'service-name',
+ tag: 'service.name',
+ operator: '=',
+ scope: TraceqlSearchScope.Resource,
+ },
+ { id: 'span-name', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
+ ],
+ });
+ }
+ }, [onOptionsChange, options]);
+
+ return (
+ <>
+ {datasource ? (
+ {}}
+ tags={[...CompletionProvider.intrinsics, ...(tags || [])]}
+ isTagsLoading={loading}
+ hideValues={true}
+ />
+ ) : (
+ Invalid data source, please create a valid data source and try again
+ )}
+ {error && (
+
+ {error.message}
+
+ )}
+ >
+ );
+}
diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue
index a5c7170fb27..93f6161c0a9 100644
--- a/public/app/plugins/datasource/tempo/dataquery.cue
+++ b/public/app/plugins/datasource/tempo/dataquery.cue
@@ -54,13 +54,10 @@ composableKinds: DataQuery: {
#TempoQueryType: "traceql" | "traceqlSearch" | "search" | "serviceMap" | "upload" | "nativeSearch" | "clear" @cuetsy(kind="type")
// static fields are pre-set in the UI, dynamic fields are added by the user
- #TraceqlSearchFilterType: "static" | "dynamic" @cuetsy(kind="type")
- #TraceqlSearchScope: "unscoped" | "resource" | "span" @cuetsy(kind="enum")
+ #TraceqlSearchScope: "unscoped" | "resource" | "span" @cuetsy(kind="enum")
#TraceqlFilter: {
// Uniquely identify the filter, will not be used in the query generation
id: string
- // The type of the filter, can either be static (pre defined in the UI) or dynamic
- type: #TraceqlSearchFilterType
// The tag for the search filter, for example: .http.status_code, .service.name, status
tag?: string
// The operator that connects the tag to the value, for example: =, >, !=, =~
diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts
index 4578ae229fe..548bc3e78b1 100644
--- a/public/app/plugins/datasource/tempo/dataquery.gen.ts
+++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts
@@ -60,8 +60,6 @@ export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'search' | 'serviceM
/**
* static fields are pre-set in the UI, dynamic fields are added by the user
*/
-export type TraceqlSearchFilterType = ('static' | 'dynamic');
-
export enum TraceqlSearchScope {
Resource = 'resource',
Span = 'span',
@@ -85,10 +83,6 @@ export interface TraceqlFilter {
* The tag for the search filter, for example: .http.status_code, .service.name, status
*/
tag?: string;
- /**
- * The type of the filter, can either be static (pre defined in the UI) or dynamic
- */
- type: TraceqlSearchFilterType;
/**
* The value for the search filter
*/
diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts
index c33aaa376cc..14e2598a55e 100644
--- a/public/app/plugins/datasource/tempo/datasource.ts
+++ b/public/app/plugins/datasource/tempo/datasource.ts
@@ -36,6 +36,7 @@ import { PrometheusDatasource } from '../prometheus/datasource';
import { PromQuery } from '../prometheus/types';
import { generateQueryFromFilters } from './SearchTraceQLEditor/utils';
+import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
import {
failedMetric,
histogramMetric,
@@ -66,6 +67,7 @@ export class TempoDatasource extends DataSourceWithBackend): Observable {
diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts
index d8f420f1e11..b7ebc925303 100644
--- a/public/app/plugins/datasource/tempo/types.ts
+++ b/public/app/plugins/datasource/tempo/types.ts
@@ -4,7 +4,7 @@ import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsS
import { LokiQuery } from '../loki/types';
-import { TempoQuery as TempoBase, TempoQueryType } from './dataquery.gen';
+import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen';
export interface SearchQueryParams {
minDuration?: string;
@@ -22,6 +22,7 @@ export interface TempoJsonData extends DataSourceJsonData {
};
search?: {
hide?: boolean;
+ filters?: TraceqlFilter[];
};
nodeGraph?: NodeGraphOptions;
lokiSearch?: {