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;
|
break;
|
||||||
}
|
}
|
||||||
case 'ADD_HISTOGRAM_QUANTILE': {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 'ADD_RATE': {
|
case 'ADD_RATE': {
|
||||||
expression = `rate(${expression}[5m])`;
|
expression = `rate(${expression}[$__rate_interval])`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ADD_SUM': {
|
case 'ADD_SUM': {
|
||||||
|
@ -23,7 +23,7 @@ describe('getQueryHints()', () => {
|
|||||||
|
|
||||||
expect(hints!.length).toBe(1);
|
expect(hints!.length).toBe(1);
|
||||||
expect(hints![0]).toMatchObject({
|
expect(hints![0]).toMatchObject({
|
||||||
label: 'Metric metric_total looks like a counter.',
|
label: 'Selected metric looks like a counter.',
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
@ -48,7 +48,7 @@ describe('getQueryHints()', () => {
|
|||||||
let hints = getQueryHints('foo', series, datasource);
|
let hints = getQueryHints('foo', series, datasource);
|
||||||
expect(hints!.length).toBe(1);
|
expect(hints!.length).toBe(1);
|
||||||
expect(hints![0]).toMatchObject({
|
expect(hints![0]).toMatchObject({
|
||||||
label: 'Metric foo is a counter.',
|
label: 'Selected metric is a counter.',
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
@ -108,7 +108,7 @@ describe('getQueryHints()', () => {
|
|||||||
const hints = getQueryHints('metric_bucket', series);
|
const hints = getQueryHints('metric_bucket', series);
|
||||||
expect(hints!.length).toBe(1);
|
expect(hints!.length).toBe(1);
|
||||||
expect(hints![0]).toMatchObject({
|
expect(hints![0]).toMatchObject({
|
||||||
label: 'Time series has buckets, you probably wanted a histogram.',
|
label: 'Selected metric has buckets.',
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
|
@ -13,12 +13,12 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet
|
|||||||
// ..._bucket metric needs a histogram_quantile()
|
// ..._bucket metric needs a histogram_quantile()
|
||||||
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
||||||
if (histogramMetric) {
|
if (histogramMetric) {
|
||||||
const label = 'Time series has buckets, you probably wanted a histogram.';
|
const label = 'Selected metric has buckets.';
|
||||||
hints.push({
|
hints.push({
|
||||||
type: 'HISTOGRAM_QUANTILE',
|
type: 'HISTOGRAM_QUANTILE',
|
||||||
label,
|
label,
|
||||||
fix: {
|
fix: {
|
||||||
label: 'Fix by adding histogram_quantile().',
|
label: 'Consider calculating aggregated quantile by adding histogram_quantile().',
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
query,
|
query,
|
||||||
@ -55,19 +55,19 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet
|
|||||||
if (counterNameMetric) {
|
if (counterNameMetric) {
|
||||||
const simpleMetric = query.trim().match(/^\w+$/);
|
const simpleMetric = query.trim().match(/^\w+$/);
|
||||||
const verb = certain ? 'is' : 'looks like';
|
const verb = certain ? 'is' : 'looks like';
|
||||||
let label = `Metric ${counterNameMetric} ${verb} a counter.`;
|
let label = `Selected metric ${verb} a counter.`;
|
||||||
let fix: QueryFix | undefined;
|
let fix: QueryFix | undefined;
|
||||||
|
|
||||||
if (simpleMetric) {
|
if (simpleMetric) {
|
||||||
fix = {
|
fix = {
|
||||||
label: 'Fix by adding rate().',
|
label: 'Consider calculating rate of counter by adding rate().',
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
query,
|
query,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
label = `${label} Try applying a rate() function.`;
|
label = `${label} Consider calculating rate of counter by adding rate().`;
|
||||||
}
|
}
|
||||||
|
|
||||||
hints.push({
|
hints.push({
|
||||||
|
@ -7,6 +7,7 @@ import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
|||||||
import PromQlLanguageProvider from '../../language_provider';
|
import PromQlLanguageProvider from '../../language_provider';
|
||||||
import { PromVisualQuery } from '../types';
|
import { PromVisualQuery } from '../types';
|
||||||
import { getLabelSelects } from '../testUtils';
|
import { getLabelSelects } from '../testUtils';
|
||||||
|
import { LoadingState, MutableDataFrame, PanelData, TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
const defaultQuery: PromVisualQuery = {
|
const defaultQuery: PromVisualQuery = {
|
||||||
metric: 'random_metric',
|
metric: 'random_metric',
|
||||||
@ -124,9 +125,64 @@ describe('PromQueryBuilder', () => {
|
|||||||
openLabelNameSelect();
|
openLabelNameSelect();
|
||||||
await waitFor(() => expect(languageProvider.fetchLabels).toBeCalled());
|
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 languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
|
||||||
const datasource = new PrometheusDatasource(
|
const datasource = new PrometheusDatasource(
|
||||||
{
|
{
|
||||||
@ -142,6 +198,7 @@ function setup(query: PromVisualQuery = defaultQuery) {
|
|||||||
datasource,
|
datasource,
|
||||||
onRunQuery: () => {},
|
onRunQuery: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<PromQueryBuilder {...props} query={query} />);
|
const { container } = render(<PromQueryBuilder {...props} query={query} />);
|
||||||
|
@ -8,8 +8,9 @@ import { PrometheusDatasource } from '../../datasource';
|
|||||||
import { NestedQueryList } from './NestedQueryList';
|
import { NestedQueryList } from './NestedQueryList';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
|
||||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||||
|
import { PromQueryBuilderHints } from './PromQueryBuilderHints';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: PromVisualQuery;
|
query: PromVisualQuery;
|
||||||
@ -17,9 +18,10 @@ export interface Props {
|
|||||||
onChange: (update: PromVisualQuery) => void;
|
onChange: (update: PromVisualQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
nested?: boolean;
|
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[]) => {
|
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||||
onChange({ ...query, labels });
|
onChange({ ...query, labels });
|
||||||
};
|
};
|
||||||
@ -106,6 +108,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||||
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
||||||
)}
|
)}
|
||||||
|
<PromQueryBuilderHints datasource={datasource} query={query} onChange={onChange} data={data} />
|
||||||
</OperationsEditorRow>
|
</OperationsEditorRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { CoreApp } from '@grafana/data';
|
import { PanelData } from '@grafana/data';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { PromQuery } from '../../types';
|
import { PromQuery } from '../../types';
|
||||||
import { buildVisualQueryFromString } from '../parsing';
|
import { buildVisualQueryFromString } from '../parsing';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
|
import { PromVisualQuery } from '../types';
|
||||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||||
import { QueryPreview } from './QueryPreview';
|
import { QueryPreview } from './QueryPreview';
|
||||||
import { PromVisualQuery } from '../types';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: PromQuery;
|
query: PromQuery;
|
||||||
datasource: PrometheusDatasource;
|
datasource: PrometheusDatasource;
|
||||||
onChange: (update: PromQuery) => void;
|
onChange: (update: PromQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
app?: CoreApp;
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +23,7 @@ export interface Props {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function PromQueryBuilderContainer(props: Props) {
|
export function PromQueryBuilderContainer(props: Props) {
|
||||||
const { query, onChange, onRunQuery, datasource } = props;
|
const { query, onChange, onRunQuery, datasource, data } = props;
|
||||||
|
|
||||||
const visQuery = buildVisualQueryFromString(query.expr || '').query;
|
const visQuery = buildVisualQueryFromString(query.expr || '').query;
|
||||||
|
|
||||||
@ -34,7 +34,13 @@ export function PromQueryBuilderContainer(props: Props) {
|
|||||||
|
|
||||||
return (
|
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} />}
|
{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}
|
datasource={props.datasource}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onRunQuery={props.onRunQuery}
|
onRunQuery={props.onRunQuery}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
|
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
|
||||||
|
Loading…
Reference in New Issue
Block a user