Prometheus: Add capability to filter label names by metric in template variable editor (#70452)

* Adds new text input in prometheus template variable UI that allows label names function to filter values by metric. 
Co-authored-by: Brendan O'Handley <brendan.ohandley@grafana.com>
This commit is contained in:
Galen Kistler 2023-06-22 14:11:06 -05:00 committed by GitHub
parent 44972d0cd5
commit 7cb1f6541e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 28 deletions

View File

@ -4433,8 +4433,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"]
[0, 0, 0, "Unexpected any. Specify a different type.", "30"]
],
"public/app/plugins/datasource/prometheus/language_provider.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -28,7 +28,7 @@ Select a Prometheus data source query type and enter the required inputs:
| Query Type | Input(\* required) | Description | Used API endpoints |
| -------------- | ------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `Label names` | none | Returns a list of all label names. | /api/v1/labels |
| `Label names` | `metric` | Returns a list of all label names matching the specified `metric` regex. | /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 |

View File

@ -4,6 +4,7 @@ import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { PrometheusDatasource } from '../datasource';
import PrometheusLanguageProvider from '../language_provider';
import { PromVariableQuery, PromVariableQueryType, StandardPromVariableQuery } from '../types';
import { PromVariableQueryEditor, Props, variableMigration } from './VariableQueryEditor';
@ -62,7 +63,7 @@ describe('PromVariableQueryEditor', () => {
beforeEach(() => {
props = {
datasource: {
hasLabelsMatchAPISupport: () => 1,
hasLabelsMatchAPISupport: () => true,
languageProvider: {
start: () => Promise.resolve([]),
syntax: () => {},
@ -71,13 +72,14 @@ describe('PromVariableQueryEditor', () => {
metricsMetadata: {},
getLabelValues: jest.fn().mockImplementation(() => ['that']),
fetchSeriesLabelsMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })),
},
} as Partial<PrometheusLanguageProvider> as PrometheusLanguageProvider,
getInitHints: () => [],
getDebounceTimeInMilliseconds: jest.fn(),
getTagKeys: jest.fn().mockImplementation(() => Promise.resolve(['this'])),
getVariables: jest.fn().mockImplementation(() => []),
metricFindQuery: jest.fn().mockImplementation(() => Promise.resolve(['that'])),
} as unknown as PrometheusDatasource,
getSeriesLabels: jest.fn().mockImplementation(() => Promise.resolve(['that'])),
} as Partial<PrometheusDatasource> as PrometheusDatasource,
query: {
refId: 'test',
query: 'label_names()',
@ -101,6 +103,26 @@ describe('PromVariableQueryEditor', () => {
await waitFor(() => expect(screen.getByText('Series query')).toBeInTheDocument());
});
test('Calls onChange for label_names(match) query', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '',
match: 'that',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label names');
expect(onChange).toHaveBeenCalledWith({
query: 'label_names(that)',
labelFilters: [],
refId,
});
});
test('Calls onChange for label_names() query', async () => {
const onChange = jest.fn();
@ -145,9 +167,13 @@ describe('PromVariableQueryEditor', () => {
await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics');
const metricInput = screen.getByLabelText('Metric selector');
await userEvent.type(metricInput, 'a');
await userEvent.type(metricInput, 'a').then((prom) => {
const queryType = screen.getByLabelText('Query type');
// click elsewhere to trigger the onBlur
return userEvent.click(queryType);
});
waitFor(() =>
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'metrics(a)',
labelFilters: [],
@ -171,7 +197,7 @@ describe('PromVariableQueryEditor', () => {
await userEvent.type(labelSelect, 'this');
await selectOptionInTest(labelSelect, 'this');
waitFor(() =>
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'label_values(this)',
labelFilters: [],
@ -199,7 +225,7 @@ describe('PromVariableQueryEditor', () => {
await userEvent.type(metricSelect, 'that');
await selectOptionInTest(metricSelect, 'that');
waitFor(() =>
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'label_values(that,this)',
labelFilters: [],

View File

@ -1,4 +1,3 @@
import { debounce } from 'lodash';
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
@ -36,9 +35,11 @@ const refId = 'PrometheusVariableQueryEditor-VariableQuery';
export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) => {
// 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('');
const [labelNamesMatch, setLabelNamesMatch] = 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
@ -62,6 +63,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
// 2. jsonnet grafana as code passes a variable as a string
const variableQuery = variableMigration(query);
setLabelNamesMatch(variableQuery.match ?? '');
setQryType(variableQuery.qryType);
setLabel(variableQuery.label ?? '');
setMetric(variableQuery.metric ?? '');
@ -111,6 +113,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
qryType,
label,
metric,
match: labelNamesMatch,
varQuery,
seriesQuery,
refId: 'PrometheusVariableQueryEditor-VariableQuery',
@ -120,7 +123,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
const queryString = migrateVariableEditorBackToVariableSupport(updatedVar);
const lblFltrs = updLabelFilters ? updLabelFilters : labelFilters;
let lblFltrs = updLabelFilters ? updLabelFilters : labelFilters;
// setting query.query property allows for update of variable definition
onChange({
@ -164,15 +167,21 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
}
};
const onLabelNamesMatchChange = (regex: string) => {
if (qryType === QueryType.LabelNames) {
onChangeWithVariableString({ qryType, match: regex });
}
};
/**
* Call onchange for metric change if metrics names (regex) query type
* Debounce this because to not call the API for every keystroke.
*/
const onMetricChange = debounce((value: string) => {
const onMetricChange = (value: string) => {
if (qryType === QueryType.MetricNames && value) {
onChangeWithVariableString({ metric: value });
}
}, 300);
};
/**
* Do not call onchange for variable query result when query type is var query result
@ -252,20 +261,50 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
</>
)}
{qryType === QueryType.LabelNames && (
<InlineFieldRow>
<InlineField
label="Metric regex"
labelWidth={20}
aria-labelledby="Metric regex"
tooltip={<div>Returns a list of label names, optionally filtering by specified metric regex.</div>}
>
<Input
type="text"
aria-label="Metric regex"
placeholder="Metric regex"
value={labelNamesMatch}
onBlur={(event) => {
setLabelNamesMatch(event.currentTarget.value);
onLabelNamesMatchChange(event.currentTarget.value);
}}
onChange={(e) => {
setLabelNamesMatch(e.currentTarget.value);
}}
width={25}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.MetricNames && (
<InlineFieldRow>
<InlineField
label="Metric regex"
labelWidth={20}
aria-labelledby="Metric selector"
tooltip={<div>Returns a list of metrics matching the specified metric regex.</div>}
>
<Input
type="text"
aria-label="Metric selector"
placeholder="Metric Regex"
placeholder="Metric regex"
value={metric}
onChange={(e) => {
setMetric(e.currentTarget.value);
}}
onBlur={(e) => {
setMetric(e.currentTarget.value);
onMetricChange(e.currentTarget.value);
}}
width={25}

View File

@ -967,7 +967,7 @@ export class PrometheusDatasource
// 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 getTagKeys(options?: any) {
async getTagKeys(options?: { series: string[] }) {
if (options?.series) {
// Get tags for the provided series only
const seriesLabels: Array<Record<string, string[]>> = await Promise.all(

View File

@ -7,6 +7,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { PrometheusDatasource } from './datasource';
import { getPrometheusTime } from './language_utils';
import {
PrometheusLabelNamesRegex,
PrometheusLabelNamesRegexWithMatch,
PrometheusMetricNamesRegex,
PrometheusQueryResultRegex,
} from './migrations/variableMigration';
import { PromQueryRequest } from './types';
export default class PrometheusMetricFindQuery {
@ -19,11 +25,23 @@ export default class PrometheusMetricFindQuery {
}
process(): Promise<MetricFindValue[]> {
const labelNamesRegex = /^label_names\(\)\s*$/;
const labelNamesRegex = PrometheusLabelNamesRegex;
const labelNamesRegexWithMatch = PrometheusLabelNamesRegexWithMatch;
const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/;
const metricNamesRegex = /^metrics\((.+)\)\s*$/;
const queryResultRegex = /^query_result\((.+)\)\s*$/;
const metricNamesRegex = PrometheusMetricNamesRegex;
const queryResultRegex = PrometheusQueryResultRegex;
const labelNamesQuery = this.query.match(labelNamesRegex);
const labelNamesMatchQuery = this.query.match(labelNamesRegexWithMatch);
if (labelNamesMatchQuery) {
const selector = `{__name__=~".*${labelNamesMatchQuery[1]}.*"}`;
return this.datasource.languageProvider.getSeriesLabels(selector, []).then((results) =>
results.map((result) => ({
text: result,
}))
);
}
if (labelNamesQuery) {
return this.datasource.getTagKeys();
}

View File

@ -1,9 +1,11 @@
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 const PrometheusLabelNamesRegex = /^label_names\(\)\s*$/;
// Note that this regex is different from the one in metric_find_query.ts because this is used pre-interpolation
export const PrometheusLabelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_$][a-zA-Z0-9_]*)\)\s*$/;
export const PrometheusMetricNamesRegex = /^metrics\((.+)\)\s*$/;
export const PrometheusQueryResultRegex = /^query_result\((.+)\)\s*$/;
export const PrometheusLabelNamesRegexWithMatch = /^label_names\((.+)\)\s*$/;
export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuery): PromVariableQuery {
// If not string, we assume PromVariableQuery
@ -16,7 +18,17 @@ export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuer
qryType: QueryType.LabelNames,
};
const labelNames = rawQuery.match(labelNamesRegex);
const labelNamesMatchQuery = rawQuery.match(PrometheusLabelNamesRegexWithMatch);
if (labelNamesMatchQuery) {
return {
...queryBase,
qryType: QueryType.LabelNames,
match: labelNamesMatchQuery[1],
};
}
const labelNames = rawQuery.match(PrometheusLabelNamesRegex);
if (labelNames) {
return {
...queryBase,
@ -24,7 +36,7 @@ export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuer
};
}
const labelValues = rawQuery.match(labelValuesRegex);
const labelValues = rawQuery.match(PrometheusLabelValuesRegex);
if (labelValues) {
const label = labelValues[2];
@ -45,7 +57,7 @@ export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuer
}
}
const metricNames = rawQuery.match(metricNamesRegex);
const metricNames = rawQuery.match(PrometheusMetricNamesRegex);
if (metricNames) {
return {
...queryBase,
@ -54,7 +66,7 @@ export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuer
};
}
const queryResult = rawQuery.match(queryResultRegex);
const queryResult = rawQuery.match(PrometheusQueryResultRegex);
if (queryResult) {
return {
...queryBase,
@ -79,6 +91,9 @@ export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuer
export function migrateVariableEditorBackToVariableSupport(QueryVariable: PromVariableQuery): string {
switch (QueryVariable.qryType) {
case QueryType.LabelNames:
if (QueryVariable.match) {
return `label_names(${QueryVariable.match})`;
}
return 'label_names()';
case QueryType.LabelValues:
if (QueryVariable.metric) {

View File

@ -192,6 +192,7 @@ export interface PromVariableQuery extends DataQuery {
varQuery?: string;
seriesQuery?: string;
labelFilters?: QueryBuilderLabelFilter[];
match?: string;
}
export type StandardPromVariableQuery = {