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 React from 'react';
import { ServiceMapSettings } from './ServiceMapSettings'; import { ServiceMapSettings } from './ServiceMapSettings';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { SearchSettings } from './SearchSettings';
export type Props = DataSourcePluginOptionsEditorProps; export type Props = DataSourcePluginOptionsEditorProps;
@ -21,7 +22,14 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} /> <TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div> </div>
{config.featureToggles.tempoServiceGraph && ( {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, TypeaheadInput,
TypeaheadOutput, TypeaheadOutput,
Select, Select,
Alert,
useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { tokenizer } from './syntax'; import { tokenizer } from './syntax';
import Prism from 'prismjs'; import Prism from 'prismjs';
import { Node } from 'slate'; import { Node } from 'slate';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import TempoLanguageProvider from './language_provider'; import TempoLanguageProvider from './language_provider';
import { TempoDatasource, TempoQuery } from './datasource'; import { TempoDatasource, TempoQuery } from './datasource';
import { debounce } from 'lodash'; 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 { interface Props {
datasource: TempoDatasource; datasource: TempoDatasource;
@ -40,19 +45,17 @@ const plugins = [
Prism.languages[PRISM_LANGUAGE] = tokenizer; Prism.languages[PRISM_LANGUAGE] = tokenizer;
const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => { const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => {
const styles = useStyles2(getStyles);
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false); const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false);
const [autocomplete, setAutocomplete] = useState<{ const [autocomplete, setAutocomplete] = useState<{
serviceNameOptions: Array<SelectableValue<string>>; serviceNameOptions: Array<SelectableValue<string>>;
selectedServiceName: SelectableValue<string> | undefined;
spanNameOptions: Array<SelectableValue<string>>; spanNameOptions: Array<SelectableValue<string>>;
selectedSpanName: SelectableValue<string> | undefined;
}>({ }>({
serviceNameOptions: [], serviceNameOptions: [],
selectedServiceName: undefined,
spanNameOptions: [], spanNameOptions: [],
selectedSpanName: undefined,
}); });
const [error, setError] = useState(null);
const fetchServiceNameOptions = useMemo( const fetchServiceNameOptions = useMemo(
() => () =>
@ -82,10 +85,20 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
useEffect(() => { useEffect(() => {
const fetchAutocomplete = async () => { const fetchAutocomplete = async () => {
await languageProvider.start(); try {
await fetchServiceNameOptions(); await languageProvider.start();
await fetchSpanNameOptions(); const serviceNameOptions = await languageProvider.getOptions('service.name');
setHasSyntaxLoaded(true); 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(); fetchAutocomplete();
}, [languageProvider, fetchServiceNameOptions, fetchSpanNameOptions]); }, [languageProvider, fetchServiceNameOptions, fetchSpanNameOptions]);
@ -109,111 +122,129 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
}; };
return ( return (
<div className={css({ maxWidth: '500px' })}> <>
<InlineFieldRow> <div className={styles.container}>
<InlineField label="Service Name" labelWidth={14} grow> <InlineFieldRow>
<Select <InlineField label="Service Name" labelWidth={14} grow>
menuShouldPortal <Select
options={autocomplete.serviceNameOptions} menuShouldPortal
value={query.serviceName || ''} options={autocomplete.serviceNameOptions}
onChange={(v) => { value={query.serviceName || ''}
onChange({ onChange={(v) => {
...query, onChange({
serviceName: v?.value || undefined, ...query,
}); serviceName: v?.value || undefined,
}} });
placeholder="Select a service" }}
onOpenMenu={fetchServiceNameOptions} placeholder="Select a service"
isClearable onOpenMenu={fetchServiceNameOptions}
/> isClearable
</InlineField> />
</InlineFieldRow> </InlineField>
<InlineFieldRow> </InlineFieldRow>
<InlineField label="Span Name" labelWidth={14} grow> <InlineFieldRow>
<Select <InlineField label="Span Name" labelWidth={14} grow>
menuShouldPortal <Select
options={autocomplete.spanNameOptions} menuShouldPortal
value={query.spanName || ''} options={autocomplete.spanNameOptions}
onChange={(v) => { value={query.spanName || ''}
onChange({ onChange={(v) => {
...query, onChange({
spanName: v?.value || undefined, ...query,
}); spanName: v?.value || undefined,
}} });
placeholder="Select a span" }}
onOpenMenu={fetchSpanNameOptions} placeholder="Select a span"
isClearable onOpenMenu={fetchSpanNameOptions}
/> isClearable
</InlineField> />
</InlineFieldRow> </InlineField>
<InlineFieldRow> </InlineFieldRow>
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in the logfmt format."> <InlineFieldRow>
<QueryField <InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in the logfmt format.">
additionalPlugins={plugins} <QueryField
query={query.search} additionalPlugins={plugins}
onTypeahead={onTypeahead} query={query.search}
onBlur={onBlur} onTypeahead={onTypeahead}
onChange={(value) => { onBlur={onBlur}
onChange({ onChange={(value) => {
...query, onChange({
search: value, ...query,
}); search: value,
}} });
placeholder="http.status_code=200 error=true" }}
cleanText={cleanText} placeholder="http.status_code=200 error=true"
onRunQuery={onRunQuery} cleanText={cleanText}
syntaxLoaded={hasSyntaxLoaded} onRunQuery={onRunQuery}
portalOrigin="tempo" syntaxLoaded={hasSyntaxLoaded}
/> portalOrigin="tempo"
</InlineField> />
</InlineFieldRow> </InlineField>
<InlineFieldRow> </InlineFieldRow>
<InlineField label="Min Duration" labelWidth={14} grow> <InlineFieldRow>
<Input <InlineField label="Min Duration" labelWidth={14} grow>
value={query.minDuration || ''} <Input
placeholder={durationPlaceholder} value={query.minDuration || ''}
onChange={(v) => placeholder={durationPlaceholder}
onChange({ onChange={(v) =>
...query, onChange({
minDuration: v.currentTarget.value, ...query,
}) minDuration: v.currentTarget.value,
} })
onKeyDown={onKeyDown} }
/> onKeyDown={onKeyDown}
</InlineField> />
</InlineFieldRow> </InlineField>
<InlineFieldRow> </InlineFieldRow>
<InlineField label="Max Duration" labelWidth={14} grow> <InlineFieldRow>
<Input <InlineField label="Max Duration" labelWidth={14} grow>
value={query.maxDuration || ''} <Input
placeholder={durationPlaceholder} value={query.maxDuration || ''}
onChange={(v) => placeholder={durationPlaceholder}
onChange({ onChange={(v) =>
...query, onChange({
maxDuration: v.currentTarget.value, ...query,
}) maxDuration: v.currentTarget.value,
} })
onKeyDown={onKeyDown} }
/> onKeyDown={onKeyDown}
</InlineField> />
</InlineFieldRow> </InlineField>
<InlineFieldRow> </InlineFieldRow>
<InlineField label="Limit" labelWidth={14} grow tooltip="Maximum numbers of returned results"> <InlineFieldRow>
<Input <InlineField label="Limit" labelWidth={14} grow tooltip="Maximum numbers of returned results">
value={query.limit || ''} <Input
type="number" value={query.limit || ''}
onChange={(v) => type="number"
onChange({ onChange={(v) =>
...query, onChange({
limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined, ...query,
}) limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined,
} })
onKeyDown={onKeyDown} }
/> onKeyDown={onKeyDown}
</InlineField> />
</InlineFieldRow> </InlineField>
</div> </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; 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' }); queryTypeOptions.push({ value: 'serviceMap', label: 'Service Map' });
} }
if (config.featureToggles.tempoSearch) { if (config.featureToggles.tempoSearch && !datasource?.search?.hide) {
queryTypeOptions.unshift({ value: 'nativeSearch', label: 'Search' }); queryTypeOptions.unshift({ value: 'nativeSearch', label: 'Search - Beta' });
} }
if (logsDatasourceUid) { 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?: { serviceMap?: {
datasourceUid?: string; datasourceUid?: string;
}; };
search?: {
hide?: boolean;
};
} }
export type TempoQuery = { export type TempoQuery = {
@ -55,12 +58,16 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
serviceMap?: { serviceMap?: {
datasourceUid?: string; datasourceUid?: string;
}; };
search?: {
hide?: boolean;
};
uploadedJson?: string | ArrayBuffer | null = null; uploadedJson?: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings<TempoJsonData>) { constructor(private instanceSettings: DataSourceInstanceSettings<TempoJsonData>) {
super(instanceSettings); super(instanceSettings);
this.tracesToLogs = instanceSettings.jsonData.tracesToLogs; this.tracesToLogs = instanceSettings.jsonData.tracesToLogs;
this.serviceMap = instanceSettings.jsonData.serviceMap; this.serviceMap = instanceSettings.jsonData.serviceMap;
this.search = instanceSettings.jsonData.search;
} }
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> { query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> {

View File

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