Tempo: Show warning if search is not available. Add opt-out config (#39489)

* Tempo: Show warning if search is not available. Add opt-out config
This commit is contained in:
Connor Lindsey 2021-09-27 07:22:49 -06:00 committed by GitHub
parent 11e362ff0a
commit 24479ff6b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 132 deletions

View File

@ -4,6 +4,7 @@ import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
import React from 'react';
import { ServiceMapSettings } from './ServiceMapSettings';
import { config } from '@grafana/runtime';
import { SearchSettings } from './SearchSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@ -21,7 +22,14 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
{config.featureToggles.tempoServiceGraph && (
<ServiceMapSettings options={options} onOptionsChange={onOptionsChange} />
<div className="gf-form-group">
<ServiceMapSettings options={options} onOptionsChange={onOptionsChange} />
</div>
)}
{config.featureToggles.tempoSearch && (
<div className="gf-form-group">
<SearchSettings options={options} onOptionsChange={onOptionsChange} />
</div>
)}
</>
);

View File

@ -9,15 +9,20 @@ import {
TypeaheadInput,
TypeaheadOutput,
Select,
Alert,
useStyles2,
} from '@grafana/ui';
import { tokenizer } from './syntax';
import Prism from 'prismjs';
import { Node } from 'slate';
import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import TempoLanguageProvider from './language_provider';
import { TempoDatasource, TempoQuery } from './datasource';
import { debounce } from 'lodash';
import { dispatch } from 'app/store/store';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
interface Props {
datasource: TempoDatasource;
@ -40,19 +45,17 @@ const plugins = [
Prism.languages[PRISM_LANGUAGE] = tokenizer;
const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => {
const styles = useStyles2(getStyles);
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false);
const [autocomplete, setAutocomplete] = useState<{
serviceNameOptions: Array<SelectableValue<string>>;
selectedServiceName: SelectableValue<string> | undefined;
spanNameOptions: Array<SelectableValue<string>>;
selectedSpanName: SelectableValue<string> | undefined;
}>({
serviceNameOptions: [],
selectedServiceName: undefined,
spanNameOptions: [],
selectedSpanName: undefined,
});
const [error, setError] = useState(null);
const fetchServiceNameOptions = useMemo(
() =>
@ -82,10 +85,20 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
useEffect(() => {
const fetchAutocomplete = async () => {
await languageProvider.start();
await fetchServiceNameOptions();
await fetchSpanNameOptions();
setHasSyntaxLoaded(true);
try {
await languageProvider.start();
const serviceNameOptions = await languageProvider.getOptions('service.name');
const spanNameOptions = await languageProvider.getOptions('name');
setHasSyntaxLoaded(true);
setAutocomplete({ serviceNameOptions, spanNameOptions });
} catch (error) {
// Display message if Tempo is connected but search 404's
if (error?.status === 404) {
setError(error);
} else {
dispatch(notifyApp(createErrorNotification('Error', error)));
}
}
};
fetchAutocomplete();
}, [languageProvider, fetchServiceNameOptions, fetchSpanNameOptions]);
@ -109,111 +122,129 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
};
return (
<div className={css({ maxWidth: '500px' })}>
<InlineFieldRow>
<InlineField label="Service Name" labelWidth={14} grow>
<Select
menuShouldPortal
options={autocomplete.serviceNameOptions}
value={query.serviceName || ''}
onChange={(v) => {
onChange({
...query,
serviceName: v?.value || undefined,
});
}}
placeholder="Select a service"
onOpenMenu={fetchServiceNameOptions}
isClearable
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Span Name" labelWidth={14} grow>
<Select
menuShouldPortal
options={autocomplete.spanNameOptions}
value={query.spanName || ''}
onChange={(v) => {
onChange({
...query,
spanName: v?.value || undefined,
});
}}
placeholder="Select a span"
onOpenMenu={fetchSpanNameOptions}
isClearable
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in the logfmt format.">
<QueryField
additionalPlugins={plugins}
query={query.search}
onTypeahead={onTypeahead}
onBlur={onBlur}
onChange={(value) => {
onChange({
...query,
search: value,
});
}}
placeholder="http.status_code=200 error=true"
cleanText={cleanText}
onRunQuery={onRunQuery}
syntaxLoaded={hasSyntaxLoaded}
portalOrigin="tempo"
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Min Duration" labelWidth={14} grow>
<Input
value={query.minDuration || ''}
placeholder={durationPlaceholder}
onChange={(v) =>
onChange({
...query,
minDuration: v.currentTarget.value,
})
}
onKeyDown={onKeyDown}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Max Duration" labelWidth={14} grow>
<Input
value={query.maxDuration || ''}
placeholder={durationPlaceholder}
onChange={(v) =>
onChange({
...query,
maxDuration: v.currentTarget.value,
})
}
onKeyDown={onKeyDown}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Limit" labelWidth={14} grow tooltip="Maximum numbers of returned results">
<Input
value={query.limit || ''}
type="number"
onChange={(v) =>
onChange({
...query,
limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined,
})
}
onKeyDown={onKeyDown}
/>
</InlineField>
</InlineFieldRow>
</div>
<>
<div className={styles.container}>
<InlineFieldRow>
<InlineField label="Service Name" labelWidth={14} grow>
<Select
menuShouldPortal
options={autocomplete.serviceNameOptions}
value={query.serviceName || ''}
onChange={(v) => {
onChange({
...query,
serviceName: v?.value || undefined,
});
}}
placeholder="Select a service"
onOpenMenu={fetchServiceNameOptions}
isClearable
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Span Name" labelWidth={14} grow>
<Select
menuShouldPortal
options={autocomplete.spanNameOptions}
value={query.spanName || ''}
onChange={(v) => {
onChange({
...query,
spanName: v?.value || undefined,
});
}}
placeholder="Select a span"
onOpenMenu={fetchSpanNameOptions}
isClearable
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in the logfmt format.">
<QueryField
additionalPlugins={plugins}
query={query.search}
onTypeahead={onTypeahead}
onBlur={onBlur}
onChange={(value) => {
onChange({
...query,
search: value,
});
}}
placeholder="http.status_code=200 error=true"
cleanText={cleanText}
onRunQuery={onRunQuery}
syntaxLoaded={hasSyntaxLoaded}
portalOrigin="tempo"
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Min Duration" labelWidth={14} grow>
<Input
value={query.minDuration || ''}
placeholder={durationPlaceholder}
onChange={(v) =>
onChange({
...query,
minDuration: v.currentTarget.value,
})
}
onKeyDown={onKeyDown}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Max Duration" labelWidth={14} grow>
<Input
value={query.maxDuration || ''}
placeholder={durationPlaceholder}
onChange={(v) =>
onChange({
...query,
maxDuration: v.currentTarget.value,
})
}
onKeyDown={onKeyDown}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Limit" labelWidth={14} grow tooltip="Maximum numbers of returned results">
<Input
value={query.limit || ''}
type="number"
onChange={(v) =>
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/${datasource.uid}`}>datasource settings</a>.
</Alert>
) : null}
</>
);
};
export default NativeSearch;
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
max-width: 500px;
`,
alert: css`
max-width: 75ch;
margin-top: ${theme.spacing(2)};
`,
});

View File

@ -99,8 +99,8 @@ class TempoQueryFieldComponent extends React.PureComponent<Props, State> {
queryTypeOptions.push({ value: 'serviceMap', label: 'Service Map' });
}
if (config.featureToggles.tempoSearch) {
queryTypeOptions.unshift({ value: 'nativeSearch', label: 'Search' });
if (config.featureToggles.tempoSearch && !datasource?.search?.hide) {
queryTypeOptions.unshift({ value: 'nativeSearch', label: 'Search - Beta' });
}
if (logsDatasourceUid) {

View File

@ -0,0 +1,41 @@
import { css } from '@emotion/css';
import { DataSourcePluginOptionsEditorProps, GrafanaTheme, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, useStyles } from '@grafana/ui';
import React from 'react';
import { TempoJsonData } from './datasource';
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}
export function SearchSettings({ options, onOptionsChange }: Props) {
const styles = useStyles(getStyles);
return (
<div className={styles.container}>
<h3 className="page-heading">Search</h3>
<InlineFieldRow className={styles.row}>
<InlineField tooltip="Removes the Search tab from the Tempo query editor." label="Hide search" labelWidth={26}>
<InlineSwitch
value={options.jsonData.search?.hide}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
...options.jsonData.search,
hide: event.currentTarget.checked,
})
}
/>
</InlineField>
</InlineFieldRow>
</div>
);
}
const getStyles = (theme: GrafanaTheme) => ({
container: css`
label: container;
width: 100%;
`,
row: css`
label: row;
align-items: baseline;
`,
});

View File

@ -35,6 +35,9 @@ export interface TempoJsonData extends DataSourceJsonData {
serviceMap?: {
datasourceUid?: string;
};
search?: {
hide?: boolean;
};
}
export type TempoQuery = {
@ -55,12 +58,16 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
serviceMap?: {
datasourceUid?: string;
};
search?: {
hide?: boolean;
};
uploadedJson?: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings<TempoJsonData>) {
super(instanceSettings);
this.tracesToLogs = instanceSettings.jsonData.tracesToLogs;
this.serviceMap = instanceSettings.jsonData.serviceMap;
this.search = instanceSettings.jsonData.search;
}
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> {

View File

@ -13,15 +13,9 @@ export default class TempoLanguageProvider extends LanguageProvider {
Object.assign(this, initialValues);
}
request = async (url: string, defaultValue: any, params = {}) => {
try {
const res = await this.datasource.metadataRequest(url, params);
return res?.data;
} catch (error) {
console.error(error);
}
return defaultValue;
request = async (url: string, params = {}) => {
const res = await this.datasource.metadataRequest(url, params);
return res?.data;
};
start = async () => {
@ -30,12 +24,8 @@ export default class TempoLanguageProvider extends LanguageProvider {
};
async fetchTags() {
try {
const response = await this.request('/api/search/tags', []);
this.tags = response.tagNames;
} catch (error) {
console.error(error);
}
const response = await this.request('/api/search/tags', []);
this.tags = response.tagNames;
}
provideCompletionItems = async (
@ -88,7 +78,7 @@ export default class TempoLanguageProvider extends LanguageProvider {
}
async getOptions(tag: string): Promise<Array<SelectableValue<string>>> {
const response = await this.request(`/api/search/tag/${tag}/values`, []);
const response = await this.request(`/api/search/tag/${tag}/values`);
let options: Array<SelectableValue<string>> = [];
if (response && response.tagValues) {