mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
Tempo: Deprecate old search (#84498)
* Add logic to update query from old search to new search i.e. nativeSearch to traceqlSearch * Remove nativeSearch query and transform * Update tracking * Remove nativeSearch from query field * Udpdate gen comment * Fix tests * Add tests * Remove comments * Remove log * Remove log * Update comment * Update ids etc for migratedQuery * Remove old nativeSearch folder * Fix tests, manual testing
This commit is contained in:
parent
1de4187a6e
commit
f5e83d07a7
@ -69,9 +69,6 @@ export const defaultTempoQuery: Partial<TempoQuery> = {
|
||||
groupBy: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* nativeSearch = Tempo search for backwards compatibility
|
||||
*/
|
||||
export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear');
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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 <input data-testid="mockeditor" value={value} onChange={(event) => 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<typeof userEvent.setup>;
|
||||
|
||||
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(
|
||||
<NativeSearch datasource={{} as TempoDatasource} query={mockQuery} onChange={jest.fn()} onRunQuery={jest.fn()} />
|
||||
);
|
||||
|
||||
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(
|
||||
<NativeSearch
|
||||
datasource={{} as TempoDatasource}
|
||||
query={mockQuery}
|
||||
onChange={handleOnChange}
|
||||
onRunQuery={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<NativeSearch datasource={{} as TempoDatasource} query={mockQuery} onChange={() => {}} 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(
|
||||
<NativeSearch datasource={{} as TempoDatasource} query={mockQuery} onChange={() => {}} 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();
|
||||
});
|
||||
});
|
@ -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<string>();
|
||||
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
|
||||
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [spanOptions, setSpanOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [error, setError] = useState<Error | FetchError | null>(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 (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Alert title="Deprecated query type" severity="warning">
|
||||
This query type has been deprecated and will be removed in Grafana v10.3. Please migrate to another Tempo
|
||||
query type.
|
||||
</Alert>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Service Name" labelWidth={14} grow>
|
||||
<Select
|
||||
inputId="service"
|
||||
options={serviceOptions}
|
||||
onOpenMenu={() => {
|
||||
loadOptions('serviceName');
|
||||
}}
|
||||
isLoading={isLoading.serviceName}
|
||||
value={serviceOptions?.find((v) => v?.value === query.serviceName) || query.serviceName}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...query,
|
||||
serviceName: v?.value,
|
||||
});
|
||||
}}
|
||||
placeholder="Select a service"
|
||||
isClearable
|
||||
onKeyDown={onKeyDown}
|
||||
aria-label={'select-service-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Span Name" labelWidth={14} grow>
|
||||
<Select
|
||||
inputId="spanName"
|
||||
options={spanOptions}
|
||||
onOpenMenu={() => {
|
||||
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}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in logfmt.">
|
||||
<TagsField
|
||||
placeholder="http.status_code=200 error=true"
|
||||
value={query.search || ''}
|
||||
onChange={handleOnChange}
|
||||
onBlur={onBlur}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Min Duration" invalid={!!inputErrors.minDuration} labelWidth={14} grow>
|
||||
<Input
|
||||
id="minDuration"
|
||||
value={query.minDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onBlur={() => {
|
||||
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}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Max Duration" invalid={!!inputErrors.maxDuration} labelWidth={14} grow>
|
||||
<Input
|
||||
id="maxDuration"
|
||||
value={query.maxDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onBlur={() => {
|
||||
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}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Limit"
|
||||
invalid={!!inputErrors.limit}
|
||||
labelWidth={14}
|
||||
grow
|
||||
tooltip="Maximum number of returned results"
|
||||
>
|
||||
<Input
|
||||
id="limit"
|
||||
value={query.limit || ''}
|
||||
placeholder={`Default: ${DEFAULT_LIMIT}`}
|
||||
type="number"
|
||||
onChange={(v) => {
|
||||
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}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
{error ? (
|
||||
<Alert title="Unable to connect to Tempo search" severity="info" className={styles.alert}>
|
||||
Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can
|
||||
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
||||
</Alert>
|
||||
) : null}
|
||||
{alertText && <TemporaryAlert severity="error" text={alertText} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NativeSearch;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
maxWidth: '500px',
|
||||
}),
|
||||
alert: css({
|
||||
maxWidth: '75ch',
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -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<string>();
|
||||
const { onChange, onBlur, placeholder } = props;
|
||||
const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText);
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, placeholder);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CodeEditor
|
||||
value={props.value}
|
||||
language={langId}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
containerStyles={styles.queryField}
|
||||
monacoOptions={{
|
||||
folding: false,
|
||||
fontSize: 14,
|
||||
lineNumbers: 'off',
|
||||
overviewRulerLanes: 0,
|
||||
renderLineHighlight: 'none',
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
verticalScrollbarSize: 8, // used as "padding-right"
|
||||
horizontal: 'hidden',
|
||||
horizontalScrollbarSize: 0,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
onBeforeEditorMount={ensureTraceQL}
|
||||
onEditorDidMount={(editor, monaco) => {
|
||||
setupAutocompleteFn(editor, monaco);
|
||||
setupPlaceholder(editor, monaco, styles);
|
||||
setupAutoSize(editor);
|
||||
}}
|
||||
/>
|
||||
{alertText && <TemporaryAlert severity="error" text={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<CompletionProvider>(
|
||||
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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
@ -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<SelectableValue<string>> } = {};
|
||||
|
||||
provideCompletionItems(
|
||||
model: monacoTypes.editor.ITextModel,
|
||||
position: monacoTypes.Position
|
||||
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CompletionList> {
|
||||
// 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<Array<SelectableValue<string>>> {
|
||||
let tagValues: Array<SelectableValue<string>>;
|
||||
|
||||
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<Completion[]> {
|
||||
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>): 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 = /(?<key>[^= ]+)(?<equals>=)?(?<value>([^ "]+)|"([^"]*)")?/;
|
||||
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 };
|
||||
}
|
@ -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: /[=><!~?:&|+\-*\/^%]+/,
|
||||
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
||||
digits: /\d+(_+\d+)*/,
|
||||
octaldigits: /[0-7]+(_+[0-7]+)*/,
|
||||
binarydigits: /[0-1]+(_+[0-1]+)*/,
|
||||
hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
|
||||
integersuffix: /(ll|LL|u|U|l|L)?(ll|LL|u|U|l|L)?/,
|
||||
floatsuffix: /[fFlL]?/,
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
// labels
|
||||
[/[a-z_.][\w./_-]*(?=\s*(=|!=|>|<|>=|<=|=~|!~))/, '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,
|
||||
},
|
||||
};
|
@ -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<TempoDatasource, TempoQuery>, Themeable2 {}
|
||||
interface State {
|
||||
@ -74,7 +74,7 @@ class TempoQueryFieldComponent extends React.PureComponent<Props, State> {
|
||||
{ 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<Props, State> {
|
||||
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<Props, State> {
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
{query.queryType === 'nativeSearch' && (
|
||||
<NativeSearch
|
||||
datasource={this.props.datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onBlur={this.props.onBlur}
|
||||
onRunQuery={this.props.onRunQuery}
|
||||
/>
|
||||
)}
|
||||
{query.queryType === 'traceqlSearch' && (
|
||||
<TraceQLSearch
|
||||
datasource={this.props.datasource}
|
||||
|
@ -54,7 +54,6 @@ const TagsInput = ({
|
||||
query,
|
||||
}: Props) => {
|
||||
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);
|
||||
|
@ -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
|
||||
|
@ -67,9 +67,6 @@ export const defaultTempoQuery: Partial<TempoQuery> = {
|
||||
groupBy: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* nativeSearch = Tempo search for backwards compatibility
|
||||
*/
|
||||
export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear');
|
||||
|
||||
/**
|
||||
|
@ -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 });
|
||||
|
@ -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<TempoQuery, TempoJson
|
||||
return of({ data: [], state: LoadingState.Done });
|
||||
}
|
||||
|
||||
// Migrate user to new query type if they are using the old search query type
|
||||
if (targets.nativeSearch?.length) {
|
||||
try {
|
||||
reportInteraction('grafana_traces_search_queried', {
|
||||
datasourceType: 'tempo',
|
||||
app: options.app ?? '',
|
||||
grafana_version: config.buildInfo.version,
|
||||
hasServiceName: targets.nativeSearch[0].serviceName ? true : false,
|
||||
hasSpanName: targets.nativeSearch[0].spanName ? true : false,
|
||||
resultLimit: targets.nativeSearch[0].limit ?? '',
|
||||
hasSearch: targets.nativeSearch[0].search ? true : false,
|
||||
minDuration: targets.nativeSearch[0].minDuration ?? '',
|
||||
maxDuration: targets.nativeSearch[0].maxDuration ?? '',
|
||||
});
|
||||
|
||||
const timeRange = { startTime: options.range.from.unix(), endTime: options.range.to.unix() };
|
||||
const query = this.applyVariables(targets.nativeSearch[0], options.scopedVars);
|
||||
const searchQuery = this.buildSearchQuery(query, timeRange);
|
||||
subQueries.push(
|
||||
this._request('/api/search', searchQuery).pipe(
|
||||
map((response) => {
|
||||
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<TempoQuery, TempoJson
|
||||
return {
|
||||
...expandedQuery,
|
||||
query: this.templateSrv.replace(query.query ?? '', scopedVars, VariableFormatID.Pipe),
|
||||
serviceName: this.templateSrv.replace(query.serviceName ?? '', scopedVars),
|
||||
spanName: this.templateSrv.replace(query.spanName ?? '', scopedVars),
|
||||
search: this.templateSrv.replace(query.search ?? '', scopedVars),
|
||||
minDuration: this.templateSrv.replace(query.minDuration ?? '', scopedVars),
|
||||
maxDuration: this.templateSrv.replace(query.maxDuration ?? '', scopedVars),
|
||||
serviceMapQuery: Array.isArray(query.serviceMapQuery)
|
||||
? query.serviceMapQuery.map((query) => this.templateSrv.replace(query, scopedVars))
|
||||
: this.templateSrv.replace(query.serviceMapQuery ?? '', scopedVars),
|
||||
@ -758,55 +732,6 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
.map((key) => `${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<PromQuery>, datasourceUid: string) {
|
||||
|
@ -48,7 +48,7 @@
|
||||
"title": "View traces",
|
||||
"internal": {
|
||||
"query": {
|
||||
"queryType": "nativeSearch",
|
||||
"queryType": "traceqlSearch",
|
||||
"serviceName": "${__data.fields[0]}"
|
||||
},
|
||||
"datasourceUid": "TNS Tempo",
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -2273,28 +2273,6 @@ export const otlpResponse = {
|
||||
],
|
||||
};
|
||||
|
||||
export const tempoSearchResponse = {
|
||||
traces: [
|
||||
{
|
||||
traceID: 'e641dcac1c3a0565',
|
||||
rootServiceName: 'requester',
|
||||
rootTraceName: 'app',
|
||||
startTimeUnixNano: '1643356828724000000',
|
||||
durationMs: 65,
|
||||
},
|
||||
{
|
||||
traceID: 'c2983496a2b12544',
|
||||
rootServiceName: '<root span not yet received>',
|
||||
startTimeUnixNano: '1643342166678000000',
|
||||
durationMs: 93,
|
||||
},
|
||||
],
|
||||
metrics: {
|
||||
inspectedTraces: 2,
|
||||
inspectedBytes: '83720',
|
||||
},
|
||||
};
|
||||
|
||||
export const traceQlResponse = {
|
||||
traces: [
|
||||
{
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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++;
|
||||
|
@ -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?: {
|
||||
|
51
public/app/plugins/datasource/tempo/utils.test.ts
Normal file
51
public/app/plugins/datasource/tempo/utils.test.ts
Normal file
@ -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');
|
||||
});
|
||||
});
|
@ -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<DataSourceApi | undefined> {
|
||||
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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user