mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
11e362ff0a
commit
24479ff6b0
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
@ -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) {
|
||||||
|
41
public/app/plugins/datasource/tempo/SearchSettings.tsx
Normal file
41
public/app/plugins/datasource/tempo/SearchSettings.tsx
Normal 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;
|
||||||
|
`,
|
||||||
|
});
|
@ -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> {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user