diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index 1909c7d3892..fbb226a11f3 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -69,9 +69,6 @@ export const defaultTempoQuery: Partial = { groupBy: [], }; -/** - * nativeSearch = Tempo search for backwards compatibility - */ export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear'); /** diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 2ee3eeca947..032df5714f2 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -141,7 +141,7 @@ type TempoQuery struct { TableType *SearchTableType `json:"tableType,omitempty"` } -// TempoQueryType nativeSearch = Tempo search for backwards compatibility +// TempoQueryType defines model for TempoQueryType. type TempoQueryType string // TraceqlFilter defines model for TraceqlFilter. diff --git a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.test.tsx b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.test.tsx deleted file mode 100644 index d174380662b..00000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; - -import { TempoDatasource } from '../datasource'; -import { TempoQuery } from '../types'; - -import NativeSearch from './NativeSearch'; - -const getOptionsV1 = jest.fn().mockImplementation(() => { - return new Promise((resolve) => { - setTimeout(() => { - resolve([ - { - value: 'customer', - label: 'customer', - }, - { - value: 'driver', - label: 'driver', - }, - ]); - }, 1000); - }); -}); - -// Have to mock CodeEditor else it causes act warnings -jest.mock('@grafana/ui', () => ({ - ...jest.requireActual('@grafana/ui'), - CodeEditor: function CodeEditor({ value, onSave }: { value: string; onSave: (newQuery: string) => void }) { - return onSave(event.target.value)} />; - }, -})); - -jest.mock('../language_provider', () => { - return jest.fn().mockImplementation(() => { - return { getOptionsV1 }; - }); -}); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getTemplateSrv: () => ({ - replace: jest.fn(), - containsTemplate: (val: string): boolean => { - return val.includes('$'); - }, - }), -})); - -let mockQuery = { - refId: 'A', - queryType: 'nativeSearch', - key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', - serviceName: 'driver', - spanName: 'customer', -} as TempoQuery; - -describe('NativeSearch', () => { - let user: ReturnType; - - beforeEach(() => { - jest.useFakeTimers(); - // Need to use delay: null here to work with fakeTimers - // see https://github.com/testing-library/user-event/issues/833 - user = userEvent.setup({ delay: null }); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('should show loader when there is a delay', async () => { - render( - - ); - - const select = screen.getByRole('combobox', { name: 'select-service-name' }); - - await user.click(select); - const loader = screen.getByText('Loading options...'); - - expect(loader).toBeInTheDocument(); - - jest.advanceTimersByTime(1000); - - await waitFor(() => expect(screen.queryByText('Loading options...')).not.toBeInTheDocument()); - }); - - it('should call the `onChange` function on click of the Input', async () => { - const promise = Promise.resolve(); - const handleOnChange = jest.fn(() => promise); - const fakeOptionChoice = { - key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', - queryType: 'nativeSearch', - refId: 'A', - serviceName: 'driver', - spanName: 'customer', - }; - - render( - {}} - /> - ); - - const select = await screen.findByRole('combobox', { name: 'select-service-name' }); - - expect(select).toBeInTheDocument(); - await user.click(select); - jest.advanceTimersByTime(1000); - - await user.type(select, 'd'); - const driverOption = await screen.findByText('driver'); - await user.click(driverOption); - - expect(handleOnChange).toHaveBeenCalledWith(fakeOptionChoice); - }); - - it('should filter the span dropdown when user types a search value', async () => { - render( - {}} onRunQuery={() => {}} /> - ); - - const select = await screen.findByRole('combobox', { name: 'select-service-name' }); - await user.click(select); - jest.advanceTimersByTime(1000); - expect(select).toBeInTheDocument(); - - await user.type(select, 'd'); - let option = await screen.findByText('driver'); - expect(option).toBeDefined(); - - await user.type(select, 'a'); - option = await screen.findByText('Hit enter to add'); - expect(option).toBeDefined(); - }); - - it('should add variable to select menu options', async () => { - mockQuery = { - ...mockQuery, - refId: '121314', - serviceName: '$service', - spanName: '$span', - }; - - render( - {}} onRunQuery={() => {}} /> - ); - - const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); - expect(asyncServiceSelect).toBeInTheDocument(); - await user.click(asyncServiceSelect); - jest.advanceTimersByTime(3000); - - await user.type(asyncServiceSelect, '$'); - const serviceOption = await screen.findByText('$service'); - expect(serviceOption).toBeDefined(); - - const asyncSpanSelect = screen.getByRole('combobox', { name: 'select-span-name' }); - expect(asyncSpanSelect).toBeInTheDocument(); - await user.click(asyncSpanSelect); - jest.advanceTimersByTime(3000); - - await user.type(asyncSpanSelect, '$'); - const operationOption = await screen.findByText('$span'); - expect(operationOption).toBeDefined(); - }); -}); diff --git a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx deleted file mode 100644 index 1653cc04de7..00000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useCallback, useState, useEffect, useMemo } from 'react'; - -import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; -import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; -import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; -import { InlineFieldRow, InlineField, Input, Alert, useStyles2, fuzzyMatch, Select } from '@grafana/ui'; - -import { DEFAULT_LIMIT, TempoDatasource } from '../datasource'; -import TempoLanguageProvider from '../language_provider'; -import { TempoQuery } from '../types'; - -import { TagsField } from './TagsField/TagsField'; - -interface Props { - datasource: TempoDatasource; - query: TempoQuery; - onChange: (value: TempoQuery) => void; - onBlur?: () => void; - onRunQuery: () => void; -} - -const durationPlaceholder = 'e.g. 1.2s, 100ms'; - -const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => { - const styles = useStyles2(getStyles); - const [alertText, setAlertText] = useState(); - const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); - const [serviceOptions, setServiceOptions] = useState>>(); - const [spanOptions, setSpanOptions] = useState>>(); - const [error, setError] = useState(null); - const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({}); - const [isLoading, setIsLoading] = useState<{ - serviceName: boolean; - spanName: boolean; - }>({ - serviceName: false, - spanName: false, - }); - - const loadOptions = useCallback( - async (name: string, query = '') => { - const lpName = name === 'serviceName' ? 'service.name' : 'name'; - setIsLoading((prevValue) => ({ ...prevValue, [name]: true })); - - try { - const options = await languageProvider.getOptionsV1(lpName); - const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); - setAlertText(undefined); - setError(null); - return filteredOptions; - } catch (error) { - if (isFetchError(error) && error?.status === 404) { - setError(error); - } else if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - return []; - } finally { - setIsLoading((prevValue) => ({ ...prevValue, [name]: false })); - } - }, - [languageProvider, setAlertText] - ); - - useEffect(() => { - const fetchOptions = async () => { - try { - const [services, spans] = await Promise.all([loadOptions('serviceName'), loadOptions('spanName')]); - if (query.serviceName && getTemplateSrv().containsTemplate(query.serviceName)) { - services.push(toOption(query.serviceName)); - } - setServiceOptions(services); - if (query.spanName && getTemplateSrv().containsTemplate(query.spanName)) { - spans.push(toOption(query.spanName)); - } - setSpanOptions(spans); - setAlertText(undefined); - setError(null); - } catch (error) { - // Display message if Tempo is connected but search 404's - if (isFetchError(error) && error?.status === 404) { - setError(error); - } else if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - } - }; - fetchOptions(); - }, [languageProvider, loadOptions, query.serviceName, query.spanName, setAlertText]); - - const onKeyDown = (keyEvent: React.KeyboardEvent) => { - if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) { - onRunQuery(); - } - }; - - const handleOnChange = useCallback( - (value: string) => { - onChange({ - ...query, - search: value, - }); - }, - [onChange, query] - ); - - const templateSrv: TemplateSrv = getTemplateSrv(); - - return ( - <> -
- - This query type has been deprecated and will be removed in Grafana v10.3. Please migrate to another Tempo - query type. - - - - { - loadOptions('spanName'); - }} - isLoading={isLoading.spanName} - value={spanOptions?.find((v) => v?.value === query.spanName) || query.spanName} - onChange={(v) => { - onChange({ - ...query, - spanName: v?.value, - }); - }} - placeholder="Select a span" - isClearable - onKeyDown={onKeyDown} - aria-label={'select-span-name'} - allowCustomValue={true} - /> - - - - - - - - - - { - const templatedMinDuration = templateSrv.replace(query.minDuration ?? ''); - if (query.minDuration && !isValidGoDuration(templatedMinDuration)) { - setInputErrors({ ...inputErrors, minDuration: true }); - } else { - setInputErrors({ ...inputErrors, minDuration: false }); - } - }} - onChange={(v) => - onChange({ - ...query, - minDuration: v.currentTarget.value, - }) - } - onKeyDown={onKeyDown} - /> - - - - - { - const templatedMaxDuration = templateSrv.replace(query.maxDuration ?? ''); - if (query.maxDuration && !isValidGoDuration(templatedMaxDuration)) { - setInputErrors({ ...inputErrors, maxDuration: true }); - } else { - setInputErrors({ ...inputErrors, maxDuration: false }); - } - }} - onChange={(v) => - onChange({ - ...query, - maxDuration: v.currentTarget.value, - }) - } - onKeyDown={onKeyDown} - /> - - - - - { - let limit = v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined; - if (limit && (!Number.isInteger(limit) || limit <= 0)) { - setInputErrors({ ...inputErrors, limit: true }); - } else { - setInputErrors({ ...inputErrors, limit: false }); - } - - onChange({ - ...query, - limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined, - }); - }} - onKeyDown={onKeyDown} - /> - - -
- {error ? ( - - Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can - configure it in the datasource settings. - - ) : null} - {alertText && } - - ); -}; - -export default NativeSearch; - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - maxWidth: '500px', - }), - alert: css({ - maxWidth: '75ch', - marginTop: theme.spacing(2), - }), -}); diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx deleted file mode 100644 index 9acc12b2899..00000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useEffect, useRef, useState } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; -import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; - -import { TempoDatasource } from '../../datasource'; - -import { CompletionProvider } from './autocomplete'; -import { languageDefinition } from './syntax'; - -interface Props { - placeholder: string; - value: string; - onChange: (val: string) => void; - onBlur?: () => void; - datasource: TempoDatasource; -} - -export function TagsField(props: Props) { - const [alertText, setAlertText] = useState(); - const { onChange, onBlur, placeholder } = props; - const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText); - const theme = useTheme2(); - const styles = getStyles(theme, placeholder); - - return ( - <> - { - setupAutocompleteFn(editor, monaco); - setupPlaceholder(editor, monaco, styles); - setupAutoSize(editor); - }} - /> - {alertText && } - - ); -} - -function setupPlaceholder(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco, styles: EditorStyles) { - const placeholderDecorators = [ - { - range: new monaco.Range(1, 1, 1, 1), - options: { - className: styles.placeholder, // The placeholder text is in styles.placeholder - isWholeLine: true, - }, - }, - ]; - - let decorators: string[] = []; - - const checkDecorators = (): void => { - const model = editor.getModel(); - - if (!model) { - return; - } - - const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : []; - decorators = model.deltaDecorations(decorators, newDecorators); - }; - - checkDecorators(); - editor.onDidChangeModelContent(checkDecorators); -} - -function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) { - const container = editor.getDomNode(); - const updateHeight = () => { - if (container) { - const contentHeight = Math.min(1000, editor.getContentHeight()); - const width = parseInt(container.style.width, 10); - container.style.width = `${width}px`; - container.style.height = `${contentHeight}px`; - editor.layout({ width, height: contentHeight }); - } - }; - editor.onDidContentSizeChange(updateHeight); - updateHeight(); -} - -/** - * Hook that returns function that will set up monaco autocomplete for the label selector - * @param datasource the Tempo datasource instance - * @param setAlertText setter for the alert text - */ -function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: string) => void) { - // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the - // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the - // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not - // need to. - const providerRef = useRef( - new CompletionProvider({ languageProvider: datasource.languageProvider }) - ); - - useEffect(() => { - const fetchTags = async () => { - try { - await datasource.languageProvider.start(); - setAlertText(undefined); - } catch (error) { - if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - } - }; - fetchTags(); - }, [datasource, setAlertText]); - - const autocompleteDisposeFun = useRef<(() => void) | null>(null); - useEffect(() => { - // when we unmount, we unregister the autocomplete-function, if it was registered - return () => { - autocompleteDisposeFun.current?.(); - }; - }, []); - - // This should be run in monaco onEditorDidMount - return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { - providerRef.current.editor = editor; - providerRef.current.monaco = monaco; - - const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current); - autocompleteDisposeFun.current = dispose; - }; -} - -// we must only run the setup code once -let setupDone = false; -const langId = 'tagsfield'; - -function ensureTraceQL(monaco: Monaco) { - if (!setupDone) { - setupDone = true; - const { aliases, extensions, mimetypes, def } = languageDefinition; - monaco.languages.register({ id: langId, aliases, extensions, mimetypes }); - monaco.languages.setMonarchTokensProvider(langId, def.language); - monaco.languages.setLanguageConfiguration(langId, def.languageConfiguration); - } -} - -interface EditorStyles { - placeholder: string; - queryField: string; -} - -const getStyles = (theme: GrafanaTheme2, placeholder: string): EditorStyles => { - return { - queryField: css({ - borderRadius: theme.shape.radius.default, - border: `1px solid ${theme.components.input.borderColor}`, - flex: 1, - }), - placeholder: css({ - '::after': { - content: `'${placeholder}'`, - fontFamily: theme.typography.fontFamilyMonospace, - opacity: 0.3, - }, - }), - }; -}; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts deleted file mode 100644 index 0b8be238785..00000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { SelectableValue } from '@grafana/data'; -import type { Monaco, monacoTypes } from '@grafana/ui'; - -import TempoLanguageProvider from '../../language_provider'; - -interface Props { - languageProvider: TempoLanguageProvider; -} - -/** - * Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco - * autocomplete system. - */ -export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider { - languageProvider: TempoLanguageProvider; - - constructor(props: Props) { - this.languageProvider = props.languageProvider; - } - - triggerCharacters = ['=', ' ']; - - // We set these directly and ae required for the provider to function. - monaco: Monaco | undefined; - editor: monacoTypes.editor.IStandaloneCodeEditor | undefined; - - private cachedValues: { [key: string]: Array> } = {}; - - provideCompletionItems( - model: monacoTypes.editor.ITextModel, - position: monacoTypes.Position - ): monacoTypes.languages.ProviderResult { - // Should not happen, this should not be called before it is initialized - if (!(this.monaco && this.editor)) { - throw new Error('provideCompletionItems called before CompletionProvider was initialized'); - } - - // if the model-id does not match, then this call is from a different editor-instance, - // not "our instance", so return nothing - if (this.editor.getModel()?.id !== model.id) { - return { suggestions: [] }; - } - - const { range, offset } = getRangeAndOffset(this.monaco, model, position); - const situation = this.getSituation(model.getValue(), offset); - const completionItems = this.getCompletions(situation); - - return completionItems.then((items) => { - // monaco by-default alphabetically orders the items. - // to stop it, we use a number-as-string sortkey, - // so that monaco keeps the order we use - const maxIndexDigits = items.length.toString().length; - const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => { - const suggestion: monacoTypes.languages.CompletionItem = { - kind: getMonacoCompletionItemKind(item.type, this.monaco!), - label: item.label, - insertText: item.insertText, - sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have - range, - }; - return suggestion; - }); - return { suggestions }; - }); - } - - private async getTagValues(tagName: string): Promise>> { - let tagValues: Array>; - - if (this.cachedValues.hasOwnProperty(tagName)) { - tagValues = this.cachedValues[tagName]; - } else { - tagValues = await this.languageProvider.getOptionsV1(tagName); - this.cachedValues[tagName] = tagValues; - } - return tagValues; - } - - /** - * Get suggestion based on the situation we are in like whether we should suggest tag names or values. - * @param situation - * @private - */ - private async getCompletions(situation: Situation): Promise { - switch (situation.type) { - // Not really sure what would make sense to suggest in this case so just leave it - case 'UNKNOWN': { - return []; - } - case 'EMPTY': { - return this.getTagsCompletions(); - } - case 'IN_NAME': - return this.getTagsCompletions(); - case 'IN_VALUE': - const tagValues = await this.getTagValues(situation.tagName); - const items: Completion[] = []; - - const getInsertionText = (val: SelectableValue): string => `"${val.label}"`; - - tagValues.forEach((val) => { - if (val?.label) { - items.push({ - label: val.label, - insertText: getInsertionText(val), - type: 'TAG_VALUE', - }); - } - }); - return items; - default: - throw new Error(`Unexpected situation ${situation}`); - } - } - - private getTagsCompletions(): Completion[] { - const tags = this.languageProvider.getAutocompleteTags(); - return tags - .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'accent' })) - .map((key) => ({ - label: key, - insertText: key, - type: 'TAG_NAME', - })); - } - - /** - * Figure out where is the cursor and what kind of suggestions are appropriate. - * @param text - * @param offset - */ - private getSituation(text: string, offset: number): Situation { - if (text === '' || offset === 0 || text[text.length - 1] === ' ') { - return { - type: 'EMPTY', - }; - } - - const textUntilCaret = text.substring(0, offset); - - const regex = /(?[^= ]+)(?=)?(?([^ "]+)|"([^"]*)")?/; - const matches = textUntilCaret.match(new RegExp(regex, 'g')); - - if (matches?.length) { - const last = matches[matches.length - 1]; - const lastMatched = last.match(regex); - if (lastMatched) { - const key = lastMatched.groups?.key; - const equals = lastMatched.groups?.equals; - - if (!key) { - return { - type: 'EMPTY', - }; - } - - if (!equals) { - return { - type: 'IN_NAME', - }; - } - - return { - type: 'IN_VALUE', - tagName: key, - }; - } - } - - return { - type: 'EMPTY', - }; - } -} - -/** - * Get item kind which is used for icon next to the suggestion. - * @param type - * @param monaco - */ -function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { - switch (type) { - case 'TAG_NAME': - return monaco.languages.CompletionItemKind.Enum; - case 'KEYWORD': - return monaco.languages.CompletionItemKind.Keyword; - case 'OPERATOR': - return monaco.languages.CompletionItemKind.Operator; - case 'TAG_VALUE': - return monaco.languages.CompletionItemKind.EnumMember; - case 'SCOPE': - return monaco.languages.CompletionItemKind.Class; - default: - throw new Error(`Unexpected CompletionType: ${type}`); - } -} - -export type CompletionType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE'; -type Completion = { - type: CompletionType; - label: string; - insertText: string; -}; - -export type Tag = { - name: string; - value: string; -}; - -export type Situation = - | { - type: 'UNKNOWN'; - } - | { - type: 'EMPTY'; - } - | { - type: 'IN_NAME'; - } - | { - type: 'IN_VALUE'; - tagName: string; - }; - -function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, position: monacoTypes.Position) { - const word = model.getWordAtPosition(position); - const range = - word != null - ? monaco.Range.lift({ - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }) - : monaco.Range.fromPositions(position); - - // documentation says `position` will be "adjusted" in `getOffsetAt` so we clone it here just for sure. - const positionClone = { - column: position.column, - lineNumber: position.lineNumber, - }; - - const offset = model.getOffsetAt(positionClone); - return { offset, range }; -} diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts deleted file mode 100644 index 6dd942b2f11..00000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { languages } from 'monaco-editor'; - -export const languageConfiguration: languages.LanguageConfiguration = { - // the default separators except `@$` - wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g, - brackets: [ - ['{', '}'], - ['(', ')'], - ], - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - folding: {}, -}; - -const operators = ['=']; - -export const language: languages.IMonarchLanguage = { - ignoreCase: false, - defaultToken: '', - tokenPostfix: '.tagsfield', - - operators, - - // we include these common regular expressions - symbols: /[=>|<|>=|<=|=~|!~))/, 'tag'], - - // all keywords have the same color - [ - /[a-zA-Z_.]\w*/, - { - cases: { - '@default': 'identifier', - }, - }, - ], - - // strings - [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string - [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string - [/"/, 'string', '@string_double'], - [/'/, 'string', '@string_single'], - - // whitespace - { include: '@whitespace' }, - - // delimiters and operators - [/[{}()\[\]]/, '@brackets'], - [/[<>](?!@symbols)/, '@brackets'], - [ - /@symbols/, - { - cases: { - '@operators': 'delimiter', - '@default': '', - }, - }, - ], - - // numbers - [/\d+/, 'number'], - [/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'], - [/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'], - [/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'], - [/0[0-7']*[0-7](@integersuffix)/, 'number.octal'], - [/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'], - [/\d[\d']*\d(@integersuffix)/, 'number'], - [/\d(@integersuffix)/, 'number'], - ], - - string_double: [ - [/[^\\"]+/, 'string'], - [/@escapes/, 'string.escape'], - [/\\./, 'string.escape.invalid'], - [/"/, 'string', '@pop'], - ], - - string_single: [ - [/[^\\']+/, 'string'], - [/@escapes/, 'string.escape'], - [/\\./, 'string.escape.invalid'], - [/'/, 'string', '@pop'], - ], - - clauses: [ - [/[^(,)]/, 'tag'], - [/\)/, 'identifier', '@pop'], - ], - - whitespace: [[/[ \t\r\n]+/, 'white']], - }, -}; - -export const languageDefinition = { - id: 'tagsfield', - extensions: ['.tagsfield'], - aliases: ['tagsfield'], - mimetypes: [], - def: { - language, - languageConfiguration, - }, -}; diff --git a/public/app/plugins/datasource/tempo/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryField.tsx index 01a4cdd415e..3196be57ba9 100644 --- a/public/app/plugins/datasource/tempo/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryField.tsx @@ -15,13 +15,13 @@ import { withTheme2, } from '@grafana/ui'; -import NativeSearch from './NativeSearch/NativeSearch'; import TraceQLSearch from './SearchTraceQLEditor/TraceQLSearch'; import { ServiceGraphSection } from './ServiceGraphSection'; import { TempoQueryType } from './dataquery.gen'; import { TempoDatasource } from './datasource'; import { QueryEditor } from './traceql/QueryEditor'; import { TempoQuery } from './types'; +import { migrateFromSearchToTraceQLSearch } from './utils'; interface Props extends QueryEditorProps, Themeable2 {} interface State { @@ -74,7 +74,7 @@ class TempoQueryFieldComponent extends React.PureComponent { { value: 'serviceMap', label: 'Service Graph' }, ]; - // Show the deprecated search option if any of the deprecated search fields are set + // Migrate user to new query type if they are using the old search query type if ( query.spanName || query.serviceName || @@ -83,7 +83,7 @@ class TempoQueryFieldComponent extends React.PureComponent { query.minDuration || query.queryType === 'nativeSearch' ) { - queryTypeOptions.unshift({ value: 'nativeSearch', label: '[Deprecated] Search' }); + onChange(migrateFromSearchToTraceQLSearch(query)); } return ( @@ -146,15 +146,6 @@ class TempoQueryFieldComponent extends React.PureComponent { - {query.queryType === 'nativeSearch' && ( - - )} {query.queryType === 'traceqlSearch' && ( { const styles = useStyles2(getStyles); - const generateId = () => uuidv4().slice(0, 8); const handleOnAdd = useCallback( () => updateFilter({ id: generateId(), operator: '=', scope: TraceqlSearchScope.Span }), [updateFilter] @@ -117,3 +116,5 @@ const TagsInput = ({ }; export default TagsInput; + +export const generateId = () => uuidv4().slice(0, 8); diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index c1a1906e266..786f000fef6 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -53,7 +53,6 @@ composableKinds: DataQuery: { tableType?: #SearchTableType } @cuetsy(kind="interface") @grafana(TSVeneer="type") - // nativeSearch = Tempo search for backwards compatibility #TempoQueryType: "traceql" | "traceqlSearch" | "serviceMap" | "upload" | "nativeSearch" | "traceId" | "clear" @cuetsy(kind="type") // The state of the TraceQL streaming search query diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index c98759b03f5..17cce1c50d0 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -67,9 +67,6 @@ export const defaultTempoQuery: Partial = { groupBy: [], }; -/** - * nativeSearch = Tempo search for backwards compatibility - */ export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear'); /** diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 1611ed42401..7deb9b62aff 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -31,7 +31,6 @@ import { TempoVariableQueryType } from './VariableQueryEditor'; import { createFetchResponse } from './_importedDependencies/test/helpers/createFetchResponse'; import { TraceqlSearchScope } from './dataquery.gen'; import { - DEFAULT_LIMIT, TempoDatasource, buildExpr, buildLinkExpr, @@ -83,11 +82,6 @@ describe('Tempo data source', () => { refId: 'x', queryType: 'traceql', query: '$interpolationVarWithPipe', - spanName: '$interpolationVar', - serviceName: '$interpolationVar', - search: '$interpolationVar', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', serviceMapQuery, filters: [ { @@ -135,11 +129,6 @@ describe('Tempo data source', () => { const ds = new TempoDatasource(defaultSettings, templateSrv); const queries = ds.interpolateVariablesInQueries([getQuery()], {}); expect(queries[0].query).toBe(textWithPipe); - expect(queries[0].serviceName).toBe(text); - expect(queries[0].spanName).toBe(text); - expect(queries[0].search).toBe(text); - expect(queries[0].minDuration).toBe(text); - expect(queries[0].maxDuration).toBe(text); expect(queries[0].serviceMapQuery).toBe(text); expect(queries[0].filters[0].value).toBe(textWithPipe); expect(queries[0].filters[1].value).toBe(text); @@ -153,11 +142,6 @@ describe('Tempo data source', () => { interpolationVar: { text: scopedText, value: scopedText }, }); expect(resp.query).toBe(textWithPipe); - expect(resp.serviceName).toBe(scopedText); - expect(resp.spanName).toBe(scopedText); - expect(resp.search).toBe(scopedText); - expect(resp.minDuration).toBe(scopedText); - expect(resp.maxDuration).toBe(scopedText); expect(resp.filters[0].value).toBe(textWithPipe); expect(resp.filters[1].value).toBe(scopedText); expect(resp.filters[1].tag).toBe(scopedText); @@ -283,31 +267,6 @@ describe('Tempo data source', () => { expect(edgesFrame.meta?.preferredVisualisationType).toBe('nodeGraph'); }); - it('should build search query correctly', () => { - const duration = '10ms'; - const templateSrv = { replace: jest.fn().mockReturnValue(duration) } as unknown as TemplateSrv; - const ds = new TempoDatasource(defaultSettings, templateSrv); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - serviceName: 'frontend', - spanName: '/config', - search: 'root.http.status_code=500', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', - limit: 10, - filters: [], - }; - const builtQuery = ds.buildSearchQuery(tempoQuery); - expect(builtQuery).toStrictEqual({ - tags: 'root.http.status_code=500 service.name="frontend" name="/config"', - minDuration: duration, - maxDuration: duration, - limit: 10, - }); - }); - it('should format metrics summary query correctly', () => { const ds = new TempoDatasource(defaultSettings, {} as TemplateSrv); const queryGroupBy = [ @@ -320,61 +279,6 @@ describe('Tempo data source', () => { expect(groupBy).toEqual('.component, span.name, resource.service.name, kind'); }); - it('should include a default limit', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - search: '', - filters: [], - }; - const builtQuery = ds.buildSearchQuery(tempoQuery); - expect(builtQuery).toStrictEqual({ - tags: '', - limit: DEFAULT_LIMIT, - }); - }); - - it('should include time range if provided', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - search: '', - filters: [], - }; - const timeRange = { startTime: 0, endTime: 1000 }; - const builtQuery = ds.buildSearchQuery(tempoQuery, timeRange); - expect(builtQuery).toStrictEqual({ - tags: '', - limit: DEFAULT_LIMIT, - start: timeRange.startTime, - end: timeRange.endTime, - }); - }); - - it('formats native search query history correctly', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - filters: [], - queryType: 'nativeSearch', - refId: 'A', - query: '', - serviceName: 'frontend', - spanName: '/config', - search: 'root.http.status_code=500', - minDuration: '1ms', - maxDuration: '100s', - limit: 10, - }; - const result = ds.getQueryDisplayText(tempoQuery); - expect(result).toBe( - 'Service Name: frontend, Span Name: /config, Search: root.http.status_code=500, Min Duration: 1ms, Max Duration: 100s, Limit: 10' - ); - }); - describe('test the testDatasource function', () => { it('should return a success msg if response.ok is true', async () => { mockObservable = () => of({ ok: true }); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 0b73600318b..f6d9600fcc5 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -1,4 +1,4 @@ -import { groupBy, identity, pick, pickBy, startCase } from 'lodash'; +import { groupBy, startCase } from 'lodash'; import { EMPTY, from, lastValueFrom, merge, Observable, of } from 'rxjs'; import { catchError, concatMap, map, mergeMap, toArray } from 'rxjs/operators'; import semver from 'semver'; @@ -14,7 +14,6 @@ import { DataSourceInstanceSettings, dateTime, FieldType, - isValidGoDuration, LoadingState, rangeUtil, ScopedVars, @@ -53,15 +52,14 @@ import { import TempoLanguageProvider from './language_provider'; import { createTableFrameFromMetricsSummaryQuery, emptyResponse, MetricsSummary } from './metricsSummary'; import { - createTableFrameFromSearch, formatTraceQLMetrics, formatTraceQLResponse, transformFromOTLP as transformFromOTEL, transformTrace, } from './resultTransformer'; import { doTempoChannelStream } from './streaming'; -import { SearchQueryParams, TempoJsonData, TempoQuery } from './types'; -import { getErrorMessage } from './utils'; +import { TempoJsonData, TempoQuery } from './types'; +import { getErrorMessage, migrateFromSearchToTraceQLSearch } from './utils'; import { TempoVariableSupport } from './variables'; export const DEFAULT_LIMIT = 20; @@ -265,37 +263,18 @@ export class TempoDatasource extends DataSourceWithBackend { - return { - data: [createTableFrameFromSearch(response.data.traces, this.instanceSettings)], - }; - }), - catchError((err) => { - return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); - }) - ) - ); - } catch (error) { - return of({ error: { message: error instanceof Error ? error.message : 'Unknown error occurred' }, data: [] }); + if ( + targets.nativeSearch[0].spanName || + targets.nativeSearch[0].serviceName || + targets.nativeSearch[0].search || + targets.nativeSearch[0].maxDuration || + targets.nativeSearch[0].minDuration || + targets.nativeSearch[0].queryType === 'nativeSearch' + ) { + const migratedQuery = migrateFromSearchToTraceQLSearch(targets.nativeSearch[0]); + targets.traceqlSearch = [migratedQuery]; } } @@ -502,11 +481,6 @@ export class TempoDatasource extends DataSourceWithBackend this.templateSrv.replace(query, scopedVars)) : this.templateSrv.replace(query.serviceMapQuery ?? '', scopedVars), @@ -758,55 +732,6 @@ export class TempoDatasource extends DataSourceWithBackend `${startCase(key)}: ${query[key]}`) .join(', '); } - - buildSearchQuery(query: TempoQuery, timeRange?: { startTime: number; endTime?: number }): SearchQueryParams { - let tags = query.search ?? ''; - - let tempoQuery = pick(query, ['minDuration', 'maxDuration', 'limit']); - // Remove empty properties - tempoQuery = pickBy(tempoQuery, identity); - - if (query.serviceName) { - tags += ` service.name="${query.serviceName}"`; - } - if (query.spanName) { - tags += ` name="${query.spanName}"`; - } - - // Set default limit - if (!tempoQuery.limit) { - tempoQuery.limit = DEFAULT_LIMIT; - } - - // Validate query inputs and remove spaces if valid - if (tempoQuery.minDuration) { - tempoQuery.minDuration = this.templateSrv.replace(tempoQuery.minDuration ?? ''); - if (!isValidGoDuration(tempoQuery.minDuration)) { - throw new Error('Please enter a valid min duration.'); - } - tempoQuery.minDuration = tempoQuery.minDuration.replace(/\s/g, ''); - } - if (tempoQuery.maxDuration) { - tempoQuery.maxDuration = this.templateSrv.replace(tempoQuery.maxDuration ?? ''); - if (!isValidGoDuration(tempoQuery.maxDuration)) { - throw new Error('Please enter a valid max duration.'); - } - tempoQuery.maxDuration = tempoQuery.maxDuration.replace(/\s/g, ''); - } - - if (!Number.isInteger(tempoQuery.limit) || tempoQuery.limit <= 0) { - throw new Error('Please enter a valid limit.'); - } - - let searchQuery: SearchQueryParams = { tags, ...tempoQuery }; - - if (timeRange) { - searchQuery.start = timeRange.startTime; - searchQuery.end = timeRange.endTime; - } - - return searchQuery; - } } function queryPrometheus(request: DataQueryRequest, datasourceUid: string) { diff --git a/public/app/plugins/datasource/tempo/mockServiceGraph.json b/public/app/plugins/datasource/tempo/mockServiceGraph.json index abdd24a0d18..267ffe9975d 100644 --- a/public/app/plugins/datasource/tempo/mockServiceGraph.json +++ b/public/app/plugins/datasource/tempo/mockServiceGraph.json @@ -48,7 +48,7 @@ "title": "View traces", "internal": { "query": { - "queryType": "nativeSearch", + "queryType": "traceqlSearch", "serviceName": "${__data.fields[0]}" }, "datasourceUid": "TNS Tempo", diff --git a/public/app/plugins/datasource/tempo/resultTransformer.test.ts b/public/app/plugins/datasource/tempo/resultTransformer.test.ts index 9d6e3816b7a..2f416d80ef3 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.test.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.test.ts @@ -1,11 +1,10 @@ import { collectorTypes } from '@opentelemetry/exporter-collector'; -import { PluginType, DataSourceInstanceSettings, dateTime, PluginMetaInfo } from '@grafana/data'; +import { PluginType, DataSourceInstanceSettings, PluginMetaInfo } from '@grafana/data'; import { transformToOTLP, transformFromOTLP, - createTableFrameFromSearch, createTableFrameFromTraceQlQuery, createTableFrameFromTraceQlQueryAsSpans, } from './resultTransformer'; @@ -14,7 +13,6 @@ import { otlpDataFrameToResponse, otlpDataFrameFromResponse, otlpResponse, - tempoSearchResponse, traceQlResponse, } from './testResponse'; import { TraceSearchMetadata } from './types'; @@ -57,32 +55,6 @@ describe('transformFromOTLP()', () => { }); }); -describe('createTableFrameFromSearch()', () => { - const mockTimeUnix = dateTime(1643357709095).valueOf(); - global.Date.now = jest.fn(() => mockTimeUnix); - test('transforms search response to dataFrame', () => { - const frame = createTableFrameFromSearch(tempoSearchResponse.traces as TraceSearchMetadata[], defaultSettings); - expect(frame.fields[0].name).toBe('traceID'); - expect(frame.fields[0].values[0]).toBe('e641dcac1c3a0565'); - - // TraceID must have unit = 'string' to prevent the ID from rendering as Infinity - expect(frame.fields[0].config.unit).toBe('string'); - - expect(frame.fields[1].name).toBe('traceService'); - expect(frame.fields[1].values[0]).toBe('requester'); - - expect(frame.fields[2].name).toBe('traceName'); - expect(frame.fields[2].values[0]).toBe('app'); - - expect(frame.fields[3].name).toBe('startTime'); - expect(frame.fields[3].values[0]).toBe(1643356828724); - expect(frame.fields[3].values[1]).toBe(1643342166678.0002); - - expect(frame.fields[4].name).toBe('traceDuration'); - expect(frame.fields[4].values[0]).toBe(65); - }); -}); - describe('createTableFrameFromTraceQlQuery()', () => { test('transforms TraceQL response to DataFrame', () => { const frameList = createTableFrameFromTraceQlQuery(traceQlResponse.traces, defaultSettings); diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index b3e5735f1a3..c260b0b78c8 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -464,63 +464,6 @@ export function transformTrace( }; } -export function createTableFrameFromSearch(data: TraceSearchMetadata[], instanceSettings: DataSourceInstanceSettings) { - const frame = new MutableDataFrame({ - name: 'Traces', - refId: 'traces', - fields: [ - { - name: 'traceID', - type: FieldType.string, - values: [], - config: { - unit: 'string', - displayNameFromDS: 'Trace ID', - links: [ - { - title: 'Trace: ${__value.raw}', - url: '', - internal: { - datasourceUid: instanceSettings.uid, - datasourceName: instanceSettings.name, - query: { - query: '${__value.raw}', - queryType: 'traceql', - }, - }, - }, - ], - }, - }, - { name: 'traceService', type: FieldType.string, config: { displayNameFromDS: 'Trace service' }, values: [] }, - { name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Trace name' }, values: [] }, - { name: 'startTime', type: FieldType.time, config: { displayNameFromDS: 'Start time' }, values: [] }, - { - name: 'traceDuration', - type: FieldType.number, - config: { displayNameFromDS: 'Duration', unit: 'ms' }, - values: [], - }, - ], - meta: { - preferredVisualisationType: 'table', - }, - }); - if (!data?.length) { - return frame; - } - // Show the most recent traces - const traceData = data - .sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000) - .map(transformToTraceData); - - for (const trace of traceData) { - frame.add(trace); - } - - return frame; -} - function transformToTraceData(data: TraceSearchMetadata) { return { traceID: data.traceID, diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/testResponse.ts index 69109410823..687e90b82cb 100644 --- a/public/app/plugins/datasource/tempo/testResponse.ts +++ b/public/app/plugins/datasource/tempo/testResponse.ts @@ -2273,28 +2273,6 @@ export const otlpResponse = { ], }; -export const tempoSearchResponse = { - traces: [ - { - traceID: 'e641dcac1c3a0565', - rootServiceName: 'requester', - rootTraceName: 'app', - startTimeUnixNano: '1643356828724000000', - durationMs: 65, - }, - { - traceID: 'c2983496a2b12544', - rootServiceName: '', - startTimeUnixNano: '1643342166678000000', - durationMs: 93, - }, - ], - metrics: { - inspectedTraces: 2, - inspectedBytes: '83720', - }, -}; - export const traceQlResponse = { traces: [ { diff --git a/public/app/plugins/datasource/tempo/tracking.test.ts b/public/app/plugins/datasource/tempo/tracking.test.ts index 8133787d32c..2066b71a623 100644 --- a/public/app/plugins/datasource/tempo/tracking.test.ts +++ b/public/app/plugins/datasource/tempo/tracking.test.ts @@ -17,23 +17,6 @@ jest.mock('@grafana/runtime', () => { grafanaVersion: 'v9.4.0', queries: { tempo: [ - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - refId: 'A', - }, - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - spanName: 'HTTP', - refId: 'A', - }, - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - spanName: '$var', - refId: 'A', - }, { datasource: { type: 'tempo', uid: 'abc' }, queryType: 'serviceMap', @@ -86,10 +69,8 @@ describe('on dashboard loaded', () => { traceql_query_count: 2, service_map_query_count: 2, upload_query_count: 1, - native_search_query_count: 3, traceql_queries_with_template_variables_count: 1, service_map_queries_with_template_variables_count: 1, - native_search_queries_with_template_variables_count: 1, }); }); }); diff --git a/public/app/plugins/datasource/tempo/tracking.ts b/public/app/plugins/datasource/tempo/tracking.ts index f44effba5ec..e5833a5c424 100644 --- a/public/app/plugins/datasource/tempo/tracking.ts +++ b/public/app/plugins/datasource/tempo/tracking.ts @@ -8,11 +8,9 @@ type TempoOnDashboardLoadedTrackingEvent = { grafana_version?: string; dashboard_id?: string; org_id?: number; - native_search_query_count: number; service_map_query_count: number; traceql_query_count: number; upload_query_count: number; - native_search_queries_with_template_variables_count: number; service_map_queries_with_template_variables_count: number; traceql_queries_with_template_variables_count: number; }; @@ -31,11 +29,9 @@ export const onDashboardLoadedHandler = ({ grafana_version: grafanaVersion, dashboard_id: dashboardId, org_id: orgId, - native_search_query_count: 0, service_map_query_count: 0, traceql_query_count: 0, upload_query_count: 0, - native_search_queries_with_template_variables_count: 0, service_map_queries_with_template_variables_count: 0, traceql_queries_with_template_variables_count: 0, }; @@ -45,18 +41,7 @@ export const onDashboardLoadedHandler = ({ continue; } - if (query.queryType === 'nativeSearch') { - stats.native_search_query_count++; - if ( - (query.serviceName && hasTemplateVariables(query.serviceName)) || - (query.spanName && hasTemplateVariables(query.spanName)) || - (query.search && hasTemplateVariables(query.search)) || - (query.minDuration && hasTemplateVariables(query.minDuration)) || - (query.maxDuration && hasTemplateVariables(query.maxDuration)) - ) { - stats.native_search_queries_with_template_variables_count++; - } - } else if (query.queryType === 'serviceMap') { + if (query.queryType === 'serviceMap') { stats.service_map_query_count++; if (query.serviceMapQuery && hasTemplateVariables(query.serviceMapQuery)) { stats.service_map_queries_with_template_variables_count++; diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts index 806445c10a6..09c31e05b93 100644 --- a/public/app/plugins/datasource/tempo/types.ts +++ b/public/app/plugins/datasource/tempo/types.ts @@ -3,15 +3,6 @@ import { NodeGraphOptions, TraceToLogsOptions } from '@grafana/o11y-ds-frontend' import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen'; -export interface SearchQueryParams { - minDuration?: string; - maxDuration?: string; - limit?: number; - tags?: string; - start?: number; - end?: number; -} - export interface TempoJsonData extends DataSourceJsonData { tracesToLogs?: TraceToLogsOptions; serviceMap?: { diff --git a/public/app/plugins/datasource/tempo/utils.test.ts b/public/app/plugins/datasource/tempo/utils.test.ts new file mode 100644 index 00000000000..65f40f78166 --- /dev/null +++ b/public/app/plugins/datasource/tempo/utils.test.ts @@ -0,0 +1,51 @@ +import { TempoQuery } from './types'; +import { migrateFromSearchToTraceQLSearch } from './utils'; + +describe('utils', () => { + it('migrateFromSearchToTraceQLSearch correctly updates the query', async () => { + const query: TempoQuery = { + refId: 'A', + filters: [], + queryType: 'nativeSearch', + serviceName: 'frontend', + spanName: 'http.server', + minDuration: '1s', + maxDuration: '10s', + search: 'component="net/http" datasource.type="tempo"', + }; + + const migratedQuery = migrateFromSearchToTraceQLSearch(query); + expect(migratedQuery.queryType).toBe('traceqlSearch'); + expect(migratedQuery.filters.length).toBe(7); + expect(migratedQuery.filters[0].scope).toBe('span'); + expect(migratedQuery.filters[0].tag).toBe('name'); + expect(migratedQuery.filters[0].operator).toBe('='); + expect(migratedQuery.filters[0].value![0]).toBe('http.server'); + expect(migratedQuery.filters[0].valueType).toBe('string'); + expect(migratedQuery.filters[1].scope).toBe('resource'); + expect(migratedQuery.filters[1].tag).toBe('service.name'); + expect(migratedQuery.filters[1].operator).toBe('='); + expect(migratedQuery.filters[1].value![0]).toBe('frontend'); + expect(migratedQuery.filters[1].valueType).toBe('string'); + expect(migratedQuery.filters[2].id).toBe('duration-type'); + expect(migratedQuery.filters[2].value).toBe('trace'); + expect(migratedQuery.filters[3].tag).toBe('duration'); + expect(migratedQuery.filters[3].operator).toBe('>'); + expect(migratedQuery.filters[3].value![0]).toBe('1s'); + expect(migratedQuery.filters[3].valueType).toBe('duration'); + expect(migratedQuery.filters[4].tag).toBe('duration'); + expect(migratedQuery.filters[4].operator).toBe('<'); + expect(migratedQuery.filters[4].value![0]).toBe('10s'); + expect(migratedQuery.filters[4].valueType).toBe('duration'); + expect(migratedQuery.filters[5].scope).toBe('unscoped'); + expect(migratedQuery.filters[5].tag).toBe('component'); + expect(migratedQuery.filters[5].operator).toBe('='); + expect(migratedQuery.filters[5].value![0]).toBe('net/http'); + expect(migratedQuery.filters[5].valueType).toBe('string'); + expect(migratedQuery.filters[6].scope).toBe('unscoped'); + expect(migratedQuery.filters[6].tag).toBe('datasource.type'); + expect(migratedQuery.filters[6].operator).toBe('='); + expect(migratedQuery.filters[6].value![0]).toBe('tempo'); + expect(migratedQuery.filters[6].valueType).toBe('string'); + }); +}); diff --git a/public/app/plugins/datasource/tempo/utils.ts b/public/app/plugins/datasource/tempo/utils.ts index 1e138747e8a..f23b77f3e57 100644 --- a/public/app/plugins/datasource/tempo/utils.ts +++ b/public/app/plugins/datasource/tempo/utils.ts @@ -1,6 +1,10 @@ import { DataSourceApi } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; +import { generateId } from './SearchTraceQLEditor/TagsInput'; +import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; +import { TempoQuery } from './types'; + export const getErrorMessage = (message: string | undefined, prefix?: string) => { const err = message ? ` (${message})` : ''; let errPrefix = prefix ? prefix : 'Error'; @@ -20,3 +24,78 @@ export async function getDS(uid?: string): Promise { return undefined; } } + +export const migrateFromSearchToTraceQLSearch = (query: TempoQuery) => { + let filters: TraceqlFilter[] = []; + if (query.spanName) { + filters.push({ + id: 'span-name', + scope: TraceqlSearchScope.Span, + tag: 'name', + operator: '=', + value: [query.spanName], + valueType: 'string', + }); + } + if (query.serviceName) { + filters.push({ + id: 'service-name', + scope: TraceqlSearchScope.Resource, + tag: 'service.name', + operator: '=', + value: [query.serviceName], + valueType: 'string', + }); + } + if (query.minDuration || query.maxDuration) { + filters.push({ + id: 'duration-type', + value: 'trace', + }); + } + if (query.minDuration) { + filters.push({ + id: 'min-duration', + tag: 'duration', + operator: '>', + value: [query.minDuration], + valueType: 'duration', + }); + } + if (query.maxDuration) { + filters.push({ + id: 'max-duration', + tag: 'duration', + operator: '<', + value: [query.maxDuration], + valueType: 'duration', + }); + } + if (query.search) { + const tags = query.search.split(' '); + for (const tag of tags) { + const [key, value] = tag.split('='); + if (key && value) { + filters.push({ + id: generateId(), + scope: TraceqlSearchScope.Unscoped, + tag: key, + operator: '=', + value: [value.replace(/(^"|"$)/g, '')], // remove quotes at start and end of string + valueType: value.startsWith('"') && value.endsWith('"') ? 'string' : undefined, + }); + } + } + } + + const migratedQuery: TempoQuery = { + datasource: query.datasource, + filters, + groupBy: query.groupBy, + limit: query.limit, + query: query.query, + queryType: 'traceqlSearch', + refId: query.refId, + }; + return migratedQuery; +};