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
pull/45388/head
Ivana Huckova 3 years ago committed by GitHub
parent 574f609550
commit 9e2caa9ddc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      public/app/plugins/datasource/prometheus/datasource.ts
  2. 6
      public/app/plugins/datasource/prometheus/query_hints.test.ts
  3. 10
      public/app/plugins/datasource/prometheus/query_hints.ts
  4. 59
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx
  5. 7
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx
  6. 16
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx
  7. 70
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderHints.tsx
  8. 1
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx

@ -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…
Cancel
Save