mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Add hints to query builder (#45288)
* Add hints to query builder * Move Query hints to own component * Use replace 5m with in rate queries * Remove unused prop
This commit is contained in:
parent
574f609550
commit
9e2caa9ddc
@ -904,11 +904,11 @@ export class PrometheusDatasource
|
||||
break;
|
||||
}
|
||||
case 'ADD_HISTOGRAM_QUANTILE': {
|
||||
expression = `histogram_quantile(0.95, sum(rate(${expression}[5m])) by (le))`;
|
||||
expression = `histogram_quantile(0.95, sum(rate(${expression}[$__rate_interval])) by (le))`;
|
||||
break;
|
||||
}
|
||||
case 'ADD_RATE': {
|
||||
expression = `rate(${expression}[5m])`;
|
||||
expression = `rate(${expression}[$__rate_interval])`;
|
||||
break;
|
||||
}
|
||||
case 'ADD_SUM': {
|
||||
|
@ -23,7 +23,7 @@ describe('getQueryHints()', () => {
|
||||
|
||||
expect(hints!.length).toBe(1);
|
||||
expect(hints![0]).toMatchObject({
|
||||
label: 'Metric metric_total looks like a counter.',
|
||||
label: 'Selected metric looks like a counter.',
|
||||
fix: {
|
||||
action: {
|
||||
type: 'ADD_RATE',
|
||||
@ -48,7 +48,7 @@ describe('getQueryHints()', () => {
|
||||
let hints = getQueryHints('foo', series, datasource);
|
||||
expect(hints!.length).toBe(1);
|
||||
expect(hints![0]).toMatchObject({
|
||||
label: 'Metric foo is a counter.',
|
||||
label: 'Selected metric is a counter.',
|
||||
fix: {
|
||||
action: {
|
||||
type: 'ADD_RATE',
|
||||
@ -108,7 +108,7 @@ describe('getQueryHints()', () => {
|
||||
const hints = getQueryHints('metric_bucket', series);
|
||||
expect(hints!.length).toBe(1);
|
||||
expect(hints![0]).toMatchObject({
|
||||
label: 'Time series has buckets, you probably wanted a histogram.',
|
||||
label: 'Selected metric has buckets.',
|
||||
fix: {
|
||||
action: {
|
||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||
|
@ -13,12 +13,12 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet
|
||||
// ..._bucket metric needs a histogram_quantile()
|
||||
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
||||
if (histogramMetric) {
|
||||
const label = 'Time series has buckets, you probably wanted a histogram.';
|
||||
const label = 'Selected metric has buckets.';
|
||||
hints.push({
|
||||
type: 'HISTOGRAM_QUANTILE',
|
||||
label,
|
||||
fix: {
|
||||
label: 'Fix by adding histogram_quantile().',
|
||||
label: 'Consider calculating aggregated quantile by adding histogram_quantile().',
|
||||
action: {
|
||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||
query,
|
||||
@ -55,19 +55,19 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet
|
||||
if (counterNameMetric) {
|
||||
const simpleMetric = query.trim().match(/^\w+$/);
|
||||
const verb = certain ? 'is' : 'looks like';
|
||||
let label = `Metric ${counterNameMetric} ${verb} a counter.`;
|
||||
let label = `Selected metric ${verb} a counter.`;
|
||||
let fix: QueryFix | undefined;
|
||||
|
||||
if (simpleMetric) {
|
||||
fix = {
|
||||
label: 'Fix by adding rate().',
|
||||
label: 'Consider calculating rate of counter by adding rate().',
|
||||
action: {
|
||||
type: 'ADD_RATE',
|
||||
query,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
label = `${label} Try applying a rate() function.`;
|
||||
label = `${label} Consider calculating rate of counter by adding rate().`;
|
||||
}
|
||||
|
||||
hints.push({
|
||||
|
@ -7,6 +7,7 @@ import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||
import PromQlLanguageProvider from '../../language_provider';
|
||||
import { PromVisualQuery } from '../types';
|
||||
import { getLabelSelects } from '../testUtils';
|
||||
import { LoadingState, MutableDataFrame, PanelData, TimeRange } from '@grafana/data';
|
||||
|
||||
const defaultQuery: PromVisualQuery = {
|
||||
metric: 'random_metric',
|
||||
@ -124,9 +125,64 @@ describe('PromQueryBuilder', () => {
|
||||
openLabelNameSelect();
|
||||
await waitFor(() => expect(languageProvider.fetchLabels).toBeCalled());
|
||||
});
|
||||
|
||||
it('shows hints for histogram metrics', async () => {
|
||||
const { container } = setup({
|
||||
metric: 'histogram_metric_bucket',
|
||||
labels: [],
|
||||
operations: [],
|
||||
});
|
||||
openMetricSelect(container);
|
||||
userEvent.click(screen.getByText('histogram_metric_bucket'));
|
||||
await waitFor(() => expect(screen.getByText('hint: add histogram_quantile()')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows hints for counter metrics', async () => {
|
||||
const { container } = setup({
|
||||
metric: 'histogram_metric_sum',
|
||||
labels: [],
|
||||
operations: [],
|
||||
});
|
||||
openMetricSelect(container);
|
||||
userEvent.click(screen.getByText('histogram_metric_sum'));
|
||||
await waitFor(() => expect(screen.getByText('hint: add rate()')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows hints for counter metrics', async () => {
|
||||
const { container } = setup({
|
||||
metric: 'histogram_metric_sum',
|
||||
labels: [],
|
||||
operations: [],
|
||||
});
|
||||
openMetricSelect(container);
|
||||
userEvent.click(screen.getByText('histogram_metric_sum'));
|
||||
await waitFor(() => expect(screen.getByText('hint: add rate()')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows multiple hints', async () => {
|
||||
const data: PanelData = {
|
||||
series: [],
|
||||
state: LoadingState.Done,
|
||||
timeRange: {} as TimeRange,
|
||||
};
|
||||
for (let i = 0; i < 25; i++) {
|
||||
data.series.push(new MutableDataFrame());
|
||||
}
|
||||
const { container } = setup(
|
||||
{
|
||||
metric: 'histogram_metric_sum',
|
||||
labels: [],
|
||||
operations: [],
|
||||
},
|
||||
data
|
||||
);
|
||||
openMetricSelect(container);
|
||||
userEvent.click(screen.getByText('histogram_metric_sum'));
|
||||
await waitFor(() => expect(screen.getAllByText(/hint:/g)).toHaveLength(2));
|
||||
});
|
||||
});
|
||||
|
||||
function setup(query: PromVisualQuery = defaultQuery) {
|
||||
function setup(query: PromVisualQuery = defaultQuery, data?: PanelData) {
|
||||
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
|
||||
const datasource = new PrometheusDatasource(
|
||||
{
|
||||
@ -142,6 +198,7 @@ function setup(query: PromVisualQuery = defaultQuery) {
|
||||
datasource,
|
||||
onRunQuery: () => {},
|
||||
onChange: () => {},
|
||||
data,
|
||||
};
|
||||
|
||||
const { container } = render(<PromQueryBuilder {...props} query={query} />);
|
||||
|
@ -8,8 +8,9 @@ import { PrometheusDatasource } from '../../datasource';
|
||||
import { NestedQueryList } from './NestedQueryList';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
|
||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||
import { PromQueryBuilderHints } from './PromQueryBuilderHints';
|
||||
|
||||
export interface Props {
|
||||
query: PromVisualQuery;
|
||||
@ -17,9 +18,10 @@ export interface Props {
|
||||
onChange: (update: PromVisualQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
nested?: boolean;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery }) => {
|
||||
export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery, data }) => {
|
||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
@ -106,6 +108,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
)}
|
||||
<PromQueryBuilderHints datasource={datasource} query={query} onChange={onChange} data={data} />
|
||||
</OperationsEditorRow>
|
||||
</>
|
||||
);
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import { PanelData } from '@grafana/data';
|
||||
import React from 'react';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PromQuery } from '../../types';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { PromVisualQuery } from '../types';
|
||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
export interface Props {
|
||||
query: PromQuery;
|
||||
datasource: PrometheusDatasource;
|
||||
onChange: (update: PromQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
app?: CoreApp;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,7 +23,7 @@ export interface Props {
|
||||
* @constructor
|
||||
*/
|
||||
export function PromQueryBuilderContainer(props: Props) {
|
||||
const { query, onChange, onRunQuery, datasource } = props;
|
||||
const { query, onChange, onRunQuery, datasource, data } = props;
|
||||
|
||||
const visQuery = buildVisualQueryFromString(query.expr || '').query;
|
||||
|
||||
@ -34,7 +34,13 @@ export function PromQueryBuilderContainer(props: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PromQueryBuilder query={visQuery} datasource={datasource} onChange={onVisQueryChange} onRunQuery={onRunQuery} />
|
||||
<PromQueryBuilder
|
||||
query={visQuery}
|
||||
datasource={datasource}
|
||||
onChange={onVisQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
data={data}
|
||||
/>
|
||||
{query.editorPreview && <QueryPreview query={query.expr} />}
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,70 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PromVisualQuery } from '../types';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { GrafanaTheme2, PanelData, QueryHint } from '@grafana/data';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { Button, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export interface Props {
|
||||
query: PromVisualQuery;
|
||||
datasource: PrometheusDatasource;
|
||||
onChange: (update: PromVisualQuery) => void;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
export const PromQueryBuilderHints = React.memo<Props>(({ datasource, query, onChange, data }) => {
|
||||
const [hints, setHints] = useState<QueryHint[]>([]);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
const promQuery = { expr: promQueryModeller.renderQuery(query), refId: '' };
|
||||
// For now show only actionable hints
|
||||
const hints = datasource.getQueryHints(promQuery, data?.series || []).filter((hint) => hint.fix?.action);
|
||||
setHints(hints);
|
||||
}, [datasource, query, onChange, data, styles.hint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hints.length > 0 && (
|
||||
<div className={styles.container}>
|
||||
{hints.map((hint) => {
|
||||
return (
|
||||
<Tooltip content={`${hint.label} ${hint.fix?.label}`} key={hint.type}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const promQuery = { expr: promQueryModeller.renderQuery(query), refId: '' };
|
||||
const newPromQuery = datasource.modifyQuery(promQuery, hint!.fix!.action);
|
||||
const visualQuery = buildVisualQueryFromString(newPromQuery.expr);
|
||||
return onChange(visualQuery.query);
|
||||
}}
|
||||
fill="outline"
|
||||
size="sm"
|
||||
className={styles.hint}
|
||||
>
|
||||
{'hint: ' + hint.fix?.action?.type.toLowerCase().replace('_', ' ') + '()'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PromQueryBuilderHints.displayName = 'PromQueryBuilderHints';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
align-items: center;
|
||||
`,
|
||||
hint: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
@ -107,6 +107,7 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
||||
datasource={props.datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={props.onRunQuery}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
|
||||
|
Loading…
Reference in New Issue
Block a user