diff --git a/.betterer.results b/.betterer.results index 699a5dfade9..9d7e63175ae 100644 --- a/.betterer.results +++ b/.betterer.results @@ -432,8 +432,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "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, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "packages/grafana-prometheus/src/language_provider.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx index 88b12b8cf13..116a06a5d87 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx @@ -138,26 +138,18 @@ describe('PromVariableQueryEditor', () => { hasLabelsMatchAPISupport: () => true, languageProvider: { start: () => Promise.resolve([]), - getLabelKeys: () => [], - metrics: [], - metricsMetadata: {}, - getLabelValues: jest.fn().mockImplementation(() => ['that']), - fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })), - queryLabelValues: jest.fn().mockResolvedValue([]), + queryLabelKeys: jest.fn().mockResolvedValue(['those']), + queryLabelValues: jest.fn().mockResolvedValue(['that']), } as Partial, - getTagKeys: jest - .fn() - .mockImplementation(() => Promise.resolve([{ text: 'this', value: 'this', label: 'this' }])), - getVariables: jest.fn().mockImplementation(() => []), - metricFindQuery: jest.fn().mockImplementation(() => - Promise.resolve([ - { - text: 'that', - value: 'that', - label: 'that', - }, - ]) - ), + getTagKeys: jest.fn().mockResolvedValue([{ text: 'this', value: 'this', label: 'this' }]), + getVariables: jest.fn().mockReturnValue([]), + metricFindQuery: jest.fn().mockResolvedValue([ + { + text: 'that', + value: 'that', + label: 'that', + }, + ]), } as Partial as PrometheusDatasource, query: { refId: 'test', diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx index 8a05059cdb6..f89baf6338f 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx @@ -129,13 +129,10 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: const labelToConsider = [{ label: '__name__', op: '=', value: metric }]; const expr = promQueryModeller.renderLabels(labelToConsider); - datasource.languageProvider - .fetchLabelsWithMatch(timeRange, expr) - .then((labelsIndex: Record) => { - const labelNames = Object.keys(labelsIndex); - const names = labelNames.map((value) => ({ label: value, value: value })); - setLabels(names, variables); - }); + datasource.languageProvider.queryLabelKeys(timeRange, expr).then((labelNames: string[]) => { + const names = labelNames.map((value) => ({ label: value, value: value })); + setLabels(names, variables); + }); } }, [datasource, qryType, metric, range]); diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index 93e374c498e..225acd63163 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -135,20 +135,6 @@ describe('PrometheusDatasource', () => { ) ) ).rejects.toMatchObject({ message: expect.stringMatching('Browser access') }); - - // Cannot test because some other tests need "./metric_find_query" to be mocked and that prevents this to be - // tested. Checked manually that this ends up with throwing - // await expect(directDs.metricFindQuery('label_names(foo)')).rejects.toBeDefined(); - - const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {}); - - await directDs.getTagKeys({ filters: [] }); - // Language provider currently catches and just logs the error - expect(errorMock).toHaveBeenCalledTimes(1); - - await expect(directDs.getTagValues({ filters: [], key: 'A' })).rejects.toMatchObject({ - message: expect.stringMatching('Browser access'), - }); }); }); @@ -980,6 +966,86 @@ describe('PrometheusDatasource', () => { expect(rangeS).toEqual({ text: 21600, value: 21600 }); }); }); + + describe('extractResourceMatcher', () => { + it('should extract matcher from given query and filters', () => { + const queries: PromQuery[] = [ + { + refId: 'A', + expr: 'metric_name{job="testjob"}', + }, + ]; + const filters: AdHocVariableFilter[] = [ + { + key: 'instance', + operator: '=', + value: 'localhost', + }, + ]; + + const result = ds.extractResourceMatcher(queries, filters); + expect(result).toBe('{__name__=~"metric_name",instance="localhost"}'); + }); + + it('should extract matcher from given query and empty filters', () => { + const queries: PromQuery[] = [ + { + refId: 'A', + expr: 'metric_name{job="testjob"}', + }, + ]; + const filters: AdHocVariableFilter[] = []; + + const result = ds.extractResourceMatcher(queries, filters); + expect(result).toBe('{__name__=~"metric_name"}'); + }); + + it('should extract matcher from given empty query expr and filters', () => { + const queries: PromQuery[] = [ + { + refId: 'A', + expr: '', + }, + ]; + const filters: AdHocVariableFilter[] = [ + { + key: 'instance', + operator: '=', + value: 'localhost', + }, + ]; + + const result = ds.extractResourceMatcher(queries, filters); + expect(result).toBe('{__name__!="",instance="localhost"}'); + }); + + it('should extract matcher from given filters only', () => { + const queries: PromQuery[] = []; + const filters: AdHocVariableFilter[] = [ + { + key: 'instance', + operator: '=', + value: 'localhost', + }, + { + key: 'job', + operator: '!=', + value: 'testjob', + }, + ]; + + const result = ds.extractResourceMatcher(queries, filters); + expect(result).toBe('{__name__!="",instance="localhost",job!="testjob"}'); + }); + + it('should extract matcher as match-all from no query and filter', () => { + const queries: PromQuery[] = []; + const filters: AdHocVariableFilter[] = []; + + const result = ds.extractResourceMatcher(queries, filters); + expect(result).toBe('{__name__!=""}'); + }); + }); }); describe('PrometheusDatasource2', () => { @@ -1010,8 +1076,8 @@ describe('PrometheusDatasource2', () => { }; ds.languageProvider = { - histogramMetrics: ['tns_request_duration_seconds_bucket'], - } as PrometheusLanguageProviderInterface; + retrieveHistogramMetrics: jest.fn().mockReturnValue(['tns_request_duration_seconds_bucket']), + } as unknown as PrometheusLanguageProviderInterface; const request = { targets: [targetA, targetB], diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 9be46cafdfc..f19dc0b86f7 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -47,13 +47,14 @@ import { prometheusRegularEscape, prometheusSpecialRegexEscape } from './escapin import { exportToAbstractQuery, importFromAbstractQuery, + populateMatchParamsFromQueries, PrometheusLanguageProvider, PrometheusLanguageProviderInterface, } from './language_provider'; import { expandRecordingRules, getPrometheusTime, getRangeSnapInterval } from './language_utils'; import { PrometheusMetricFindQuery } from './metric_find_query'; import { getQueryHints } from './query_hints'; -import { promQueryModeller } from './querybuilder/shared/modeller_instance'; +import { renderLabelsWithoutBrackets } from './querybuilder/shared/rendering/labels'; import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types'; import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache'; import { transformV2 } from './result_transformer'; @@ -349,7 +350,7 @@ export class PrometheusDatasource shouldRunExemplarQuery(target: PromQuery, request: DataQueryRequest): boolean { if (target.exemplar) { // We check all already processed targets and only create exemplar target for not used metric names - const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m)); + const metricName = this.languageProvider.retrieveHistogramMetrics().find((m) => target.expr.includes(m)); // Remove targets that weren't processed yet (in targets array they are after current target) const currentTargetIdx = request.targets.findIndex((t) => t.refId === target.refId); const targets = request.targets.slice(0, currentTargetIdx).filter((t) => !t.hide); @@ -523,25 +524,12 @@ export class PrometheusDatasource .map((k) => ({ value: k, text: k })); } - if (!options || options.filters.length === 0) { - await this.languageProvider.fetchLabels(options.timeRange, options.queries); - return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k })); - } + const match = this.extractResourceMatcher(options.queries ?? [], options.filters); - const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({ - label: f.key, - value: f.value, - op: f.operator, - })); - const expr = promQueryModeller.renderLabels(labelFilters); - - let labelsIndex: Record = await this.languageProvider.fetchLabelsWithMatch( - options.timeRange, - expr - ); + let labelKeys: string[] = await this.languageProvider.queryLabelKeys(options.timeRange, match, '0'); // filter out already used labels - return Object.keys(labelsIndex) + return labelKeys .filter((labelName) => !options.filters.find((filter) => filter.key === labelName)) .map((k) => ({ value: k, text: k })); } @@ -567,26 +555,36 @@ export class PrometheusDatasource ).map((v) => ({ value: v, text: v })); } - const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({ + const match = this.extractResourceMatcher(options.queries ?? [], options.filters); + + return (await this.languageProvider.queryLabelValues(options.timeRange, options.key, match, '0')).map((v) => ({ + value: v, + text: v, + })); + } + + /** + * It creates a matcher string for resource calls + * @param queries + * @param adhocFilters + * + * @example + * queries=[{expr:`metricName{label="value"}`}] + * adhocFilters={key:"instance", operator:"=", value:"localhost"} + * returns {__name__=~"metricName", instance="localhost"} + */ + extractResourceMatcher(queries: PromQuery[], adhocFilters: AdHocVariableFilter[]): string { + // Extract metric names from queries we have already + const metricMatch = populateMatchParamsFromQueries(queries); + const labelFilters: QueryBuilderLabelFilter[] = adhocFilters.map((f) => ({ label: f.key, value: f.value, op: f.operator, })); - - const expr = promQueryModeller.renderLabels(labelFilters); - - if (this.hasLabelsMatchAPISupport()) { - return ( - await this.languageProvider.fetchSeriesValuesWithMatch(options.timeRange, options.key, expr, requestId) - ).map((v) => ({ - value: v, - text: v, - })); - } - - const params = this.getTimeRangeParams(options.timeRange ?? getDefaultTimeRange()); - const result = await this.metadataRequest(`/api/v1/label/${options.key}/values`, params); - return result?.data?.data?.map((value: any) => ({ text: value })) ?? []; + // Extract label filters from the filters we have already + const labelsMatch = renderLabelsWithoutBrackets(labelFilters); + // Create a matcher using metric names and label filters + return `{${[metricMatch, ...labelsMatch].join(',')}}`; } interpolateVariablesInQueries( diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index b45489a25dc..5b1af5244cc 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -861,47 +861,47 @@ describe('PrometheusLanguageProvider with feature toggle', () => { describe('populateMatchParamsFromQueries', () => { it('should add match params from queries', () => { - const initialParams = new URLSearchParams(); const queries: PromQuery[] = [ { expr: 'metric1', refId: '1' }, { expr: 'metric2', refId: '2' }, ]; - - const result = populateMatchParamsFromQueries(initialParams, queries); - - const matches = Array.from(result.getAll('match[]')); - expect(matches).toContain('metric1'); - expect(matches).toContain('metric2'); + const result = populateMatchParamsFromQueries(queries); + expect(result).toBe(`__name__=~"metric1|metric2"`); }); it('should handle binary queries', () => { - const initialParams = new URLSearchParams(); const queries: PromQuery[] = [{ expr: 'binary{label="val"} + second{}', refId: '1' }]; - - const result = populateMatchParamsFromQueries(initialParams, queries); - - const matches = Array.from(result.getAll('match[]')); - expect(matches).toContain('binary'); - expect(matches).toContain('second'); + const result = populateMatchParamsFromQueries(queries); + expect(result).toBe(`__name__=~"binary|second"`); }); it('should handle undefined queries', () => { - const initialParams = new URLSearchParams({ param: 'value' }); - - const result = populateMatchParamsFromQueries(initialParams, undefined); - - expect(result.toString()).toBe('param=value'); + const result = populateMatchParamsFromQueries(undefined); + expect(result).toBe('__name__!=""'); }); it('should handle UTF8 metrics', () => { - // Using the mocked isValidLegacyName function from jest.mock setup - const initialParams = new URLSearchParams(); const queries: PromQuery[] = [{ expr: '{"utf8.metric", label="value"}', refId: '1' }]; + const result = populateMatchParamsFromQueries(queries); + expect(result).toContain('__name__=~"utf8.metric"'); + }); - const result = populateMatchParamsFromQueries(initialParams, queries); + it('should handle UTF8 metrics with normal metrics', () => { + const queries: PromQuery[] = [{ expr: '{"utf8.metric", label="value"} + second{}', refId: '1' }]; + const result = populateMatchParamsFromQueries(queries); + expect(result).toContain('__name__=~"utf8.metric|second"'); + }); - const matches = Array.from(result.getAll('match[]')); - expect(matches).toContain('{"utf8.metric"}'); + it('should return match-all matcher if there is no expr in queries', () => { + const queries: PromQuery[] = [{ expr: '', refId: '1' }]; + const result = populateMatchParamsFromQueries(queries); + expect(result).toBe('__name__!=""'); + }); + + it('should return match-all matcher if there is no query', () => { + const queries: PromQuery[] = []; + const result = populateMatchParamsFromQueries(queries); + expect(result).toBe('__name__!=""'); }); }); }); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index e6f34923067..fb1220d001a 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -17,7 +17,7 @@ import { import { BackendSrvRequest } from '@grafana/runtime'; import { buildCacheHeaders, getDaysToCacheMetadata, getDefaultCacheHeaders } from './caching'; -import { REMOVE_SERIES_LIMIT, DEFAULT_SERIES_LIMIT } from './components/metrics-browser/types'; +import { DEFAULT_SERIES_LIMIT, REMOVE_SERIES_LIMIT } from './components/metrics-browser/types'; import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; import { PrometheusDatasource } from './datasource'; import { @@ -30,14 +30,14 @@ import { import PromqlSyntax from './promql'; import { buildVisualQueryFromString } from './querybuilder/parsing'; import { LabelsApiClient, ResourceApiClient, SeriesApiClient } from './resource_clients'; -import { PromMetricsMetadata, PromQuery } from './types'; +import { MATCH_ALL_LABELS_STR, PromMetricsMetadata, PromQuery } from './types'; import { escapeForUtf8Support, isValidLegacyName } from './utf8_support'; const DEFAULT_KEYS = ['job', 'instance']; const EMPTY_SELECTOR = '{}'; /** - * Prometheus API endpoints for fetching resoruces + * Prometheus API endpoints for fetching resources */ const API_V1 = { METADATA: '/api/v1/metadata', @@ -785,23 +785,26 @@ function getNameLabelValue(promQuery: string, tokens: Array { - return (queries ?? []).reduce((params, query) => { +export const populateMatchParamsFromQueries = (queries?: PromQuery[]): string => { + if (!queries) { + return MATCH_ALL_LABELS_STR; + } + + const metrics = (queries ?? []).reduce((params, query) => { const visualQuery = buildVisualQueryFromString(query.expr); - const isUtf8Metric = !isValidLegacyName(visualQuery.query.metric); - params.append('match[]', isUtf8Metric ? `{"${visualQuery.query.metric}"}` : visualQuery.query.metric); + if (visualQuery.query.metric !== '') { + params.push(visualQuery.query.metric); + } if (visualQuery.query.binaryQueries) { visualQuery.query.binaryQueries.forEach((bq) => { - params.append('match[]', isUtf8Metric ? `{"${bq.query.metric}"}` : bq.query.metric); + params.push(bq.query.metric); }); } return params; - }, initialParams); + }, []); + + return metrics.length === 0 ? MATCH_ALL_LABELS_STR : `__name__=~"${metrics.join('|')}"`; }; diff --git a/packages/grafana-prometheus/src/metric_find_query.test.ts b/packages/grafana-prometheus/src/metric_find_query.test.ts index 8a0aabe6480..74adce9bfb1 100644 --- a/packages/grafana-prometheus/src/metric_find_query.test.ts +++ b/packages/grafana-prometheus/src/metric_find_query.test.ts @@ -1,464 +1,233 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/metric_find_query.test.ts -import { Observable, of } from 'rxjs'; - -import { DataSourceInstanceSettings, TimeRange, toUtc } from '@grafana/data'; -import { BackendDataSourceResponse, BackendSrvRequest, FetchResponse, TemplateSrv } from '@grafana/runtime'; +import { getDefaultTimeRange, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from './datasource'; -import { getPrometheusTime } from './language_utils'; import { PrometheusMetricFindQuery } from './metric_find_query'; -import { PromApplication, PromOptions } from './types'; -import { escapeForUtf8Support } from './utf8_support'; -const fetchMock = jest.fn((options: BackendSrvRequest): Observable> => { - return of({} as unknown as FetchResponse); -}); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => { - return { - fetch: fetchMock, - }; - }, -})); - -const instanceSettings = { - url: 'proxied', - id: 1, - uid: 'ABCDEF', - user: 'test', - password: 'mupp', - jsonData: { - httpMethod: 'GET', - prometheusVersion: '2.20.0', - prometheusType: PromApplication.Prometheus, - }, -} as Partial> as DataSourceInstanceSettings; -const raw: TimeRange = { - from: toUtc('2018-04-25 10:00'), - to: toUtc('2018-04-25 11:00'), - raw: { - from: '2018-04-25 10:00', - to: '2018-04-25 11:00', - }, -}; - -const templateSrvStub = { - getAdhocFilters: jest.fn().mockImplementation(() => []), - replace: jest.fn().mockImplementation((a: string) => a), -} as unknown as TemplateSrv; - -beforeEach(() => { - jest.clearAllMocks(); -}); +jest.mock('./datasource'); describe('PrometheusMetricFindQuery', () => { - let legacyPrometheusDatasource: PrometheusDatasource; - let prometheusDatasource: PrometheusDatasource; + let datasource: jest.Mocked; + let timeRange: TimeRange; + beforeEach(() => { - legacyPrometheusDatasource = new PrometheusDatasource(instanceSettings, templateSrvStub); - prometheusDatasource = new PrometheusDatasource( - { - ...instanceSettings, - jsonData: { ...instanceSettings.jsonData, prometheusVersion: '2.2.0', prometheusType: PromApplication.Mimir }, + datasource = { + metadataRequest: jest.fn(), + languageProvider: { + start: jest.fn(), + queryLabelKeys: jest.fn().mockResolvedValue([]), + queryLabelValues: jest.fn().mockResolvedValue([]), }, - templateSrvStub - ); + getTagKeys: jest.fn(), + } as unknown as jest.Mocked; + + timeRange = getDefaultTimeRange(); }); - const setupMetricFindQuery = ( - data: { - query: string; - response: { - data: unknown; - }; - }, - datasource?: PrometheusDatasource - ) => { - fetchMock.mockImplementation(() => of({ status: 'success', data: data.response } as unknown as FetchResponse)); - return new PrometheusMetricFindQuery(datasource ?? legacyPrometheusDatasource, data.query); - }; + describe('Label Names Query', () => { + it('should call getTagKeys for simple label_names query', async () => { + const expectedLabelNames = [{ text: 'label1' }, { text: 'label2' }, { text: 'label3' }]; + datasource.getTagKeys.mockResolvedValue(expectedLabelNames); - describe('When performing metricFindQuery', () => { - it('label_names() should generate label name search query', async () => { - const query = setupMetricFindQuery({ - query: 'label_names()', - response: { - data: ['name1', 'name2', 'name3'], - }, - }); - const results = await query.process(raw); + const query = new PrometheusMetricFindQuery(datasource, 'label_names()'); + const results = await query.process(timeRange); - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/labels?start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - showErrorAlert: false, - headers: { - 'X-Grafana-Cache': 'private, max-age=60', - }, - }); + expect(results).toEqual(expectedLabelNames); + expect(datasource.getTagKeys).toHaveBeenCalledWith({ filters: [], timeRange }); }); - it('label_values(resource) should generate label search query', async () => { - const query = setupMetricFindQuery({ - query: 'label_values(resource)', - response: { - data: ['value1', 'value2', 'value3'], - }, - }); - const results = await query.process(raw); + it('should call queryLabelKeys with correct parameters', async () => { + const query = new PrometheusMetricFindQuery(datasource, 'label_names(metric_name) '); + await query.process(timeRange); + expect(datasource.languageProvider.queryLabelKeys).toHaveBeenCalledWith( + timeRange, + '{__name__=~".*metric_name.*"}' + ); + }); + }); - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/resource/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - headers: {}, - }); + describe('Label Values Query', () => { + it('should call queryLabelValues with correct parameters without metric filter', async () => { + const query = new PrometheusMetricFindQuery(datasource, 'label_values(label1)'); + await query.process(timeRange); + + expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith(timeRange, 'label1', undefined); }); - const emptyFilters = ['{}', '{ }', ' { } ', ' {} ']; + it('should call queryLabelValues with correct parameters with metric filter', async () => { + const query = new PrometheusMetricFindQuery(datasource, 'label_values(metric{label="value"}, label1)'); + await query.process(timeRange); - emptyFilters.forEach((emptyFilter) => { - const queryString = `label_values(${emptyFilter}, resource)`; - it(`Empty filter, query, ${queryString} should just generate label search query`, async () => { - const query = setupMetricFindQuery({ - query: queryString, - response: { - data: ['value1', 'value2', 'value3'], + expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith( + timeRange, + 'label1', + 'metric{label="value"}' + ); + }); + }); + + describe('Metric Names Query', () => { + it('should call queryLabelValues with correct parameters', async () => { + const query = new PrometheusMetricFindQuery(datasource, 'metrics(.*metric.*)'); + await query.process(timeRange); + + expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith( + timeRange, + '__name__', + '{__name__=~".*metric.*"}' + ); + }); + }); + + describe('Query Result', () => { + it('should handle scalar result', async () => { + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { + data: { + resultType: 'scalar', + result: [1234567, '42'], }, - }); - const results = await query.process(raw); - - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/resource/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - headers: {}, - }); - }); - }); - - // - it('label_values(metric, resource) should generate series query with correct time', async () => { - const query = setupMetricFindQuery({ - query: 'label_values(metric, resource)', - response: { - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: 'value3' }, - ], }, }); - const results = await query.process(raw); - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/series?match${encodeURIComponent( - '[]' - )}=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - showErrorAlert: false, - headers: {}, - }); + const query = new PrometheusMetricFindQuery(datasource, 'query_result(sum(metric))'); + const results = await query.process(timeRange); + + expect(results).toEqual([{ text: '42', expandable: false }]); }); - it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query with correct time', async () => { - const query = setupMetricFindQuery({ - query: 'label_values(metric{label1="foo", label2="bar", label3="baz"}, resource)', - response: { - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: 'value3' }, - ], - }, - }); - const results = await query.process(raw); - - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: '/api/datasources/uid/ABCDEF/resources/api/v1/series?match%5B%5D=metric%7Blabel1%3D%22foo%22%2C%20label2%3D%22bar%22%2C%20label3%3D%22baz%22%7D&start=1524650400&end=1524654000', - hideFromInspector: true, - showErrorAlert: false, - headers: {}, - }); - }); - - it('label_values(metric, resource) result should not contain empty string', async () => { - const query = setupMetricFindQuery({ - query: 'label_values(metric, resource)', - response: { - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: '' }, - ], - }, - }); - const results = await query.process(raw); - - expect(results).toHaveLength(2); - expect(results[0].text).toBe('value1'); - expect(results[1].text).toBe('value2'); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/series?match${encodeURIComponent( - '[]' - )}=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - showErrorAlert: false, - headers: {}, - }); - }); - // - - it('metrics(metric.*) should generate metric name query', async () => { - const query = setupMetricFindQuery({ - query: 'metrics(metric.*)', - response: { - data: ['metric1', 'metric2', 'metric3', 'nomatch'], - }, - }); - const results = await query.process(raw); - - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/__name__/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - headers: {}, - }); - }); - - it('query_result(metric) should generate metric name query', async () => { - const query = setupMetricFindQuery({ - query: 'query_result(metric)', - response: { + it('should handle vector result', async () => { + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { data: { resultType: 'vector', result: [ { - metric: { __name__: 'metric', job: 'testjob' }, - value: [1443454528.0, '3846'], + metric: { __name__: 'metric', label: 'value' }, + value: [1234567, '42'], }, ], }, }, }); - const results = await query.process(raw); - expect(results).toHaveLength(1); - expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000'); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric&time=${raw.to.unix()}`, - headers: {}, - hideFromInspector: true, - showErrorAlert: false, - }); - }); + const query = new PrometheusMetricFindQuery(datasource, 'query_result(metric)'); + const results = await query.process(timeRange); - it('query_result(metric) should pass time parameter to datasource.metric_find_query', async () => { - const query = setupMetricFindQuery({ - query: 'query_result(metric)', - response: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'metric', job: 'testjob' }, - value: [1443454528.0, '3846'], - }, - ], - }, + expect(results).toEqual([ + { + text: 'metric{label="value"} 42 1234567000', + expandable: true, }, - }); - const results = await query.process(raw); - - const expectedTime = getPrometheusTime(raw.to, true); - - expect(results).toHaveLength(1); - expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000'); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric&time=${expectedTime}`, - headers: {}, - hideFromInspector: true, - showErrorAlert: false, - }); + ]); }); - it('query_result(metric) should handle scalar resultTypes separately', async () => { - const query = setupMetricFindQuery({ - query: 'query_result(1+1)', - response: { + it('should handle scalar result with timestamp', async () => { + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { data: { resultType: 'scalar', result: [1443454528.0, '2'], }, }, }); - const results = await query.process(raw); - expect(results).toHaveLength(1); - expect(results[0].text).toBe('2'); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=1%2B1&time=${raw.to.unix()}`, - headers: {}, - hideFromInspector: true, - showErrorAlert: false, - }); + + const query = new PrometheusMetricFindQuery(datasource, 'query_result(1+1)'); + const results = await query.process(timeRange); + + expect(results).toEqual([{ text: '2', expandable: false }]); }); - it('up{job="job1"} should fallback using generate series query', async () => { - const query = setupMetricFindQuery({ - query: 'up{job="job1"}', - response: { - data: [ - { __name__: 'up', instance: '127.0.0.1:1234', job: 'job1' }, - { __name__: 'up', instance: '127.0.0.1:5678', job: 'job1' }, - { __name__: 'up', instance: '127.0.0.1:9102', job: 'job1' }, - ], - }, - }); - const results = await query.process(raw); - - expect(results).toHaveLength(3); - expect(results[0].text).toBe('up{instance="127.0.0.1:1234",job="job1"}'); - expect(results[1].text).toBe('up{instance="127.0.0.1:5678",job="job1"}'); - expect(results[2].text).toBe('up{instance="127.0.0.1:9102",job="job1"}'); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/series?match${encodeURIComponent('[]')}=${encodeURIComponent( - 'up{job="job1"}' - )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - showErrorAlert: false, - headers: {}, - }); - }); - - // - it('label_values(metric, resource) should generate label values query with correct time', async () => { - const metricName = 'metricName'; - const resourceName = 'resourceName'; - const query = setupMetricFindQuery( - { - query: `label_values(${metricName}, ${resourceName})`, - response: { - data: [ - { __name__: `${metricName}`, resourceName: 'value1' }, - { __name__: `${metricName}`, resourceName: 'value2' }, - { __name__: `${metricName}`, resourceName: 'value3' }, + it('should handle vector result with metric name and labels', async () => { + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'metric', job: 'testjob' }, + value: [1443454528.0, '3846'], + }, ], }, }, - prometheusDatasource - ); - const results = await query.process(raw); - - expect(results).toHaveLength(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/${resourceName}/values?match${encodeURIComponent( - '[]' - )}=${metricName}&start=${raw.from.unix()}&end=${raw.to.unix()}`, - hideFromInspector: true, - headers: {}, }); + + const query = new PrometheusMetricFindQuery(datasource, 'query_result(metric)'); + const results = await query.process(timeRange); + + expect(results).toEqual([{ text: 'metric{job="testjob"} 3846 1443454528000', expandable: true }]); }); - it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate label values query with correct time', async () => { - const metricName = 'metricName'; - const resourceName = 'resourceName'; - const label1Name = 'label1'; - const label1Value = 'label1Value'; - const query = setupMetricFindQuery( - { - query: `label_values(${metricName}{${label1Name}="${label1Value}"}, ${resourceName})`, - response: { - data: [{ __name__: metricName, resourceName: label1Value }], + it('should throw error for unknown result type', async () => { + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { + data: { + resultType: 'unknown', }, }, - prometheusDatasource - ); - const results = await query.process(raw); + }); - expect(results).toHaveLength(1); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/${resourceName}/values?match%5B%5D=${metricName}%7B${label1Name}%3D%22${label1Value}%22%7D&start=1524650400&end=1524654000`, - hideFromInspector: true, - headers: {}, + const query = new PrometheusMetricFindQuery(datasource, 'query_result(metric)'); + await expect(query.process(timeRange)).rejects.toThrow('Unknown/Unhandled result type: [unknown]'); + }); + }); + + describe('Series Query', () => { + it('should return series', async () => { + const metric = { __name__: 'metric', label: 'value' }; + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { + data: [metric], + }, + }); + + const query = new PrometheusMetricFindQuery(datasource, 'metric{label="value"}'); + const results = await query.process(timeRange); + + expect(results).toEqual([{ text: 'metric{label="value"}', expandable: true }]); + expect(datasource.metadataRequest).toHaveBeenCalledWith('/api/v1/series', { + 'match[]': 'metric{label="value"}', + start: expect.any(String), + end: expect.any(String), }); }); - // - describe('utf8 metric and label support', () => { - it('utf8 label - label_values(a_utf8_http_requests_total,instance.test) should generate label values query', () => { - const metricName = 'a_utf8_http_requests_total'; - const label = 'instance.test'; - const query = `label_values(${metricName},${label})`; - const metricFindQuery = new PrometheusMetricFindQuery(prometheusDatasource, query); - metricFindQuery.process(raw); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/${escapeForUtf8Support(label)}/values?match%5B%5D=${metricName}&start=1524650400&end=1524654000`, - hideFromInspector: true, - headers: {}, - }); + + it('should return series with metric name and labels', async () => { + const metric = { __name__: 'up', instance: '127.0.0.1:1234', job: 'job1' }; + datasource.metadataRequest = jest.fn().mockResolvedValue({ + data: { + data: [metric], + }, }); - it('utf8 metric - label_values(utf8.http_requests_total,instance_test) should generate label values query', () => { - const metricName = 'utf8.http_requests_total'; - const label = 'instance_test'; - const query = `label_values(${metricName},${label})`; - const metricFindQuery = new PrometheusMetricFindQuery(prometheusDatasource, query); - metricFindQuery.process(raw); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/${label}/values?match%5B%5D=${metricName}&start=1524650400&end=1524654000`, - hideFromInspector: true, - headers: {}, - }); - }); + const query = new PrometheusMetricFindQuery(datasource, 'up{job="job1"}'); + const results = await query.process(timeRange); - it('utf8 metric and label - label_values(utf8.http_requests_total,instance.test) should generate label values query', () => { - const metricName = 'utf8.http_requests_total'; - const label = 'instance.test'; - const query = `label_values(${metricName},${label})`; - const metricFindQuery = new PrometheusMetricFindQuery(prometheusDatasource, query); - metricFindQuery.process(raw); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith({ - method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/label/${escapeForUtf8Support(label)}/values?match%5B%5D=${metricName}&start=1524650400&end=1524654000`, - hideFromInspector: true, - headers: {}, - }); - }); + expect(results).toEqual([{ text: 'up{instance="127.0.0.1:1234",job="job1"}', expandable: true }]); + }); + }); + + describe('UTF-8 Support', () => { + it('should handle UTF-8 label names in label_values query', async () => { + datasource.languageProvider.queryLabelValues = jest.fn().mockResolvedValue(['value1', 'value2']); + + const query = new PrometheusMetricFindQuery(datasource, 'label_values(metric,instance.test)'); + await query.process(timeRange); + + expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith( + timeRange, + 'U__instance_2e_test', + 'metric' + ); + }); + + it('should handle UTF-8 metric names in label_values query', async () => { + datasource.languageProvider.queryLabelValues = jest.fn().mockResolvedValue(['value1', 'value2']); + + const query = new PrometheusMetricFindQuery(datasource, 'label_values(utf8.metric,label)'); + await query.process(timeRange); + + expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith(timeRange, 'label', 'utf8.metric'); }); }); }); diff --git a/packages/grafana-prometheus/src/metric_find_query.ts b/packages/grafana-prometheus/src/metric_find_query.ts index 0fb7d6d7db8..6568548ea28 100644 --- a/packages/grafana-prometheus/src/metric_find_query.ts +++ b/packages/grafana-prometheus/src/metric_find_query.ts @@ -1,5 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/metric_find_query.ts -import { chain, map as _map, uniq } from 'lodash'; +import { map as _map } from 'lodash'; import { MetricFindValue, TimeRange } from '@grafana/data'; @@ -13,7 +13,8 @@ import { PrometheusQueryResultRegex, } from './migrations/variableMigration'; import { getOriginalMetricName } from './result_transformer'; -import { escapeForUtf8Support, isValidLegacyName } from './utf8_support'; +import { METRIC_LABEL } from './types'; +import { escapeForUtf8Support } from './utf8_support'; export class PrometheusMetricFindQuery { constructor( @@ -24,7 +25,7 @@ export class PrometheusMetricFindQuery { this.query = query; } - process(timeRange: TimeRange): Promise { + async process(timeRange: TimeRange): Promise { const labelNamesRegex = PrometheusLabelNamesRegex; const labelNamesRegexWithMatch = PrometheusLabelNamesRegexWithMatch; const labelValuesRegex = PrometheusLabelValuesRegex; @@ -35,11 +36,8 @@ export class PrometheusMetricFindQuery { if (labelNamesMatchQuery) { const selector = `{__name__=~".*${labelNamesMatchQuery[1]}.*"}`; - return this.datasource.languageProvider.getSeriesLabels(timeRange, selector, []).then((results) => - results.map((result) => ({ - text: result, - })) - ); + const keys = await this.datasource.languageProvider.queryLabelKeys(timeRange, selector); + return keys.filter((key) => key !== METRIC_LABEL).map((result) => ({ text: result })); } if (labelNamesQuery) { @@ -51,16 +49,16 @@ export class PrometheusMetricFindQuery { const filter = labelValuesQuery[1]; const label = labelValuesQuery[2]; if (isFilterDefined(filter)) { - return this.labelValuesQuery(label, timeRange, filter); + return await this.labelValuesQuery(label, timeRange, filter); } else { // Exclude the filter part of the expression because it is blank or empty - return this.labelValuesQuery(label, timeRange); + return await this.labelValuesQuery(label, timeRange); } } const metricNamesQuery = this.query.match(metricNamesRegex); if (metricNamesQuery) { - return this.metricNameQuery(metricNamesQuery[1], timeRange); + return await this.metricNameQuery(metricNamesQuery[1], timeRange); } const queryResultQuery = this.query.match(queryResultRegex); @@ -71,73 +69,25 @@ export class PrometheusMetricFindQuery { // if query contains full metric name, return metric name and label list const expressions = ['label_values()', 'metrics()', 'query_result()']; if (!expressions.includes(this.query)) { - return this.metricNameAndLabelsQuery(this.query, timeRange); + return await this.metricNameAndLabelsQuery(this.query, timeRange); } return Promise.resolve([]); } - labelValuesQuery(label: string, range: TimeRange, metric?: string) { - const start = getPrometheusTime(range.from, false); - const end = getPrometheusTime(range.to, true); - const params = { ...(metric && { 'match[]': metric }), start: start.toString(), end: end.toString() }; - - let escapedLabel = label; - if (!isValidLegacyName(label)) { - escapedLabel = escapeForUtf8Support(label); - } - - if (!metric || this.datasource.hasLabelsMatchAPISupport()) { - const url = `/api/v1/label/${escapedLabel}/values`; - - return this.datasource.metadataRequest(url, params).then((result) => { - return _map(result.data.data, (value) => { - return { text: value }; - }); - }); - } else { - const url = `/api/v1/series`; - - return this.datasource.metadataRequest(url, params).then((result) => { - const _labels = _map(result.data.data, (metric) => { - return metric[label] || ''; - }).filter((label) => { - return label !== ''; - }); - - return uniq(_labels).map((metric) => { - return { - text: metric, - expandable: true, - }; - }); - }); - } + async labelValuesQuery(label: string, range: TimeRange, metric?: string) { + const escapedLabel = escapeForUtf8Support(label); + const values = await this.datasource.languageProvider.queryLabelValues(range, escapedLabel, metric); + return values.map((value) => ({ text: value })); } - metricNameQuery(metricFilterPattern: string, range: TimeRange) { - const start = getPrometheusTime(range.from, false); - const end = getPrometheusTime(range.to, true); - const params = { - start: start.toString(), - end: end.toString(), - }; - const url = `/api/v1/label/__name__/values`; - - return this.datasource.metadataRequest(url, params).then((result) => { - return chain(result.data.data) - .filter((metricName) => { - const r = new RegExp(metricFilterPattern); - return r.test(metricName); - }) - .map((matchedMetricName) => { - return { - text: matchedMetricName, - expandable: true, - }; - }) - .value(); - }); + async metricNameQuery(metricFilterPattern: string, range: TimeRange) { + const names = await this.datasource.languageProvider.queryLabelValues( + range, + METRIC_LABEL, + `{__name__=~"${metricFilterPattern}"}` + ); + return names.map((n) => ({ text: n, expandable: true })); } queryResultQuery(query: string, range: TimeRange) { @@ -179,7 +129,7 @@ export class PrometheusMetricFindQuery { }); } - metricNameAndLabelsQuery(query: string, range: TimeRange): Promise { + async metricNameAndLabelsQuery(query: string, range: TimeRange): Promise { const start = getPrometheusTime(range.from, false); const end = getPrometheusTime(range.to, true); const params = { @@ -188,16 +138,11 @@ export class PrometheusMetricFindQuery { end: end.toString(), }; - const url = `/api/v1/series`; - - return this.datasource.metadataRequest(url, params).then((result) => { - return _map(result.data.data, (metric: { [key: string]: string }) => { - return { - text: getOriginalMetricName(metric), - expandable: true, - }; - }); - }); + const result = await this.datasource.metadataRequest(`/api/v1/series`, params); + return result.data.data.map((metric: Record) => ({ + text: getOriginalMetricName(metric), + expandable: true, + })); } } diff --git a/packages/grafana-prometheus/src/querybuilder/shared/rendering/labels.ts b/packages/grafana-prometheus/src/querybuilder/shared/rendering/labels.ts index c06270dabac..2d8f59ce6f4 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/rendering/labels.ts +++ b/packages/grafana-prometheus/src/querybuilder/shared/rendering/labels.ts @@ -29,3 +29,22 @@ export function renderLabels(labels: QueryBuilderLabelFilter[]): string { return expr + `}`; } + +export function renderLabelsWithoutBrackets(labels: QueryBuilderLabelFilter[]): string[] { + if (labels.length === 0) { + return []; + } + + const renderedLabels: string[] = []; + for (const filter of labels) { + let labelValue = filter.value; + const usingRegexOperator = filter.op === '=~' || filter.op === '!~'; + + if (config.featureToggles.prometheusSpecialCharsInLabelValues && !usingRegexOperator) { + labelValue = prometheusRegularEscape(labelValue); + } + renderedLabels.push(`${utf8Support(filter.label)}${filter.op}"${labelValue}"`); + } + + return renderedLabels; +} diff --git a/packages/grafana-prometheus/src/resource_clients.test.ts b/packages/grafana-prometheus/src/resource_clients.test.ts index 4604a77e652..ac1499af17f 100644 --- a/packages/grafana-prometheus/src/resource_clients.test.ts +++ b/packages/grafana-prometheus/src/resource_clients.test.ts @@ -437,6 +437,60 @@ describe('SeriesApiClient', () => { expect(secondResult).toEqual(['grafana', 'prometheus']); expect(mockRequest).not.toHaveBeenCalled(); }); + + it('should create a proper matcher when the given match is a metric name only', async () => { + mockRequest.mockResolvedValue([ + { __name__: 'metric1', job: 'grafana' }, + { __name__: 'metric2', job: 'prometheus' }, + ]); + + await client.queryLabelValues(mockTimeRange, 'job', 'metric1'); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/series', + expect.objectContaining({ + 'match[]': '{__name__="metric1",job!=""}', + }), + expect.any(Object) + ); + }); + + it('should create a proper matcher when the given match is a query', async () => { + mockRequest.mockResolvedValue([ + { __name__: 'metric1', job: 'grafana' }, + { __name__: 'metric2', job: 'prometheus' }, + ]); + + await client.queryLabelValues(mockTimeRange, 'job', 'metric1{instance="test"}'); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/series', + expect.objectContaining({ + 'match[]': '{__name__="metric1",instance="test",job!=""}', + }), + expect.any(Object) + ); + }); + + it('should create a proper matcher when the given match is a utf8 query', async () => { + mockRequest.mockResolvedValue([ + { __name__: 'metric1', job: 'grafana' }, + { __name__: 'metric2', job: 'prometheus' }, + ]); + + await client.queryLabelValues(mockTimeRange, 'job', '{"metric.name", instance="test"}'); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/series', + expect.objectContaining({ + 'match[]': '{__name__="metric.name",instance="test",job!=""}', + }), + expect.any(Object) + ); + }); }); describe('SeriesCache', () => { diff --git a/packages/grafana-prometheus/src/resource_clients.ts b/packages/grafana-prometheus/src/resource_clients.ts index b0cd76c7cb8..d15da2f60ab 100644 --- a/packages/grafana-prometheus/src/resource_clients.ts +++ b/packages/grafana-prometheus/src/resource_clients.ts @@ -6,7 +6,8 @@ import { DEFAULT_SERIES_LIMIT } from './components/metrics-browser/types'; import { PrometheusDatasource } from './datasource'; import { removeQuotesIfExist } from './language_provider'; import { getRangeSnapInterval, processHistogramMetrics } from './language_utils'; -import { PrometheusCacheLevel } from './types'; +import { buildVisualQueryFromString } from './querybuilder/parsing'; +import { EMPTY_MATCHER, MATCH_ALL_LABELS, METRIC_LABEL, PrometheusCacheLevel } from './types'; import { escapeForUtf8Support, utf8Support } from './utf8_support'; type PrometheusSeriesResponse = Array<{ [key: string]: string }>; @@ -33,10 +34,6 @@ type RequestFn = ( options?: Partial ) => Promise; -const EMPTY_MATCHER = '{}'; -const MATCH_ALL_LABELS = '{__name__!=""}'; -const METRIC_LABEL = '__name__'; - export abstract class BaseResourceClient { constructor( protected readonly request: RequestFn, @@ -208,10 +205,24 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl limit: string = DEFAULT_SERIES_LIMIT ): Promise => { const utf8SafeLabelKey = utf8Support(labelKey); - const effectiveMatch = - !match || match === EMPTY_MATCHER - ? `{${utf8SafeLabelKey}!=""}` - : match.slice(0, match.length - 1).concat(`,${utf8SafeLabelKey}!=""}`); + let effectiveMatch = ''; + if (!match || match === EMPTY_MATCHER) { + // Just and empty matcher {} or no matcher + effectiveMatch = `{${utf8SafeLabelKey}!=""}`; + } else { + const { + query: { metric, labels }, + } = buildVisualQueryFromString(match); + labels.push({ + label: utf8SafeLabelKey, + op: '!=', + value: '', + }); + const metricFilter = metric ? `__name__="${metric}",` : ''; + const labelFilters = labels.map((lf) => `${utf8Support(lf.label)}${lf.op}"${lf.value}"`).join(','); + effectiveMatch = `{${metricFilter}${labelFilters}}`; + } + const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, limit); if (maybeCachedValues) { return maybeCachedValues; @@ -234,6 +245,9 @@ class ResourceClientsCache { constructor(private cacheLevel: PrometheusCacheLevel = PrometheusCacheLevel.High) {} public setLabelKeys(timeRange: TimeRange, match: string, limit: string, keys: string[]) { + if (keys.length === 0) { + return; + } // Check and potentially clean cache before adding new entry this.cleanCacheIfNeeded(); const cacheKey = this.getCacheKey(timeRange, match, limit, 'key'); @@ -252,6 +266,9 @@ class ResourceClientsCache { } public setLabelValues(timeRange: TimeRange, match: string, limit: string, values: string[]) { + if (values.length === 0) { + return; + } // Check and potentially clean cache before adding new entry this.cleanCacheIfNeeded(); const cacheKey = this.getCacheKey(timeRange, match, limit, 'value'); diff --git a/packages/grafana-prometheus/src/types.ts b/packages/grafana-prometheus/src/types.ts index 709d81af22a..cb577ecec52 100644 --- a/packages/grafana-prometheus/src/types.ts +++ b/packages/grafana-prometheus/src/types.ts @@ -189,3 +189,8 @@ export type RecordingRuleIdentifier = { identifier?: string; identifierValue?: string; }; + +export const EMPTY_MATCHER = '{}'; +export const MATCH_ALL_LABELS_STR = '__name__!=""'; +export const MATCH_ALL_LABELS = '{__name__!=""}'; +export const METRIC_LABEL = '__name__';