mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
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:
parent
9d7d33ebb3
commit
70d49b017d
@ -1,49 +1,47 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { FetchError } from '@grafana/runtime';
|
||||
import { IconButton, InlineLabel, Tooltip, useStyles } from '@grafana/ui';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
interface Props {
|
||||
query: PromQuery;
|
||||
onChange: (value: PromQuery) => void;
|
||||
isEnabled: boolean;
|
||||
onChange: (isEnabled: boolean) => void;
|
||||
datasource: PrometheusDatasource;
|
||||
}
|
||||
|
||||
export function PromExemplarField(props: Props) {
|
||||
const [error, setError] = useState<FetchError>();
|
||||
export function PromExemplarField({ datasource, onChange, isEnabled }: Props) {
|
||||
const [error, setError] = useState<string>();
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = props.datasource.exemplarErrors.subscribe((err) => {
|
||||
const subscription = datasource.exemplarErrors.subscribe((err) => {
|
||||
setError(err);
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [props]);
|
||||
}, [datasource]);
|
||||
|
||||
const iconButtonStyles = cx(
|
||||
{
|
||||
[styles.activeIcon]: !!props.query.exemplar,
|
||||
[styles.activeIcon]: isEnabled,
|
||||
},
|
||||
styles.eyeIcon
|
||||
);
|
||||
|
||||
return (
|
||||
<InlineLabel width="auto">
|
||||
<Tooltip content={!!error ? 'Exemplars are not supported in this version of prometheus.' : ''}>
|
||||
<Tooltip content={error ?? ''}>
|
||||
<div className={styles.iconWrapper}>
|
||||
Exemplars
|
||||
<IconButton
|
||||
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}
|
||||
className={iconButtonStyles}
|
||||
onClick={() => {
|
||||
props.onChange({ ...props.query, exemplar: !props.query.exemplar });
|
||||
onChange(!isEnabled);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -77,7 +77,11 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PromExemplarField query={query} onChange={onChange} datasource={datasource} />
|
||||
<PromExemplarField
|
||||
isEnabled={Boolean(query.exemplar)}
|
||||
onChange={(isEnabled) => onChange({ ...query, exemplar: isEnabled })}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ interface State {
|
||||
interval?: string;
|
||||
intervalFactorOption: SelectableValue<number>;
|
||||
instant: boolean;
|
||||
exemplar: boolean;
|
||||
}
|
||||
|
||||
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],
|
||||
// Switch options
|
||||
instant: Boolean(query.instant),
|
||||
exemplar: Boolean(query.exemplar),
|
||||
};
|
||||
}
|
||||
|
||||
@ -90,6 +92,11 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
||||
this.setState({ legendFormat });
|
||||
};
|
||||
|
||||
onExemplarChange = (isEnabled: boolean) => {
|
||||
this.query.exemplar = isEnabled;
|
||||
this.setState({ exemplar: isEnabled }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onRunQuery = () => {
|
||||
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.
|
||||
@ -99,8 +106,8 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, query, range, data, onChange } = this.props;
|
||||
const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state;
|
||||
const { datasource, query, range, data } = this.props;
|
||||
const { formatOption, instant, interval, intervalFactorOption, legendFormat, exemplar } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -186,7 +193,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
||||
</InlineFormLabel>
|
||||
</div>
|
||||
|
||||
<PromExemplarField query={this.query} onChange={onChange} datasource={this.props.datasource} />
|
||||
<PromExemplarField isEnabled={exemplar} onChange={this.onExemplarChange} datasource={datasource} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -189,16 +189,8 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
"getPrometheusTime": [MockFunction],
|
||||
}
|
||||
}
|
||||
onChange={[MockFunction]}
|
||||
query={
|
||||
Object {
|
||||
"exemplar": true,
|
||||
"expr": "",
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A",
|
||||
}
|
||||
}
|
||||
isEnabled={true}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1746,6 +1746,32 @@ describe('prepareTargets', () => {
|
||||
});
|
||||
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', () => {
|
||||
|
@ -44,7 +44,8 @@ import { PrometheusVariableSupport } from './variables';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
|
||||
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> {
|
||||
type: string;
|
||||
@ -62,7 +63,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined;
|
||||
lookupsDisabled: boolean;
|
||||
customQueryParameters: any;
|
||||
exemplarErrors: Subject<FetchError> = new Subject();
|
||||
exemplarErrors: Subject<string> = new Subject();
|
||||
|
||||
constructor(
|
||||
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 (GET_AND_POST_MEDATADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
|
||||
if (GET_AND_POST_METADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
|
||||
try {
|
||||
return await this._request<T>(url, data, { method: this.httpMethod, hideFromInspector: true }).toPromise();
|
||||
} catch (err) {
|
||||
@ -238,12 +239,17 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
queries.push(this.createQuery(instantTarget, options, start, end));
|
||||
activeTargets.push(instantTarget);
|
||||
} 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);
|
||||
exemplarTarget.requestId += '_exemplar';
|
||||
target.exemplar = false;
|
||||
queries.push(this.createQuery(exemplarTarget, options, start, end));
|
||||
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));
|
||||
activeTargets.push(target);
|
||||
@ -310,8 +316,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
|
||||
if (query.exemplar) {
|
||||
return this.getExemplars(query).pipe(
|
||||
catchError((err: FetchError) => {
|
||||
this.exemplarErrors.next(err);
|
||||
catchError(() => {
|
||||
this.exemplarErrors.next(EXEMPLARS_NOT_AVAILABLE);
|
||||
return of({
|
||||
data: [],
|
||||
state: LoadingState.Done,
|
||||
@ -357,8 +363,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
|
||||
if (query.exemplar) {
|
||||
return this.getExemplars(query).pipe(
|
||||
catchError((err: FetchError) => {
|
||||
this.exemplarErrors.next(err);
|
||||
catchError(() => {
|
||||
this.exemplarErrors.next(EXEMPLARS_NOT_AVAILABLE);
|
||||
return of({
|
||||
data: [],
|
||||
state: LoadingState.Done,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
Field,
|
||||
FieldType,
|
||||
MutableDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
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
|
||||
// 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];
|
||||
|
||||
if (!frame) {
|
||||
return emptyDataQueryResponse;
|
||||
}
|
||||
|
||||
for (const fieldName of ['serviceTags', 'logs', 'tags']) {
|
||||
const field = frame.fields.find((f) => f.name === fieldName);
|
||||
if (field) {
|
||||
@ -70,3 +76,20 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery> {
|
||||
return query.query;
|
||||
}
|
||||
}
|
||||
|
||||
const emptyDataQueryResponse = {
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'trace',
|
||||
type: FieldType.trace,
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user