mirror of
https://github.com/grafana/grafana.git
synced 2025-02-03 20:21:01 -06:00
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:
parent
44972d0cd5
commit
7cb1f6541e
@ -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"],
|
||||
|
@ -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 |
|
||||
|
@ -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: [],
|
||||
|
@ -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}
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -192,6 +192,7 @@ export interface PromVariableQuery extends DataQuery {
|
||||
varQuery?: string;
|
||||
seriesQuery?: string;
|
||||
labelFilters?: QueryBuilderLabelFilter[];
|
||||
match?: string;
|
||||
}
|
||||
|
||||
export type StandardPromVariableQuery = {
|
||||
|
Loading…
Reference in New Issue
Block a user