Prometheus Datasource: Improve Prom query variable editor (#58292)

* add prom query var editor with tests and migrations

* fix migration, now query not expr

* fix label_values migration

* remove comments

* fix label_values() variables order

* update UI and use more clear language

* fix tests

* use null coalescing operators

* allow users to query label values with label and metric if they have not set there flavor and version

* use enums instead of numbers for readability

* fix label&metrics switch

* update type in qv editor

* reuse datasource function to get all label names, getLabelNames(), prev named getTagKeys()

* use getLabelNames in the query var editor

* make label_values() variables label and metric more readable in the migration

* fix tooltip for label_values to remove API reference

* clean up tooltips and allow newlines in query_result function

* change function wording and exprType to query type/qryType for readability

* update prometheus query variable docs

* Update public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx

Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>

---------

Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>
This commit is contained in:
Brendan O'Handley 2023-02-09 15:35:36 -05:00 committed by GitHub
parent 1f984409a2
commit eedcd7d5b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 552 additions and 50 deletions

View File

@ -6261,9 +6261,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/datasource/prometheus/query_hints.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -23,17 +23,17 @@ For an introduction to templating and template variables, refer to the [Templati
## Use query variables
You can use variables of the type _Query_ to query Prometheus for a list of metrics, labels, or label values.
Use variables of the type _Query_ to query Prometheus for a list of metrics, labels, or label values.
You can use these Prometheus data source functions in the **Query** input field:
Select a Prometheus data source query type and enter the required inputs:
| Name | Description | Used API endpoints |
| ----------------------------- | ----------------------------------------------------------------------- | --------------------------------- |
| `label_names()` | Returns a list of label names. | /api/v1/labels |
| `label_values(label)` | Returns a list of label values for the `label` in every metric. | /api/v1/label/`label`/values |
| `label_values(metric, label)` | Returns a list of label values for the `label` in the specified metric. | /api/v1/series |
| `metrics(metric)` | Returns a list of metrics matching the specified `metric` regex. | /api/v1/label/\_\_name\_\_/values |
| `query_result(query)` | Returns a list of Prometheus query result for the `query`. | /api/v1/query |
| Query Type | Input(\* required) | Description | Used API endpoints |
| -------------- | ------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `Label names` | none | Returns a list of all label names. | /api/v1/labels |
| `Label values` | `label`\*, `metric` | Returns a list of label values for the `label` in all metrics or the optional metric. | /api/v1/label/`label`/values or /api/v1/series |
| `Metrics` | `metric` | Returns a list of metrics matching the specified `metric` regex. | /api/v1/label/\_\_name\_\_/values |
| `Query result` | `query` | Returns a list of Prometheus query result for the `query`. | /api/v1/query |
| `Series query` | `metric`, `label` or both | Returns a list of time series associated with the entered data. | /api/v1/series |
For details on _metric names_, _label names_, and _label values_, refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).

View File

@ -0,0 +1,142 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { PrometheusDatasource } from '../datasource';
import { PromVariableQueryEditor, Props } from './VariableQueryEditor';
const refId = 'PrometheusVariableQueryEditor-VariableQuery';
describe('PromVariableQueryEditor', () => {
let props: Props;
beforeEach(() => {
props = {
datasource: {
hasLabelsMatchAPISupport: () => 1,
languageProvider: {
start: () => Promise.resolve([]),
syntax: () => {},
getLabelKeys: () => [],
metrics: [],
},
getInitHints: () => [],
} as unknown as PrometheusDatasource,
query: {
refId: 'test',
query: 'label_names()',
},
onRunQuery: () => {},
onChange: () => {},
history: [],
};
});
test('Displays a group of function options', async () => {
render(<PromVariableQueryEditor {...props} />);
const select = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(select);
await waitFor(() => expect(screen.getAllByText('Label names')).toHaveLength(2));
await waitFor(() => expect(screen.getByText('Label values')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Metrics')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Query result')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Series query')).toBeInTheDocument());
});
test('Calls onChange for label_names() query', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label names');
expect(onChange).toHaveBeenCalledWith({
query: 'label_names()',
refId,
});
});
test('Does not call onChange for other queries', async () => {
const onChange = jest.fn();
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Query result');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Series query');
expect(onChange).not.toHaveBeenCalled();
});
test('Calls onChange for metrics() with argument onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'metrics(a)',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Metric selector');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: 'metrics(a)',
refId,
});
});
test('Calls onChange for query_result() with argument onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'query_result(a)',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Prometheus Query');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: 'query_result(a)',
refId,
});
});
test('Calls onChange for Match[] series with argument onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '{a: "example"}',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Series Query');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: '{a: "example"}',
refId,
});
});
});

View File

@ -0,0 +1,257 @@
import React, { FC, FormEvent, useEffect, useState } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
import { PrometheusDatasource } from '../datasource';
import {
migrateVariableEditorBackToVariableSupport,
migrateVariableQueryToEditor,
} from '../migrations/variableMigration';
import { PromOptions, PromQuery, PromVariableQuery, PromVariableQueryType as QueryType } from '../types';
export const variableOptions = [
{ label: 'Label names', value: QueryType.LabelNames },
{ label: 'Label values', value: QueryType.LabelValues },
{ label: 'Metrics', value: QueryType.MetricNames },
{ label: 'Query result', value: QueryType.VarQueryResult },
{ label: 'Series query', value: QueryType.SeriesQuery },
];
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions, PromVariableQuery>;
const refId = 'PrometheusVariableQueryEditor-VariableQuery';
export const PromVariableQueryEditor: FC<Props> = ({ onChange, query, datasource }) => {
// to select the query type, i.e. label_names, label_values, etc.
const [qryType, setQryType] = useState<number | undefined>(undefined);
// list of variables for each function
const [label, setLabel] = useState('');
// metric is used for both label_values() and metric()
// label_values() metric requires a whole/complete metric
// metric() is expected to be a part of a metric string
const [metric, setMetric] = useState('');
// varQuery is a whole query, can include math/rates/etc
const [varQuery, setVarQuery] = useState('');
// seriesQuery is only a whole
const [seriesQuery, setSeriesQuery] = useState('');
// list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function
const [labelOptions, setLabelOptions] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
if (!query) {
return;
}
// Changing from standard to custom variable editor changes the string attr from expr to query
const variableQuery = query.query ? migrateVariableQueryToEditor(query.query) : query;
setQryType(variableQuery.qryType);
setLabel(variableQuery.label ?? '');
setMetric(variableQuery.metric ?? '');
setVarQuery(variableQuery.varQuery ?? '');
setSeriesQuery(variableQuery.seriesQuery ?? '');
// set the migrated label in the label options
if (variableQuery.label) {
setLabelOptions([{ label: variableQuery.label, value: variableQuery.label }]);
}
}, [query]);
// set the label names options for the label values var query
useEffect(() => {
if (qryType !== QueryType.LabelValues) {
return;
}
datasource.getLabelNames().then((labelNames: Array<{ text: string }>) => {
setLabelOptions(labelNames.map(({ text }) => ({ label: text, value: text })));
});
}, [datasource, qryType]);
const onChangeWithVariableString = (qryType: QueryType) => {
const queryVar = {
qryType: qryType,
label,
metric,
varQuery,
seriesQuery,
refId: 'PrometheusVariableQueryEditor-VariableQuery',
};
const queryString = migrateVariableEditorBackToVariableSupport(queryVar);
onChange({
query: queryString,
refId,
});
};
const onQueryTypeChange = (newType: SelectableValue<QueryType>) => {
setQryType(newType.value);
if (newType.value === QueryType.LabelNames) {
onChangeWithVariableString(newType.value);
}
};
const onLabelChange = (newLabel: SelectableValue<string>) => {
setLabel(newLabel.value ?? '');
};
const onMetricChange = (e: FormEvent<HTMLInputElement>) => {
setMetric(e.currentTarget.value);
};
const onVarQueryChange = (e: FormEvent<HTMLTextAreaElement>) => {
setVarQuery(e.currentTarget.value);
};
const onSeriesQueryChange = (e: FormEvent<HTMLInputElement>) => {
setSeriesQuery(e.currentTarget.value);
};
const handleBlur = () => {
if (qryType === QueryType.LabelNames) {
onChangeWithVariableString(qryType);
} else if (qryType === QueryType.LabelValues && label) {
onChangeWithVariableString(qryType);
} else if (qryType === QueryType.MetricNames && metric) {
onChangeWithVariableString(qryType);
} else if (qryType === QueryType.VarQueryResult && varQuery) {
onChangeWithVariableString(qryType);
} else if (qryType === QueryType.SeriesQuery && seriesQuery) {
onChangeWithVariableString(qryType);
}
};
return (
<InlineFieldRow>
<InlineField
label="Query Type"
labelWidth={20}
tooltip={
<div>The Prometheus data source plugin provides the following query types for template variables.</div>
}
>
<Select
placeholder="Select query type"
aria-label="Query type"
onChange={onQueryTypeChange}
onBlur={handleBlur}
value={qryType}
options={variableOptions}
width={25}
/>
</InlineField>
{qryType === QueryType.LabelValues && (
<>
<InlineField
label="Label"
labelWidth={20}
required
tooltip={
<div>
Returns a list of label values for the label name in all metrics unless the metric is specified.
</div>
}
>
<Select
aria-label="label-select"
onChange={onLabelChange}
onBlur={handleBlur}
value={label}
options={labelOptions}
width={25}
allowCustomValue
/>
</InlineField>
<InlineField
label="Metric"
labelWidth={20}
tooltip={<div>Optional: returns a list of label values for the label name in the specified metric.</div>}
>
<Input
type="text"
aria-label="Metric selector"
placeholder="Optional metric selector"
value={metric}
onChange={onMetricChange}
onBlur={handleBlur}
width={25}
/>
</InlineField>
</>
)}
{qryType === QueryType.MetricNames && (
<>
<InlineField
label="Metric Regex"
labelWidth={20}
tooltip={<div>Returns a list of metrics matching the specified metric regex.</div>}
>
<Input
type="text"
aria-label="Metric selector"
placeholder="Metric Regex"
value={metric}
onChange={onMetricChange}
onBlur={handleBlur}
width={25}
/>
</InlineField>
</>
)}
{qryType === QueryType.VarQueryResult && (
<>
<InlineField
label="Query"
labelWidth={20}
tooltip={
<div>
Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e.
sum(go_goroutines).
</div>
}
>
<TextArea
type="text"
aria-label="Prometheus Query"
placeholder="Prometheus Query"
value={varQuery}
onChange={onVarQueryChange}
onBlur={handleBlur}
cols={100}
/>
</InlineField>
</>
)}
{qryType === QueryType.SeriesQuery && (
<>
<InlineField
label="Series Query"
labelWidth={20}
tooltip={
<div>
Enter enter a metric with labels, only a metric or only labels, i.e.
go_goroutines&#123;instance=&quot;localhost:9090&quot;&#125;, go_goroutines, or
&#123;instance=&quot;localhost:9090&quot;&#125;. Returns a list of time series associated with the
entered data.
</div>
}
>
<Input
type="text"
aria-label="Series Query"
placeholder="Series Query"
value={seriesQuery}
onChange={onSeriesQueryChange}
onBlur={handleBlur}
width={100}
/>
</InlineField>
</>
)}
</InlineFieldRow>
);
};

View File

@ -153,7 +153,7 @@ describe('PrometheusDatasource', () => {
).rejects.toMatchObject({
message: expect.stringMatching('Browser access'),
});
await expect(directDs.getTagKeys()).rejects.toMatchObject({
await expect(directDs.getLabelNames()).rejects.toMatchObject({
message: expect.stringMatching('Browser access'),
});
await expect(directDs.getTagValues()).rejects.toMatchObject({

View File

@ -286,7 +286,7 @@ export class PrometheusDatasource
hideFromInspector: true,
...options,
})
); // toPromise until we change getTagValues, getTagKeys to Observable
); // toPromise until we change getTagValues, getLabelNames to Observable
}
interpolateQueryExpr(value: string | string[] = [], variable: any) {
@ -908,7 +908,10 @@ export class PrometheusDatasource
);
}
async getTagKeys(options?: any) {
// this is used to get label keys, a.k.a label names
// it is used in metric_find_query.ts
// and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
async getLabelNames(options?: any) {
if (options?.series) {
// Get tags for the provided series only
const seriesLabels: Array<Record<string, string[]>> = await Promise.all(

View File

@ -24,7 +24,7 @@ export default class PrometheusMetricFindQuery {
const queryResultRegex = /^query_result\((.+)\)\s*$/;
const labelNamesQuery = this.query.match(labelNamesRegex);
if (labelNamesQuery) {
return this.labelNamesQuery();
return this.datasource.getLabelNames();
}
const labelValuesQuery = this.query.match(labelValuesRegex);
@ -47,24 +47,12 @@ export default class PrometheusMetricFindQuery {
}
// if query contains full metric name, return metric name and label list
return this.metricNameAndLabelsQuery(this.query);
}
const expressions = ['label_values()', 'metrics()', 'query_result()'];
if (!expressions.includes(this.query)) {
return this.metricNameAndLabelsQuery(this.query);
}
labelNamesQuery() {
const start = this.datasource.getPrometheusTime(this.range.from, false);
const end = this.datasource.getPrometheusTime(this.range.to, true);
const params = {
start: start.toString(),
end: end.toString(),
};
const url = `/api/v1/labels`;
return this.datasource.metadataRequest(url, params).then((result: any) => {
return _map(result.data.data, (value) => {
return { text: value };
});
});
return Promise.resolve([]);
}
labelValuesQuery(label: string, metric?: string) {

View File

@ -0,0 +1,104 @@
import { PromVariableQuery, PromVariableQueryType as QueryType } from '../types';
const labelNamesRegex = /^label_names\(\)\s*$/;
const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/;
const metricNamesRegex = /^metrics\((.+)\)\s*$/;
const queryResultRegex = /^query_result\((.+)\)\s*$/;
export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuery): PromVariableQuery {
// If not string, we assume PromVariableQuery
if (typeof rawQuery !== 'string') {
return rawQuery;
}
const queryBase = {
refId: 'PrometheusDatasource-VariableQuery',
qryType: QueryType.LabelNames,
};
const labelNames = rawQuery.match(labelNamesRegex);
if (labelNames) {
return {
...queryBase,
qryType: QueryType.LabelNames,
};
}
const labelValues = rawQuery.match(labelValuesRegex);
if (labelValues) {
const label = labelValues[2];
const metric = labelValues[1];
if (metric) {
return {
...queryBase,
qryType: QueryType.LabelValues,
label,
metric,
};
} else {
return {
...queryBase,
qryType: QueryType.LabelValues,
label,
};
}
}
const metricNames = rawQuery.match(metricNamesRegex);
if (metricNames) {
return {
...queryBase,
qryType: QueryType.MetricNames,
metric: metricNames[1],
};
}
const queryResult = rawQuery.match(queryResultRegex);
if (queryResult) {
return {
...queryBase,
qryType: QueryType.VarQueryResult,
varQuery: queryResult[1],
};
}
// seriesQuery does not have a function and no regex above
if (!labelNames && !labelValues && !metricNames && !queryResult) {
return {
...queryBase,
qryType: QueryType.SeriesQuery,
seriesQuery: rawQuery,
};
}
return queryBase;
}
// migrate it back to a string with the correct varialbes in place
export function migrateVariableEditorBackToVariableSupport(QueryVariable: PromVariableQuery): string {
switch (QueryVariable.qryType) {
case QueryType.LabelNames:
return 'label_names()';
case QueryType.LabelValues:
if (QueryVariable.metric) {
return `label_values(${QueryVariable.metric},${QueryVariable.label})`;
} else {
return `label_values(${QueryVariable.label})`;
}
case QueryType.MetricNames:
return `metrics(${QueryVariable.metric})`;
case QueryType.VarQueryResult:
const varQuery = removeLineBreaks(QueryVariable.varQuery);
return `query_result(${varQuery})`;
case QueryType.SeriesQuery:
return '' + QueryVariable.seriesQuery;
}
return '';
}
// allow line breaks in query result textarea
function removeLineBreaks(input?: string) {
return input ? input.replace(/[\r\n]+/gm, '') : '';
}

View File

@ -22,6 +22,7 @@ export interface PromQuery extends DataQuery {
showingTable?: boolean;
/** Code, Builder or Explain */
editorMode?: QueryEditorMode;
query?: string;
}
export interface PromOptions extends DataSourceJsonData {
@ -165,3 +166,21 @@ export enum LegendFormatMode {
Verbose = '__verbose',
Custom = '__custom',
}
export enum PromVariableQueryType {
LabelNames,
LabelValues,
MetricNames,
VarQueryResult,
SeriesQuery,
}
export interface PromVariableQuery extends DataQuery {
query?: string;
expr?: string;
qryType?: PromVariableQueryType;
label?: string;
metric?: string;
varQuery?: string;
seriesQuery?: string;
}

View File

@ -1,22 +1,17 @@
import { from, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import {
DataQueryRequest,
DataQueryResponse,
rangeUtil,
StandardVariableQuery,
StandardVariableSupport,
} from '@grafana/data';
import { CustomVariableSupport, DataQueryRequest, DataQueryResponse, rangeUtil } from '@grafana/data';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { getTimeSrv, TimeSrv } from '../../../features/dashboard/services/TimeSrv';
import { PromVariableQueryEditor } from './components/VariableQueryEditor';
import { PrometheusDatasource } from './datasource';
import PrometheusMetricFindQuery from './metric_find_query';
import { PromQuery } from './types';
export class PrometheusVariableSupport extends StandardVariableSupport<PrometheusDatasource> {
export class PrometheusVariableSupport extends CustomVariableSupport<PrometheusDatasource> {
constructor(
private readonly datasource: PrometheusDatasource,
private readonly templateSrv: TemplateSrv = getTemplateSrv(),
@ -26,8 +21,10 @@ export class PrometheusVariableSupport extends StandardVariableSupport<Prometheu
this.query = this.query.bind(this);
}
editor = PromVariableQueryEditor;
query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
const query = request.targets[0].expr;
const query = request.targets[0].query;
if (!query) {
return of({ data: [] });
}
@ -48,11 +45,4 @@ export class PrometheusVariableSupport extends StandardVariableSupport<Prometheu
return metricFindStream.pipe(map((results) => ({ data: results })));
}
toDataQuery(query: StandardVariableQuery): PromQuery {
return {
refId: 'PrometheusDatasource-VariableQuery',
expr: query.query,
};
}
}

View File

@ -29,7 +29,7 @@ export function ServiceGraphSection({
const [hasKeys, setHasKeys] = useState<boolean | undefined>(undefined);
useEffect(() => {
async function fn(ds: PrometheusDatasource) {
const keys = await ds.getTagKeys({
const keys = await ds.getLabelNames({
series: [
'traces_service_graph_request_server_seconds_sum',
'traces_service_graph_request_total',