From 9e2caa9ddcaba7c4e5e8f6f8c5f0a52385185f50 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 11 Feb 2022 16:50:35 +0100 Subject: [PATCH] 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 --- .../datasource/prometheus/datasource.ts | 4 +- .../datasource/prometheus/query_hints.test.ts | 6 +- .../datasource/prometheus/query_hints.ts | 10 +-- .../components/PromQueryBuilder.test.tsx | 59 +++++++++++++++- .../components/PromQueryBuilder.tsx | 7 +- .../components/PromQueryBuilderContainer.tsx | 16 +++-- .../components/PromQueryBuilderHints.tsx | 70 +++++++++++++++++++ .../components/PromQueryEditorSelector.tsx | 1 + 8 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderHints.tsx diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 4e64d73abbe..36db41a738c 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -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': { diff --git a/public/app/plugins/datasource/prometheus/query_hints.test.ts b/public/app/plugins/datasource/prometheus/query_hints.test.ts index 88948718049..381934e694d 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.test.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.test.ts @@ -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', diff --git a/public/app/plugins/datasource/prometheus/query_hints.ts b/public/app/plugins/datasource/prometheus/query_hints.ts index 0d620b9ec52..5ab6b8c2832 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.ts @@ -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({ diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx index d477c475d24..04f81e765c9 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx @@ -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(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx index 9eddc271c4b..ea52381ee3c 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx @@ -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(({ datasource, query, onChange, onRunQuery }) => { +export const PromQueryBuilder = React.memo(({ datasource, query, onChange, onRunQuery, data }) => { const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { onChange({ ...query, labels }); }; @@ -106,6 +108,7 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange {query.binaryQueries && query.binaryQueries.length > 0 && ( )} + ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx index 791678c860f..791e97f3393 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx @@ -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 ( <> - + {query.editorPreview && } ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderHints.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderHints.tsx new file mode 100644 index 00000000000..7fe177d6ab1 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderHints.tsx @@ -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(({ datasource, query, onChange, data }) => { + const [hints, setHints] = useState([]); + 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 && ( +
+ {hints.map((hint) => { + return ( + + + + ); + })} +
+ )} + + ); +}); + +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)}; + `, + }; +}; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index 94f65d8f025..2062f6a71e4 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -107,6 +107,7 @@ export const PromQueryEditorSelector = React.memo((props) datasource={props.datasource} onChange={onChange} onRunQuery={props.onRunQuery} + data={data} /> )} {editorMode === QueryEditorMode.Explain && }