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 commit7e64b93b5f. * 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 commit5c93ac324f. * 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 commit38e493c189. * Revert "Revert "always return data"" This reverts commitb5d3b5d2b0. * 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:
+1
-2
@@ -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],
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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__';
|
||||
|
||||
Reference in New Issue
Block a user