Prometheus: Fix instant query to run two times when exemplars enabled (#32508)

* Prometheus: Fix instant query to run two times when exemplars enabled

* Update exemplar messages

* Tempo: show empty response if response is empty
This commit is contained in:
Zoltán Bedi 2021-04-06 18:35:00 +02:00 committed by GitHub
parent 9d7d33ebb3
commit 70d49b017d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 34 deletions

View File

@ -1,49 +1,47 @@
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { FetchError } from '@grafana/runtime';
import { IconButton, InlineLabel, Tooltip, useStyles } from '@grafana/ui'; import { IconButton, InlineLabel, Tooltip, useStyles } from '@grafana/ui';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
import { PromQuery } from '../types';
interface Props { interface Props {
query: PromQuery; isEnabled: boolean;
onChange: (value: PromQuery) => void; onChange: (isEnabled: boolean) => void;
datasource: PrometheusDatasource; datasource: PrometheusDatasource;
} }
export function PromExemplarField(props: Props) { export function PromExemplarField({ datasource, onChange, isEnabled }: Props) {
const [error, setError] = useState<FetchError>(); const [error, setError] = useState<string>();
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
useEffect(() => { useEffect(() => {
const subscription = props.datasource.exemplarErrors.subscribe((err) => { const subscription = datasource.exemplarErrors.subscribe((err) => {
setError(err); setError(err);
}); });
return () => { return () => {
subscription.unsubscribe(); subscription.unsubscribe();
}; };
}, [props]); }, [datasource]);
const iconButtonStyles = cx( const iconButtonStyles = cx(
{ {
[styles.activeIcon]: !!props.query.exemplar, [styles.activeIcon]: isEnabled,
}, },
styles.eyeIcon styles.eyeIcon
); );
return ( return (
<InlineLabel width="auto"> <InlineLabel width="auto">
<Tooltip content={!!error ? 'Exemplars are not supported in this version of prometheus.' : ''}> <Tooltip content={error ?? ''}>
<div className={styles.iconWrapper}> <div className={styles.iconWrapper}>
Exemplars Exemplars
<IconButton <IconButton
name="eye" name="eye"
tooltip={!!props.query.exemplar ? 'Disable query with exemplars' : 'Enable query with exemplars'} tooltip={isEnabled ? 'Disable query with exemplars' : 'Enable query with exemplars'}
disabled={!!error} disabled={!!error}
className={iconButtonStyles} className={iconButtonStyles}
onClick={() => { onClick={() => {
props.onChange({ ...props.query, exemplar: !props.query.exemplar }); onChange(!isEnabled);
}} }}
/> />
</div> </div>

View File

@ -77,7 +77,11 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
/> />
</div> </div>
<PromExemplarField query={query} onChange={onChange} datasource={datasource} /> <PromExemplarField
isEnabled={Boolean(query.exemplar)}
onChange={(isEnabled) => onChange({ ...query, exemplar: isEnabled })}
datasource={datasource}
/>
</div> </div>
); );
} }

View File

@ -32,6 +32,7 @@ interface State {
interval?: string; interval?: string;
intervalFactorOption: SelectableValue<number>; intervalFactorOption: SelectableValue<number>;
instant: boolean; instant: boolean;
exemplar: boolean;
} }
export class PromQueryEditor extends PureComponent<Props, State> { export class PromQueryEditor extends PureComponent<Props, State> {
@ -55,6 +56,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0], INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0],
// Switch options // Switch options
instant: Boolean(query.instant), instant: Boolean(query.instant),
exemplar: Boolean(query.exemplar),
}; };
} }
@ -90,6 +92,11 @@ export class PromQueryEditor extends PureComponent<Props, State> {
this.setState({ legendFormat }); this.setState({ legendFormat });
}; };
onExemplarChange = (isEnabled: boolean) => {
this.query.exemplar = isEnabled;
this.setState({ exemplar: isEnabled }, this.onRunQuery);
};
onRunQuery = () => { onRunQuery = () => {
const { query } = this; const { query } = this;
// Change of query.hide happens outside of this component and is just passed as prop. We have to update it when running queries. // Change of query.hide happens outside of this component and is just passed as prop. We have to update it when running queries.
@ -99,8 +106,8 @@ export class PromQueryEditor extends PureComponent<Props, State> {
}; };
render() { render() {
const { datasource, query, range, data, onChange } = this.props; const { datasource, query, range, data } = this.props;
const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state; const { formatOption, instant, interval, intervalFactorOption, legendFormat, exemplar } = this.state;
return ( return (
<div> <div>
@ -186,7 +193,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
</InlineFormLabel> </InlineFormLabel>
</div> </div>
<PromExemplarField query={this.query} onChange={onChange} datasource={this.props.datasource} /> <PromExemplarField isEnabled={exemplar} onChange={this.onExemplarChange} datasource={datasource} />
</div> </div>
</div> </div>
); );

View File

@ -189,16 +189,8 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
"getPrometheusTime": [MockFunction], "getPrometheusTime": [MockFunction],
} }
} }
onChange={[MockFunction]} isEnabled={true}
query={ onChange={[Function]}
Object {
"exemplar": true,
"expr": "",
"interval": "",
"legendFormat": "",
"refId": "A",
}
}
/> />
</div> </div>
</div> </div>

View File

@ -1746,6 +1746,32 @@ describe('prepareTargets', () => {
}); });
expect(activeTargets[0]).toEqual(target); expect(activeTargets[0]).toEqual(target);
}); });
it('should give back 2 targets when exemplar enabled', () => {
const target: PromQuery = {
refId: 'A',
expr: 'up',
exemplar: true,
};
const { queries, activeTargets } = getPrepareTargetsContext(target);
expect(queries).toHaveLength(2);
expect(activeTargets).toHaveLength(2);
expect(activeTargets[0].exemplar).toBe(true);
expect(activeTargets[1].exemplar).toBe(false);
});
it('should give back 1 target when exemplar and instant are enabled', () => {
const target: PromQuery = {
refId: 'A',
expr: 'up',
exemplar: true,
instant: true,
};
const { queries, activeTargets } = getPrepareTargetsContext(target);
expect(queries).toHaveLength(1);
expect(activeTargets).toHaveLength(1);
expect(activeTargets[0].instant).toBe(true);
});
}); });
describe('when run from Explore', () => { describe('when run from Explore', () => {

View File

@ -44,7 +44,8 @@ import { PrometheusVariableSupport } from './variables';
import PrometheusMetricFindQuery from './metric_find_query'; import PrometheusMetricFindQuery from './metric_find_query';
export const ANNOTATION_QUERY_STEP_DEFAULT = '60s'; export const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
const GET_AND_POST_MEDATADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels']; const EXEMPLARS_NOT_AVAILABLE = 'Exemplars for this data source are not available.';
const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels'];
export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> { export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> {
type: string; type: string;
@ -62,7 +63,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined; exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined;
lookupsDisabled: boolean; lookupsDisabled: boolean;
customQueryParameters: any; customQueryParameters: any;
exemplarErrors: Subject<FetchError> = new Subject(); exemplarErrors: Subject<string> = new Subject();
constructor( constructor(
instanceSettings: DataSourceInstanceSettings<PromOptions>, instanceSettings: DataSourceInstanceSettings<PromOptions>,
@ -147,7 +148,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
} }
// If URL includes endpoint that supports POST and GET method, try to use configured method. This might fail as POST is supported only in v2.10+. // If URL includes endpoint that supports POST and GET method, try to use configured method. This might fail as POST is supported only in v2.10+.
if (GET_AND_POST_MEDATADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) { if (GET_AND_POST_METADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
try { try {
return await this._request<T>(url, data, { method: this.httpMethod, hideFromInspector: true }).toPromise(); return await this._request<T>(url, data, { method: this.httpMethod, hideFromInspector: true }).toPromise();
} catch (err) { } catch (err) {
@ -238,12 +239,17 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
queries.push(this.createQuery(instantTarget, options, start, end)); queries.push(this.createQuery(instantTarget, options, start, end));
activeTargets.push(instantTarget); activeTargets.push(instantTarget);
} else { } else {
if (target.exemplar) { // It doesn't make sense to query for exemplars in dashboard if only instant is selected
if (target.exemplar && !target.instant) {
const exemplarTarget = cloneDeep(target); const exemplarTarget = cloneDeep(target);
exemplarTarget.requestId += '_exemplar'; exemplarTarget.requestId += '_exemplar';
target.exemplar = false; target.exemplar = false;
queries.push(this.createQuery(exemplarTarget, options, start, end)); queries.push(this.createQuery(exemplarTarget, options, start, end));
activeTargets.push(exemplarTarget); activeTargets.push(exemplarTarget);
this.exemplarErrors.next();
}
if (target.exemplar && target.instant) {
this.exemplarErrors.next('Exemplars are not available for instant queries.');
} }
queries.push(this.createQuery(target, options, start, end)); queries.push(this.createQuery(target, options, start, end));
activeTargets.push(target); activeTargets.push(target);
@ -310,8 +316,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
if (query.exemplar) { if (query.exemplar) {
return this.getExemplars(query).pipe( return this.getExemplars(query).pipe(
catchError((err: FetchError) => { catchError(() => {
this.exemplarErrors.next(err); this.exemplarErrors.next(EXEMPLARS_NOT_AVAILABLE);
return of({ return of({
data: [], data: [],
state: LoadingState.Done, state: LoadingState.Done,
@ -357,8 +363,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
if (query.exemplar) { if (query.exemplar) {
return this.getExemplars(query).pipe( return this.getExemplars(query).pipe(
catchError((err: FetchError) => { catchError(() => {
this.exemplarErrors.next(err); this.exemplarErrors.next(EXEMPLARS_NOT_AVAILABLE);
return of({ return of({
data: [], data: [],
state: LoadingState.Done, state: LoadingState.Done,

View File

@ -7,6 +7,7 @@ import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
Field, Field,
FieldType, FieldType,
MutableDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime'; import { DataSourceWithBackend } from '@grafana/runtime';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -32,6 +33,11 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery> {
// Seems like we can't just map the values as the frame we got from backend has some default processing // Seems like we can't just map the values as the frame we got from backend has some default processing
// and will stringify the json back when we try to set it. So we create a new field and swap it instead. // and will stringify the json back when we try to set it. So we create a new field and swap it instead.
const frame: DataFrame = response.data[0]; const frame: DataFrame = response.data[0];
if (!frame) {
return emptyDataQueryResponse;
}
for (const fieldName of ['serviceTags', 'logs', 'tags']) { for (const fieldName of ['serviceTags', 'logs', 'tags']) {
const field = frame.fields.find((f) => f.name === fieldName); const field = frame.fields.find((f) => f.name === fieldName);
if (field) { if (field) {
@ -70,3 +76,20 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery> {
return query.query; return query.query;
} }
} }
const emptyDataQueryResponse = {
data: [
new MutableDataFrame({
fields: [
{
name: 'trace',
type: FieldType.trace,
values: [],
},
],
meta: {
preferredVisualisationType: 'trace',
},
}),
],
};