Tempo: Easily filter by trace duration (#79931)

* Easily filter by trace duration

* Add test

* Update onChange
This commit is contained in:
Joey
2024-01-03 10:00:22 +00:00
committed by GitHub
parent fd2c326964
commit e372b54722
3 changed files with 59 additions and 5 deletions

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { EditorRow } from '@grafana/experimental'; import { EditorRow } from '@grafana/experimental';
import { config, FetchError, getTemplateSrv } from '@grafana/runtime'; import { config, FetchError, getTemplateSrv } from '@grafana/runtime';
import { Alert, HorizontalGroup, useStyles2 } from '@grafana/ui'; import { Alert, HorizontalGroup, Select, useStyles2 } from '@grafana/ui';
import { createErrorNotification } from '../../../../core/copy/appNotification'; import { createErrorNotification } from '../../../../core/copy/appNotification';
import { notifyApp } from '../../../../core/reducers/appNotification'; import { notifyApp } from '../../../../core/reducers/appNotification';
@@ -95,12 +95,15 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
// filter out tags that already exist in the static fields // filter out tags that already exist in the static fields
const staticTags = datasource.search?.filters?.map((f) => f.tag) || []; const staticTags = datasource.search?.filters?.map((f) => f.tag) || [];
staticTags.push('duration'); staticTags.push('duration');
staticTags.push('traceDuration');
// Dynamic filters are all filters that don't match the ID of a filter in the datasource configuration // Dynamic filters are all filters that don't match the ID of a filter in the datasource configuration
// The duration and status fields are a special case since its selector is hard-coded // The duration and status fields are a special case since its selector is hard-coded
const dynamicFilters = (query.filters || []).filter( const dynamicFilters = (query.filters || []).filter(
(f) => (f) =>
!hardCodedFilterIds.includes(f.id) && (datasource.search?.filters?.findIndex((sf) => sf.id === f.id) || 0) === -1 !hardCodedFilterIds.includes(f.id) &&
(datasource.search?.filters?.findIndex((sf) => sf.id === f.id) || 0) === -1 &&
f.id !== 'duration-type'
); );
return ( return (
@@ -150,10 +153,25 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
/> />
</InlineSearchField> </InlineSearchField>
<InlineSearchField <InlineSearchField
label={'Span Duration'} label={'Duration'}
tooltip="The span duration, i.e. end - start time of the span. Accepted units are ns, ms, s, m, h" tooltip="The trace or span duration, i.e. end - start time of the trace/span. Accepted units are ns, ms, s, m, h"
> >
<HorizontalGroup spacing={'sm'}> <HorizontalGroup spacing={'sm'}>
<Select
options={[
{ label: 'span', value: 'span' },
{ label: 'trace', value: 'trace' },
]}
value={findFilter('duration-type')?.value ?? 'span'}
onChange={(v) => {
const filter = findFilter('duration-type') || {
id: 'duration-type',
value: 'span',
};
updateFilter({ ...filter, value: v?.value });
}}
aria-label={'duration type'}
/>
<DurationInput <DurationInput
filter={ filter={
findFilter('min-duration') || { findFilter('min-duration') || {

View File

@@ -21,6 +21,32 @@ describe('generateQueryFromFilters generates the correct query for', () => {
expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}'); expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}');
}); });
describe('generates correct query for duration when duration type', () => {
it('not set', () => {
expect(
generateQueryFromFilters([
{ id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' },
])
).toBe('{duration>100ms}');
});
it('set to span', () => {
expect(
generateQueryFromFilters([
{ id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' },
{ id: 'duration-type', value: 'span' },
])
).toBe('{duration>100ms}');
});
it('set to trace', () => {
expect(
generateQueryFromFilters([
{ id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' },
{ id: 'duration-type', value: 'trace' },
])
).toBe('{traceDuration>100ms}');
});
});
it('a field with tag, operator and tag', () => { it('a field with tag, operator and tag', () => {
expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe( expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe(
'{.footag=foovalue}' '{.footag=foovalue}'

View File

@@ -9,7 +9,7 @@ import { Scope } from '../types';
export const generateQueryFromFilters = (filters: TraceqlFilter[]) => { export const generateQueryFromFilters = (filters: TraceqlFilter[]) => {
return `{${filters return `{${filters
.filter((f) => f.tag && f.operator && f.value?.length) .filter((f) => f.tag && f.operator && f.value?.length)
.map((f) => `${scopeHelper(f)}${f.tag}${f.operator}${valueHelper(f)}`) .map((f) => `${scopeHelper(f)}${tagHelper(f, filters)}${f.operator}${valueHelper(f)}`)
.join(' && ')}}`; .join(' && ')}}`;
}; };
@@ -31,6 +31,16 @@ const scopeHelper = (f: TraceqlFilter) => {
(f.scope === TraceqlSearchScope.Resource || f.scope === TraceqlSearchScope.Span ? f.scope?.toLowerCase() : '') + '.' (f.scope === TraceqlSearchScope.Resource || f.scope === TraceqlSearchScope.Span ? f.scope?.toLowerCase() : '') + '.'
); );
}; };
const tagHelper = (f: TraceqlFilter, filters: TraceqlFilter[]) => {
if (f.tag === 'duration') {
const durationType = filters.find((f) => f.id === 'duration-type');
if (durationType) {
return durationType.value === 'trace' ? 'traceDuration' : 'duration';
}
return f.tag;
}
return f.tag;
};
export const filterScopedTag = (f: TraceqlFilter) => { export const filterScopedTag = (f: TraceqlFilter) => {
return scopeHelper(f) + f.tag; return scopeHelper(f) + f.tag;