mirror of
https://github.com/grafana/grafana.git
synced 2024-11-28 03:34:15 -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 { 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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user