Prometheus: Use new language provider methods for variable handling (#106277)

* refactor language provider

* update tests

* more tests

* betterer and api endpoints

* copilot updates

* betterer

* remove default value

* prettier

* introduce new methods

* provide unit tests for labelValues

* update metadata fetch

* move all cache related stuff in caching.ts

* provide interface

* provide deprecation messages

* unit tests for new interface

* separation of concerns

* update tests

* fix unit test

* fix some types

* Revert "fix some types"

This reverts commit 7e64b93b5f.

* revert interface usage

* betterer

* use PrometheusLanguageProviderInterface in everywhere

* introduce resource clients

* unit tests

* act accordingly with the feature toggle

* some more unit tests

* add feature toggle

* Revert "add feature toggle"

This reverts commit 5c93ac324f.

* remove feature toggle

* update tests

* backward compatibility

* fix scope issues

* comment update

* stronger types

* prettier

* betterer

* use new methods in metrics browser and query field

* always return data

* Revert "always return data"

This reverts commit 38e493c189.

* Revert "Revert "always return data""

This reverts commit b5d3b5d2b0.

* handle error

* lint

* use new method in query builder

* fix metrics modal tests too

* use labelValues method while searching in combobox

* update metrics modal regex search

* lint

* fix unit test

* introduce resource clients and better refactoring

* prettier

* type fixes

* betterer

* no empty matcher for series calls

* better matchers

* add additional tests

* proper match string for series

* introduce series cache

* introduce series cache for series label values

* lint

* cache values too

* utf8 safe label values query with series endpoint

* fix unit tests

* caching for labels api client

* betterer

* support new language provider methods in variable editor

* use queries and adhoc filters to extract matchers

* filter out __name__

* improve match string in series query label value calls

* update unit tests

* betterer

* betterer
This commit is contained in:
ismail simsek
2025-06-18 20:36:13 +02:00
committed by GitHub
parent dd7fbb7b64
commit f273c1c373
13 changed files with 472 additions and 608 deletions
+1 -2
View File
@@ -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"],
@@ -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<PrometheusLanguageProviderInterface>,
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<PrometheusDatasource> as PrometheusDatasource,
query: {
refId: 'test',
@@ -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<string, string[]>) => {
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]);
@@ -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],
+32 -34
View File
@@ -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<PromQuery>): 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<string, string[]> = 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<PromQuery>=[{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(
@@ -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__!=""');
});
});
});
@@ -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<string | Prism.Token
* This is used to filter time series data based on existing queries.
* Handles UTF8 metrics by properly escaping them.
*
* @param {URLSearchParams} initialParams - Initial URL parameters
* @param {PromQuery[]} queries - Array of Prometheus queries
* @returns {URLSearchParams} URL parameters with match[] parameters added
* @returns {string} Metric names as a regex matcher
*/
export const populateMatchParamsFromQueries = (
initialParams: URLSearchParams,
queries?: PromQuery[]
): URLSearchParams => {
return (queries ?? []).reduce((params, query) => {
export const populateMatchParamsFromQueries = (queries?: PromQuery[]): string => {
if (!queries) {
return MATCH_ALL_LABELS_STR;
}
const metrics = (queries ?? []).reduce<string[]>((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('|')}"`;
};
@@ -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<FetchResponse<BackendDataSourceResponse>> => {
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<DataSourceInstanceSettings<PromOptions>> as DataSourceInstanceSettings<PromOptions>;
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<PrometheusDatasource>;
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<PrometheusDatasource>;
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: {},
});
});
});
// <LegacyPrometheus>
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: {},
});
});
// </LegacyPrometheus>
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: {},
});
});
// <ModernPrometheus>
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),
});
});
// </ ModernPrometheus>
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');
});
});
});
@@ -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<MetricFindValue[]> {
async process(timeRange: TimeRange): Promise<MetricFindValue[]> {
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<MetricFindValue[]> {
async metricNameAndLabelsQuery(query: string, range: TimeRange): Promise<MetricFindValue[]> {
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<string, string>) => ({
text: getOriginalMetricName(metric),
expandable: true,
}));
}
}
@@ -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;
}
@@ -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', () => {
@@ -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<BackendSrvRequest>
) => Promise<unknown>;
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<string[]> => {
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');
+5
View File
@@ -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__';