diff --git a/.betterer.results b/.betterer.results index ef98b4f852d..6445289d814 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/docs/sources/datasources/prometheus/template-variables/index.md b/docs/sources/datasources/prometheus/template-variables/index.md index 07da9e491dc..c2ee82823ee 100644 --- a/docs/sources/datasources/prometheus/template-variables/index.md +++ b/docs/sources/datasources/prometheus/template-variables/index.md @@ -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 | diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx index b68df23183f..7517e96d3ba 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx @@ -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 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 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(); + + 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: [], diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx index a576155c412..bb606cfcbb9 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx @@ -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(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 && ( + + Returns a list of label names, optionally filtering by specified metric regex.} + > + { + setLabelNamesMatch(event.currentTarget.value); + onLabelNamesMatchChange(event.currentTarget.value); + }} + onChange={(e) => { + setLabelNamesMatch(e.currentTarget.value); + }} + width={25} + /> + + + )} + {qryType === QueryType.MetricNames && ( Returns a list of metrics matching the specified metric regex.} > { setMetric(e.currentTarget.value); + }} + onBlur={(e) => { + setMetric(e.currentTarget.value); onMetricChange(e.currentTarget.value); }} width={25} diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index b422c5c2864..ee234893944 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -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> = await Promise.all( diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index a0225c60b5a..b78750b8a81 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -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 { - 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(); } diff --git a/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts b/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts index f10987118c2..9e695cc3eb2 100644 --- a/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts +++ b/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts @@ -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) { diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index f1de7af6b07..04a756a0e55 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -192,6 +192,7 @@ export interface PromVariableQuery extends DataQuery { varQuery?: string; seriesQuery?: string; labelFilters?: QueryBuilderLabelFilter[]; + match?: string; } export type StandardPromVariableQuery = {