Chore: Remove deprecated language_provider methods in prometheus package (#114361)
* remove deprecated language provider and its methods * remove more deprecated code * yarn lint:prune
This commit is contained in:
@@ -397,7 +397,7 @@
|
||||
},
|
||||
"packages/grafana-prometheus/src/language_provider.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 7
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"packages/grafana-prometheus/src/language_utils.ts": {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory.
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const SUGGESTIONS_LIMIT = 10000;
|
||||
|
||||
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
|
||||
|
||||
@@ -45,8 +41,3 @@ export const GET_AND_POST_METADATA_ENDPOINTS = [
|
||||
'api/v1/labels',
|
||||
'suggestions',
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const REMOVE_SERIES_LIMIT = 'none';
|
||||
|
||||
@@ -86,7 +86,6 @@ export { PrometheusVariableSupport } from './variables';
|
||||
export type { PrometheusLanguageProviderInterface } from './language_provider';
|
||||
|
||||
// For Metrics Drilldown
|
||||
export { default as PromQlLanguageProvider } from './language_provider';
|
||||
export { getPrometheusTime } from './language_utils';
|
||||
export { isValidLegacyName, utf8Support, wrapUtf8Filters } from './utf8_support';
|
||||
export { buildVisualQueryFromString } from './querybuilder/parsing';
|
||||
|
||||
@@ -10,16 +10,6 @@ export class EmptyLanguageProviderMock {
|
||||
});
|
||||
}
|
||||
|
||||
getLabelKeys = jest.fn().mockReturnValue([]);
|
||||
getLabelValues = jest.fn().mockReturnValue([]);
|
||||
getSeries = jest.fn().mockReturnValue({ __name__: [] });
|
||||
getSeriesValues = jest.fn().mockReturnValue([]);
|
||||
fetchSeries = jest.fn().mockReturnValue([]);
|
||||
fetchSeriesLabels = jest.fn().mockReturnValue([]);
|
||||
fetchSeriesLabelsMatch = jest.fn().mockReturnValue([]);
|
||||
fetchLabelsWithMatch = jest.fn().mockReturnValue([]);
|
||||
fetchLabels = jest.fn();
|
||||
loadMetricsMetadata = jest.fn();
|
||||
retrieveMetrics = jest.fn().mockReturnValue(['metric']);
|
||||
queryLabelKeys = jest.fn().mockResolvedValue([]);
|
||||
queryLabelValues = jest.fn().mockResolvedValue([]);
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts
|
||||
import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data';
|
||||
|
||||
import { getCacheDurationInMinutes } from './caching';
|
||||
import { DEFAULT_SERIES_LIMIT } from './constants';
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import {
|
||||
exportToAbstractQuery,
|
||||
importFromAbstractQuery,
|
||||
populateMatchParamsFromQueries,
|
||||
PrometheusLanguageProvider,
|
||||
} from './language_provider';
|
||||
import { getPrometheusTime, getRangeSnapInterval } from './language_utils';
|
||||
import { PrometheusCacheLevel, PromQuery } from './types';
|
||||
|
||||
jest.mock('./language_utils', () => ({
|
||||
...jest.requireActual('./language_utils'),
|
||||
getPrometheusTime: jest.requireActual('./language_utils').getPrometheusTime,
|
||||
getRangeSnapInterval: jest.requireActual('./language_utils').getRangeSnapInterval,
|
||||
}));
|
||||
|
||||
import { getCacheDurationInMinutes } from './caching';
|
||||
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
|
||||
import { DEFAULT_SERIES_LIMIT } from './constants';
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import {
|
||||
exportToAbstractQuery,
|
||||
importFromAbstractQuery,
|
||||
PrometheusLanguageProviderInterface,
|
||||
PrometheusLanguageProvider,
|
||||
populateMatchParamsFromQueries,
|
||||
} from './language_provider';
|
||||
import { getPrometheusTime, getRangeSnapInterval } from './language_utils';
|
||||
import { PrometheusCacheLevel, PromQuery } from './types';
|
||||
|
||||
const now = new Date(1681300293392).getTime();
|
||||
const timeRangeDurationSeconds = 1;
|
||||
const toPrometheusTime = getPrometheusTime(dateTime(now), false);
|
||||
@@ -58,607 +56,7 @@ const getMockQuantizedTimeRangeParams = (override?: Partial<TimeRange>): TimeRan
|
||||
...override,
|
||||
});
|
||||
|
||||
// Common test helper to verify request parameters
|
||||
const verifyRequestParams = (
|
||||
requestSpy: jest.SpyInstance,
|
||||
expectedUrl: string,
|
||||
expectedParams: unknown,
|
||||
expectedOptions?: unknown
|
||||
) => {
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy).toHaveBeenCalledWith(expectedUrl, expect.objectContaining(expectedParams), expectedOptions);
|
||||
};
|
||||
|
||||
describe('Prometheus Language Provider', () => {
|
||||
const defaultDatasource: PrometheusDatasource = {
|
||||
metadataRequest: () => ({ data: { data: [] } }),
|
||||
getTimeRangeParams: getTimeRangeParams,
|
||||
interpolateString: (string: string) => string,
|
||||
hasLabelsMatchAPISupport: () => false,
|
||||
getDaysToCacheMetadata: () => 1,
|
||||
getAdjustedInterval: () => getRangeSnapInterval(PrometheusCacheLevel.None, getMockQuantizedTimeRangeParams()),
|
||||
cacheLevel: PrometheusCacheLevel.None,
|
||||
getIntervalVars: () => ({}),
|
||||
getRangeScopedVars: () => ({}),
|
||||
seriesLimit: DEFAULT_SERIES_LIMIT,
|
||||
} as unknown as PrometheusDatasource;
|
||||
|
||||
describe('Series and label fetching', () => {
|
||||
const timeRange = getMockTimeRange();
|
||||
|
||||
describe('getSeries', () => {
|
||||
it('should use fetchDefaultSeries for empty selector', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
const fetchDefaultSeriesSpy = jest.spyOn(languageProvider, 'fetchDefaultSeries');
|
||||
fetchDefaultSeriesSpy.mockResolvedValue({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] });
|
||||
|
||||
const result = await languageProvider.getSeries(timeRange, '{}');
|
||||
|
||||
expect(fetchDefaultSeriesSpy).toHaveBeenCalledWith(timeRange);
|
||||
expect(result).toEqual({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] });
|
||||
});
|
||||
|
||||
it('should use fetchSeriesLabels for non-empty selector', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
const fetchSeriesLabelsSpy = jest.spyOn(languageProvider, 'fetchSeriesLabels');
|
||||
fetchSeriesLabelsSpy.mockResolvedValue({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] });
|
||||
|
||||
const result = await languageProvider.getSeries(timeRange, '{job="grafana"}');
|
||||
|
||||
expect(fetchSeriesLabelsSpy).toHaveBeenCalledWith(timeRange, '{job="grafana"}', undefined, 'none');
|
||||
expect(result).toEqual({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] });
|
||||
});
|
||||
|
||||
it('should include name label when withName is true', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
const fetchSeriesLabelsSpy = jest.spyOn(languageProvider, 'fetchSeriesLabels');
|
||||
fetchSeriesLabelsSpy.mockResolvedValue({ __name__: ['metric1', 'metric2'], job: ['job1'] });
|
||||
|
||||
const result = await languageProvider.getSeries(timeRange, '{job="grafana"}', true);
|
||||
|
||||
expect(fetchSeriesLabelsSpy).toHaveBeenCalledWith(timeRange, '{job="grafana"}', true, 'none');
|
||||
expect(result).toHaveProperty('__name__');
|
||||
expect(result.__name__).toEqual(['metric1', 'metric2']);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
jest.spyOn(languageProvider, 'fetchSeriesLabels').mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await languageProvider.getSeries(timeRange, '{job="grafana"}');
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeriesLabels', () => {
|
||||
it('should call labels endpoint when API support is available', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
hasLabelsMatchAPISupport: () => true,
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesLabels = languageProvider.getSeriesLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
const labelName = 'job';
|
||||
const labelValue = 'grafana';
|
||||
getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [
|
||||
{
|
||||
name: labelName,
|
||||
value: labelValue,
|
||||
op: '=',
|
||||
},
|
||||
] as Label[]);
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/labels', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': '{job="grafana"}',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call series endpoint when API support is not available', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
getAdjustedInterval: (_: TimeRange) =>
|
||||
getRangeSnapInterval(PrometheusCacheLevel.None, getMockQuantizedTimeRangeParams()),
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesLabels = languageProvider.getSeriesLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
const labelName = 'job';
|
||||
const labelValue = 'grafana';
|
||||
getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [
|
||||
{
|
||||
name: labelName,
|
||||
value: labelValue,
|
||||
op: '=',
|
||||
},
|
||||
] as Label[]);
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': '{job="grafana"}',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call labels endpoint with quantized time parameters when cache level is set', () => {
|
||||
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Low);
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
hasLabelsMatchAPISupport: () => true,
|
||||
cacheLevel: PrometheusCacheLevel.Low,
|
||||
getAdjustedInterval: (_: TimeRange) =>
|
||||
getRangeSnapInterval(PrometheusCacheLevel.Low, getMockQuantizedTimeRangeParams()),
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesLabels = languageProvider.getSeriesLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
const labelName = 'job';
|
||||
const labelValue = 'grafana';
|
||||
getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [
|
||||
{
|
||||
name: labelName,
|
||||
value: labelValue,
|
||||
op: '=',
|
||||
},
|
||||
] as Label[]);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy).toHaveBeenCalledWith(
|
||||
'/api/v1/labels',
|
||||
{
|
||||
end: (
|
||||
dateTime(fromPrometheusTime * 1000)
|
||||
.add(timeSnapMinutes, 'minute')
|
||||
.startOf('minute')
|
||||
.valueOf() / 1000
|
||||
).toString(),
|
||||
'match[]': '{job="grafana"}',
|
||||
start: (
|
||||
dateTime(toPrometheusTime * 1000)
|
||||
.startOf('minute')
|
||||
.valueOf() / 1000
|
||||
).toString(),
|
||||
},
|
||||
{ headers: { 'X-Grafana-Cache': `private, max-age=${timeSnapMinutes * 60}` } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeriesValues', () => {
|
||||
it('should call series endpoint when labels match API is not supported', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesValues = languageProvider.getSeriesValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
getSeriesValues(timeRange, 'job', '{job="grafana"}');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': '{job="grafana"}',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call label values endpoint when labels match API is supported', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
hasLabelsMatchAPISupport: () => true,
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesValues = languageProvider.getSeriesValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
const labelName = 'job';
|
||||
const labelValue = 'grafana';
|
||||
getSeriesValues(timeRange, labelName, `{${labelName}="${labelValue}"}`);
|
||||
|
||||
verifyRequestParams(requestSpy, `/api/v1/label/${labelName}/values`, {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': `{${labelName}="${labelValue}"}`,
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly interpolate template variables in queries', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'interpolated-'),
|
||||
} as PrometheusDatasource);
|
||||
const getSeriesValues = languageProvider.getSeriesValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
getSeriesValues(timeRange, 'job', '{instance="$instance", job="grafana"}');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': '{instance="interpolated-instance", job="grafana"}',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSeries', () => {
|
||||
it('should use match[] parameter in request', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
await languageProvider.start(timeRange);
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await languageProvider.fetchSeries(timeRange, '{job="grafana"}');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': '{job="grafana"}',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSeriesLabels', () => {
|
||||
it('should interpolate variables in series queries', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'interpolated-'),
|
||||
} as PrometheusDatasource);
|
||||
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchSeriesLabels(getMockTimeRange(), '$metric', undefined, DEFAULT_SERIES_LIMIT);
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': 'interpolated-metric',
|
||||
start: fromPrometheusTimeString,
|
||||
limit: DEFAULT_SERIES_LIMIT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include limit parameter when "none" is specified', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
} as PrometheusDatasource);
|
||||
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchSeriesLabels(getMockTimeRange(), 'metric-with-limit', undefined, 'none');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/series', {
|
||||
end: toPrometheusTimeString,
|
||||
'match[]': 'metric-with-limit',
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabels API', () => {
|
||||
const tr = getMockTimeRange();
|
||||
|
||||
const getParams = (requestSpy: ReturnType<typeof jest.spyOn>) => {
|
||||
return requestSpy.mock.calls[0][1]?.toString() ?? 'undefined';
|
||||
};
|
||||
|
||||
describe('with POST method', () => {
|
||||
let languageProvider: PrometheusLanguageProviderInterface;
|
||||
|
||||
beforeEach(() => {
|
||||
languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
httpMethod: 'POST',
|
||||
} as PrometheusDatasource);
|
||||
});
|
||||
|
||||
it('should send single metric to request', async () => {
|
||||
const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
const params = getParams(requestSpy);
|
||||
expect(params).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket'));
|
||||
});
|
||||
|
||||
it('should extract metrics from complex PromQL expressions', async () => {
|
||||
const mockQueries: PromQuery[] = [
|
||||
{
|
||||
refId: 'C',
|
||||
expr: 'histogram_quantile(0.95, sum(rate(go_gc_pauses_seconds_bucket[$__rate_interval])) by (le))',
|
||||
},
|
||||
];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
const params = getParams(requestSpy);
|
||||
expect(params).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket'));
|
||||
});
|
||||
|
||||
it('should combine metrics from multiple queries', async () => {
|
||||
const mockQueries: PromQuery[] = [
|
||||
{ refId: 'B', expr: 'process_cpu_seconds_total' },
|
||||
{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' },
|
||||
];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
const params = getParams(requestSpy);
|
||||
expect(params).toMatch(encodeURI('match[]=process_cpu_seconds_total&match[]=go_gc_pauses_seconds_bucket'));
|
||||
});
|
||||
|
||||
it('should extract multiple metrics from binary operations', async () => {
|
||||
const mockQueries: PromQuery[] = [
|
||||
{ refId: 'B', expr: 'process_cpu_seconds_total + go_gc_pauses_seconds_bucket' },
|
||||
];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
const params = getParams(requestSpy);
|
||||
expect(params).toMatch(encodeURI('match[]=process_cpu_seconds_total&match[]=go_gc_pauses_seconds_bucket'));
|
||||
});
|
||||
|
||||
it('should set and return labelKeys from API response', async () => {
|
||||
const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['foo', 'bar']);
|
||||
|
||||
const keys = await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(languageProvider.labelKeys).toEqual(['bar', 'foo']); // Sorted order
|
||||
expect(keys).toEqual(['bar', 'foo']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with GET method', () => {
|
||||
let languageProvider: PrometheusLanguageProviderInterface;
|
||||
|
||||
beforeEach(() => {
|
||||
languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
httpMethod: 'GET',
|
||||
} as PrometheusDatasource);
|
||||
});
|
||||
|
||||
it('should send query metrics in URL for GET requests', async () => {
|
||||
const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0]).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket'));
|
||||
});
|
||||
|
||||
it('should handle empty queries correctly', async () => {
|
||||
const mockQueries: PromQuery[] = [{ refId: 'A', expr: '' }];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
await fetchLabel(tr, mockQueries);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0].indexOf('match[]')).toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Label value handling', () => {
|
||||
describe('fetchLabelValues', () => {
|
||||
it('should interpolate variables in labels', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'interpolated_'),
|
||||
} as PrometheusDatasource);
|
||||
const fetchLabelValues = languageProvider.fetchLabelValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchLabelValues(getMockTimeRange(), '$job');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/label/interpolated_job/values', {
|
||||
end: toPrometheusTimeString,
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly encode UTF-8 labels', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'http.status:sum'),
|
||||
} as PrometheusDatasource);
|
||||
const fetchLabelValues = languageProvider.fetchLabelValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchLabelValues(getMockTimeRange(), '"http.status:sum"');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/label/U__http_2e_status:sum/values', {
|
||||
end: toPrometheusTimeString,
|
||||
start: fromPrometheusTimeString,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle special characters safely in label values', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'value with spaces & special chars'),
|
||||
} as PrometheusDatasource);
|
||||
const fetchLabelValues = languageProvider.fetchLabelValues;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchLabelValues(getMockTimeRange(), '$job');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0]).not.toContain(' ');
|
||||
expect(requestSpy.mock.calls[0][0]).not.toContain('&');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSeriesValuesWithMatch', () => {
|
||||
it('should handle UTF-8 encoding for special label names', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => string.replace(/\$/g, 'http.status:sum'),
|
||||
} as PrometheusDatasource);
|
||||
const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchSeriesValuesWithMatch(getMockTimeRange(), '"http.status:sum"', '{__name__="a_utf8_http_requests_total"}');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/label/U__http_2e_status:sum/values', {
|
||||
end: toPrometheusTimeString,
|
||||
start: fromPrometheusTimeString,
|
||||
'match[]': '{__name__="a_utf8_http_requests_total"}',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not encode standard Prometheus label names', () => {
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
} as PrometheusDatasource);
|
||||
const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
|
||||
fetchSeriesValuesWithMatch(getMockTimeRange(), '"http_status_sum"', '{__name__="a_utf8_http_requests_total"}');
|
||||
|
||||
verifyRequestParams(requestSpy, '/api/v1/label/http_status_sum/values', {
|
||||
end: toPrometheusTimeString,
|
||||
start: fromPrometheusTimeString,
|
||||
'match[]': '{__name__="a_utf8_http_requests_total"}',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSuggestions', () => {
|
||||
it('should send POST request with correct parameters', async () => {
|
||||
const timeRange = getMockTimeRange();
|
||||
const mockQueries: PromQuery[] = [{ refId: 'A', expr: 'metric1' }];
|
||||
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => `interpolated_${string}`,
|
||||
getIntervalVars: () => ({ __interval: '1m' }),
|
||||
getRangeScopedVars: () => ({ __range: { text: '1h', value: '1h' } }),
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['suggestion1', 'suggestion2']);
|
||||
|
||||
// Simplifying the test by not passing complex scope objects that require more type definitions
|
||||
const result = await languageProvider.fetchSuggestions(
|
||||
timeRange,
|
||||
mockQueries,
|
||||
undefined, // omitting scopes parameter
|
||||
[{ key: 'instance', operator: '=', value: 'localhost' }],
|
||||
'metric',
|
||||
100
|
||||
);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0]).toBe('/suggestions');
|
||||
|
||||
// Check method and content type
|
||||
expect(requestSpy.mock.calls[0][2]).toMatchObject({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
// Check query parameters
|
||||
expect(requestSpy.mock.calls[0][1]).toMatchObject({
|
||||
labelName: 'metric',
|
||||
limit: 100,
|
||||
queries: ['interpolated_metric1'],
|
||||
});
|
||||
|
||||
expect(result).toEqual(['suggestion1', 'suggestion2']);
|
||||
});
|
||||
|
||||
it('should use default time range if not provided', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
|
||||
|
||||
await languageProvider.fetchSuggestions(undefined, [], [], [], 'test');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
// Default time range should be used
|
||||
expect(requestSpy.mock.calls[0][1]).toHaveProperty('start');
|
||||
expect(requestSpy.mock.calls[0][1]).toHaveProperty('end');
|
||||
});
|
||||
|
||||
it('should handle empty response gracefully', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
jest.spyOn(languageProvider, 'request').mockResolvedValue(null);
|
||||
|
||||
const result = await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should include cache headers when cacheLevel is set', async () => {
|
||||
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
cacheLevel: PrometheusCacheLevel.Medium,
|
||||
} as PrometheusDatasource);
|
||||
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
|
||||
|
||||
await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][2]?.headers).toHaveProperty('X-Grafana-Cache');
|
||||
expect(requestSpy.mock.calls[0][2]?.headers?.['X-Grafana-Cache']).toContain(
|
||||
`private, max-age=${timeSnapMinutes * 60}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query transformation', () => {
|
||||
describe('importFromAbstractQuery', () => {
|
||||
it('should handle empty queries', async () => {
|
||||
const result = importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
|
||||
expect(result).toEqual({ refId: 'bar', expr: '', range: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportToAbstractQuery', () => {
|
||||
it('should extract labels and metric name from PromQL', async () => {
|
||||
const abstractQuery = exportToAbstractQuery({
|
||||
refId: 'bar',
|
||||
expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
|
||||
instant: true,
|
||||
range: false,
|
||||
});
|
||||
|
||||
expect(abstractQuery).toMatchObject({
|
||||
refId: 'bar',
|
||||
labelMatchers: [
|
||||
{ name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
|
||||
{ name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
|
||||
{ name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
|
||||
{ name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
|
||||
{ name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PrometheusLanguageProvider with feature toggle', () => {
|
||||
describe('PrometheusLanguageProvider', () => {
|
||||
const defaultDatasource: PrometheusDatasource = {
|
||||
metadataRequest: () => ({ data: { data: [] } }),
|
||||
getTimeRangeParams: getTimeRangeParams,
|
||||
@@ -712,7 +110,6 @@ describe('PrometheusLanguageProvider with feature toggle', () => {
|
||||
expect(resourceClientStartSpy).toHaveBeenCalled();
|
||||
expect(queryMetadataSpy).toHaveBeenCalled();
|
||||
expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata);
|
||||
expect(provider.metricsMetadata).toEqual(mockMetadata); // Check backward compatibility
|
||||
});
|
||||
|
||||
it('should call queryMetricsMetadata with datasource seriesLimit during start', async () => {
|
||||
@@ -755,6 +152,7 @@ describe('PrometheusLanguageProvider with feature toggle', () => {
|
||||
const customLimit = 1000;
|
||||
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata);
|
||||
|
||||
// TODO spy on /api/v1/metadata endpoint or fetch
|
||||
const result = await provider.queryMetricsMetadata(customLimit);
|
||||
|
||||
expect(queryMetadataSpy).toHaveBeenCalledWith(customLimit);
|
||||
@@ -1034,4 +432,118 @@ describe('PrometheusLanguageProvider with feature toggle', () => {
|
||||
expect(result).toEqual(['__name__=~"go_cpu_classes_idle_cpu_seconds_total"']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSuggestions', () => {
|
||||
it('should send POST request with correct parameters', async () => {
|
||||
const timeRange = getMockTimeRange();
|
||||
const mockQueries: PromQuery[] = [{ refId: 'A', expr: 'metric1' }];
|
||||
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
interpolateString: (string: string) => `interpolated_${string}`,
|
||||
getIntervalVars: () => ({ __interval: '1m' }),
|
||||
getRangeScopedVars: () => ({ __range: { text: '1h', value: '1h' } }),
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['suggestion1', 'suggestion2']);
|
||||
|
||||
// Simplifying the test by not passing complex scope objects that require more type definitions
|
||||
const result = await languageProvider.fetchSuggestions(
|
||||
timeRange,
|
||||
mockQueries,
|
||||
undefined, // omitting scopes parameter
|
||||
[{ key: 'instance', operator: '=', value: 'localhost' }],
|
||||
'metric',
|
||||
100
|
||||
);
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0]).toBe('/suggestions');
|
||||
|
||||
// Check method and content type
|
||||
expect(requestSpy.mock.calls[0][2]).toMatchObject({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
// Check query parameters
|
||||
expect(requestSpy.mock.calls[0][1]).toMatchObject({
|
||||
labelName: 'metric',
|
||||
limit: 100,
|
||||
queries: ['interpolated_metric1'],
|
||||
});
|
||||
|
||||
expect(result).toEqual(['suggestion1', 'suggestion2']);
|
||||
});
|
||||
|
||||
it('should use default time range if not provided', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
|
||||
|
||||
await languageProvider.fetchSuggestions(undefined, [], [], [], 'test');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
// Default time range should be used
|
||||
expect(requestSpy.mock.calls[0][1]).toHaveProperty('start');
|
||||
expect(requestSpy.mock.calls[0][1]).toHaveProperty('end');
|
||||
});
|
||||
|
||||
it('should handle empty response gracefully', async () => {
|
||||
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
|
||||
jest.spyOn(languageProvider, 'request').mockResolvedValue(null);
|
||||
|
||||
const result = await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should include cache headers when cacheLevel is set', async () => {
|
||||
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
|
||||
const languageProvider = new PrometheusLanguageProvider({
|
||||
...defaultDatasource,
|
||||
cacheLevel: PrometheusCacheLevel.Medium,
|
||||
} as PrometheusDatasource);
|
||||
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
|
||||
|
||||
await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][2]?.headers).toHaveProperty('X-Grafana-Cache');
|
||||
expect(requestSpy.mock.calls[0][2]?.headers?.['X-Grafana-Cache']).toContain(
|
||||
`private, max-age=${timeSnapMinutes * 60}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query transformation', () => {
|
||||
describe('importFromAbstractQuery', () => {
|
||||
it('should handle empty queries', async () => {
|
||||
const result = importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
|
||||
expect(result).toEqual({ refId: 'bar', expr: '', range: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportToAbstractQuery', () => {
|
||||
it('should extract labels and metric name from PromQL', async () => {
|
||||
const abstractQuery = exportToAbstractQuery({
|
||||
refId: 'bar',
|
||||
expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
|
||||
instant: true,
|
||||
range: false,
|
||||
});
|
||||
|
||||
expect(abstractQuery).toMatchObject({
|
||||
refId: 'bar',
|
||||
labelMatchers: [
|
||||
{ name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
|
||||
{ name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
|
||||
{ name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
|
||||
{ name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
|
||||
{ name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.ts
|
||||
import { once } from 'lodash';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import {
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
AbstractQuery,
|
||||
AdHocVariableFilter,
|
||||
getDefaultTimeRange,
|
||||
LanguageProvider,
|
||||
Scope,
|
||||
scopeFilterOperatorMap,
|
||||
ScopeSpecFilter,
|
||||
@@ -17,34 +15,12 @@ import {
|
||||
import { BackendSrvRequest } from '@grafana/runtime';
|
||||
|
||||
import { buildCacheHeaders, getDaysToCacheMetadata, getDefaultCacheHeaders } from './caching';
|
||||
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
|
||||
import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, REMOVE_SERIES_LIMIT } from './constants';
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import {
|
||||
extractLabelMatchers,
|
||||
fixSummariesMetadata,
|
||||
processHistogramMetrics,
|
||||
processLabels,
|
||||
removeQuotesIfExist,
|
||||
toPromLikeQuery,
|
||||
} from './language_utils';
|
||||
import { extractLabelMatchers, fixSummariesMetadata, toPromLikeQuery } from './language_utils';
|
||||
import { promqlGrammar } from './promql';
|
||||
import { buildVisualQueryFromString } from './querybuilder/parsing';
|
||||
import { LabelsApiClient, ResourceApiClient, SeriesApiClient } from './resource_clients';
|
||||
import { PromMetricsMetadata, PromQuery } from './types';
|
||||
import { escapeForUtf8Support, isValidLegacyName } from './utf8_support';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
|
||||
/**
|
||||
* Prometheus API endpoints for fetching resources
|
||||
*/
|
||||
const API_V1 = {
|
||||
METADATA: '/api/v1/metadata',
|
||||
SERIES: '/api/v1/series',
|
||||
LABELS: '/api/v1/labels',
|
||||
LABELS_VALUES: (labelKey: string) => `/api/v1/label/${labelKey}/values`,
|
||||
};
|
||||
|
||||
interface PrometheusBaseLanguageProvider {
|
||||
datasource: PrometheusDatasource;
|
||||
@@ -53,7 +29,7 @@ interface PrometheusBaseLanguageProvider {
|
||||
* When no timeRange provided, we will use the default time range (now/now-6h)
|
||||
* @param timeRange
|
||||
*/
|
||||
start: (timeRange?: TimeRange) => Promise<any[]>;
|
||||
start: (timeRange?: TimeRange) => Promise<unknown[]>;
|
||||
|
||||
request: (url: string, params?: any, options?: Partial<BackendSrvRequest>) => Promise<any>;
|
||||
|
||||
@@ -68,455 +44,6 @@ interface PrometheusBaseLanguageProvider {
|
||||
) => Promise<string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This interface is deprecated and will be removed.
|
||||
*/
|
||||
interface PrometheusLegacyLanguageProvider {
|
||||
/**
|
||||
* @deprecated Use retrieveHistogramMetrics() method instead
|
||||
*/
|
||||
histogramMetrics: string[];
|
||||
/**
|
||||
* @deprecated Use retrieveMetrics() method instead
|
||||
*/
|
||||
metrics: string[];
|
||||
/**
|
||||
* @deprecated Use retrieveMetricsMetadata() method instead
|
||||
*/
|
||||
metricsMetadata?: PromMetricsMetadata;
|
||||
/**
|
||||
* @deprecated Use retrieveLabelKeys() method instead
|
||||
*/
|
||||
labelKeys: string[];
|
||||
|
||||
/**
|
||||
* @deprecated Use queryMetricsMetadata() method instead.
|
||||
*/
|
||||
loadMetricsMetadata: () => void;
|
||||
/**
|
||||
* @deprecated Use retrieveMetricsMetadata() method instead
|
||||
*/
|
||||
getLabelKeys: () => string[];
|
||||
/**
|
||||
* @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions
|
||||
*/
|
||||
getSeries: (timeRange: TimeRange, selector: string, withName?: boolean) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchLabelValues: (range: TimeRange, key: string, limit?: string | number) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
getLabelValues: (range: TimeRange, key: string) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions
|
||||
*/
|
||||
fetchLabels: (timeRange: TimeRange, queries?: PromQuery[], limit?: string) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
getSeriesValues: (timeRange: TimeRange, labelName: string, selector: string) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchSeriesValuesWithMatch: (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
match?: string,
|
||||
requestId?: string,
|
||||
withLimit?: string | number
|
||||
) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
getSeriesLabels: (timeRange: TimeRange, selector: string, otherLabels: Label[]) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchLabelsWithMatch: (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchSeriesLabels: (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchSeriesLabelsMatch: (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions
|
||||
*/
|
||||
fetchSeries: (timeRange: TimeRange, match: string) => Promise<Array<Record<string, string>>>;
|
||||
/**
|
||||
* @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions
|
||||
*/
|
||||
fetchDefaultSeries: (timeRange: TimeRange) => Promise<{}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Old implementation of prometheus language provider.
|
||||
* @deprecated Use PrometheusLanguageProviderInterface and PrometheusLanguageProvider class instead.
|
||||
*/
|
||||
export default class PromQlLanguageProvider extends LanguageProvider implements PrometheusLegacyLanguageProvider {
|
||||
declare startTask: Promise<any>;
|
||||
declare labelFetchTs: number;
|
||||
|
||||
datasource: PrometheusDatasource;
|
||||
|
||||
histogramMetrics: string[];
|
||||
metrics: string[];
|
||||
metricsMetadata?: PromMetricsMetadata;
|
||||
labelKeys: string[] = [];
|
||||
|
||||
constructor(datasource: PrometheusDatasource, initialValues?: Partial<PromQlLanguageProvider>) {
|
||||
super();
|
||||
|
||||
this.datasource = datasource;
|
||||
this.histogramMetrics = [];
|
||||
this.metrics = [];
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
|
||||
request = async (url: string, params = {}, options?: Partial<BackendSrvRequest>) => {
|
||||
try {
|
||||
const res = await this.datasource.metadataRequest(url, params, options);
|
||||
return res.data.data;
|
||||
} catch (error) {
|
||||
if (!isCancelledError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Overridden by PrometheusLanguageProvider
|
||||
*/
|
||||
start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise<any[]> => {
|
||||
if (this.datasource.lookupsDisabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || [];
|
||||
this.histogramMetrics = processHistogramMetrics(this.metrics).sort();
|
||||
return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]);
|
||||
};
|
||||
|
||||
async loadMetricsMetadata() {
|
||||
const secondsInDay = 86400;
|
||||
const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay);
|
||||
this.metricsMetadata = fixSummariesMetadata(
|
||||
await this.request(
|
||||
API_V1.METADATA,
|
||||
{},
|
||||
{
|
||||
showErrorAlert: false,
|
||||
...headers,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getLabelKeys(): string[] {
|
||||
return this.labelKeys;
|
||||
}
|
||||
|
||||
async getSeries(timeRange: TimeRange, selector: string, withName?: boolean): Promise<Record<string, string[]>> {
|
||||
if (this.datasource.lookupsDisabled) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
return await this.fetchDefaultSeries(timeRange);
|
||||
} else {
|
||||
return await this.fetchSeriesLabels(timeRange, selector, withName, REMOVE_SERIES_LIMIT);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: better error handling
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
fetchLabelValues = async (range: TimeRange, key: string, limit?: string | number): Promise<string[]> => {
|
||||
const params = { ...this.datasource.getAdjustedInterval(range), ...(limit ? { limit } : {}) };
|
||||
const interpolatedName = this.datasource.interpolateString(key);
|
||||
const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName));
|
||||
const value = await this.request(
|
||||
API_V1.LABELS_VALUES(interpolatedAndEscapedName),
|
||||
params,
|
||||
getDefaultCacheHeaders(this.datasource.cacheLevel)
|
||||
);
|
||||
return value ?? [];
|
||||
};
|
||||
|
||||
async getLabelValues(range: TimeRange, key: string): Promise<string[]> {
|
||||
return await this.fetchLabelValues(range, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all label keys
|
||||
*/
|
||||
fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[], limit?: string): Promise<string[]> => {
|
||||
let url = API_V1.LABELS;
|
||||
const timeParams = this.datasource.getAdjustedInterval(timeRange);
|
||||
this.labelFetchTs = Date.now().valueOf();
|
||||
|
||||
const searchParams = new URLSearchParams({ ...timeParams, ...(limit ? { limit } : {}) });
|
||||
queries?.forEach((q) => {
|
||||
const visualQuery = buildVisualQueryFromString(q.expr);
|
||||
if (visualQuery.query.metric !== '') {
|
||||
const isUtf8Metric = !isValidLegacyName(visualQuery.query.metric);
|
||||
searchParams.append('match[]', isUtf8Metric ? `{"${visualQuery.query.metric}"}` : visualQuery.query.metric);
|
||||
if (visualQuery.query.binaryQueries) {
|
||||
visualQuery.query.binaryQueries.forEach((bq) => {
|
||||
searchParams.append('match[]', isUtf8Metric ? `{"${bq.query.metric}"}` : bq.query.metric);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.datasource.httpMethod === 'GET') {
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
const res = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
if (Array.isArray(res)) {
|
||||
this.labelKeys = res.slice().sort();
|
||||
return [...this.labelKeys];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets series values
|
||||
* Function to replace old getSeries calls in a way that will provide faster endpoints
|
||||
* for new prometheus instances, while maintaining backward compatability
|
||||
*/
|
||||
getSeriesValues = async (timeRange: TimeRange, labelName: string, selector: string): Promise<string[]> => {
|
||||
if (!this.datasource.hasLabelsMatchAPISupport()) {
|
||||
const data = await this.getSeries(timeRange, selector);
|
||||
return data[removeQuotesIfExist(labelName)] ?? [];
|
||||
}
|
||||
return await this.fetchSeriesValuesWithMatch(timeRange, labelName, selector);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all values for a label, with optional match[]
|
||||
*/
|
||||
fetchSeriesValuesWithMatch = async (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
match?: string,
|
||||
requestId?: string,
|
||||
withLimit?: string | number
|
||||
): Promise<string[]> => {
|
||||
const interpolatedName = name ? this.datasource.interpolateString(name) : null;
|
||||
const interpolatedMatch = match ? this.datasource.interpolateString(match) : null;
|
||||
const range = this.datasource.getAdjustedInterval(timeRange);
|
||||
const urlParams = {
|
||||
...range,
|
||||
...(interpolatedMatch && { 'match[]': interpolatedMatch }),
|
||||
...(withLimit ? { limit: withLimit } : {}),
|
||||
};
|
||||
let requestOptions: Partial<BackendSrvRequest> | undefined = {
|
||||
...getDefaultCacheHeaders(this.datasource.cacheLevel),
|
||||
...(requestId && { requestId }),
|
||||
};
|
||||
|
||||
if (!Object.keys(requestOptions).length) {
|
||||
requestOptions = undefined;
|
||||
}
|
||||
|
||||
const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName ?? ''));
|
||||
|
||||
const value = await this.request(API_V1.LABELS_VALUES(interpolatedAndEscapedName), urlParams, requestOptions);
|
||||
return value ?? [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets series labels
|
||||
* Function to replace old getSeries calls in a way that will provide faster endpoints for new prometheus instances,
|
||||
* while maintaining backward compatability. The old API call got the labels and the values in a single query,
|
||||
* but with the new query we need two calls, one to get the labels, and another to get the values.
|
||||
*/
|
||||
getSeriesLabels = async (timeRange: TimeRange, selector: string, otherLabels: Label[]): Promise<string[]> => {
|
||||
let possibleLabelNames, data: Record<string, string[]>;
|
||||
|
||||
if (!this.datasource.hasLabelsMatchAPISupport()) {
|
||||
data = await this.getSeries(timeRange, selector);
|
||||
possibleLabelNames = Object.keys(data); // all names from prometheus
|
||||
} else {
|
||||
// Exclude __name__ from output
|
||||
otherLabels.push({ name: '__name__', value: '', op: '!=' });
|
||||
data = await this.fetchSeriesLabelsMatch(timeRange, selector);
|
||||
possibleLabelNames = Object.keys(data);
|
||||
}
|
||||
|
||||
const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query
|
||||
return possibleLabelNames.filter((l) => !usedLabelNames.has(l));
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch labels using the best endpoint that datasource supports.
|
||||
* This is cached by its args but also by the global timeRange currently selected as they can change over requested time.
|
||||
*/
|
||||
fetchLabelsWithMatch = async (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
if (this.datasource.hasLabelsMatchAPISupport()) {
|
||||
return this.fetchSeriesLabelsMatch(timeRange, name, withLimit);
|
||||
} else {
|
||||
return this.fetchSeriesLabels(timeRange, name, withName, REMOVE_SERIES_LIMIT);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch labels for a series using /series endpoint. This is cached by its args but also by the global timeRange currently selected as
|
||||
* they can change over requested time.
|
||||
*/
|
||||
fetchSeriesLabels = async (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
const interpolatedName = this.datasource.interpolateString(name);
|
||||
const range = this.datasource.getAdjustedInterval(timeRange);
|
||||
let urlParams = {
|
||||
...range,
|
||||
'match[]': interpolatedName,
|
||||
...(withLimit !== 'none' ? { limit: withLimit ?? DEFAULT_SERIES_LIMIT } : {}),
|
||||
};
|
||||
|
||||
const data = await this.request(API_V1.SERIES, urlParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
const { values } = processLabels(data, withName);
|
||||
return values;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch labels for a series using /labels endpoint. This is cached by its args but also by the global timeRange currently selected as
|
||||
* they can change over requested time.
|
||||
*/
|
||||
fetchSeriesLabelsMatch = async (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
const interpolatedName = this.datasource.interpolateString(name);
|
||||
const range = this.datasource.getAdjustedInterval(timeRange);
|
||||
const urlParams = {
|
||||
...range,
|
||||
'match[]': interpolatedName,
|
||||
...(withLimit ? { limit: withLimit } : {}),
|
||||
};
|
||||
|
||||
const data: string[] = await this.request(
|
||||
API_V1.LABELS,
|
||||
urlParams,
|
||||
getDefaultCacheHeaders(this.datasource.cacheLevel)
|
||||
);
|
||||
// Convert string array to Record<string , []>
|
||||
return data.reduce((ac, a) => ({ ...ac, [a]: '' }), {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels.
|
||||
*/
|
||||
fetchSeries = async (timeRange: TimeRange, match: string): Promise<Array<Record<string, string>>> => {
|
||||
const range = this.datasource.getTimeRangeParams(timeRange);
|
||||
const params = { ...range, 'match[]': match };
|
||||
return await this.request(API_V1.SERIES, params, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch this only one as we assume this won't change over time. This is cached differently from fetchSeriesLabels
|
||||
* because we can cache more aggressively here and also we do not want to invalidate this cache the same way as in
|
||||
* fetchSeriesLabels.
|
||||
*/
|
||||
fetchDefaultSeries = once(async (timeRange: TimeRange) => {
|
||||
const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(timeRange, key)));
|
||||
return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch labels or values for a label based on the queries, scopes, filters and time range
|
||||
*/
|
||||
fetchSuggestions = async (
|
||||
timeRange?: TimeRange,
|
||||
queries?: PromQuery[],
|
||||
scopes?: Scope[],
|
||||
adhocFilters?: AdHocVariableFilter[],
|
||||
labelName?: string,
|
||||
limit?: number,
|
||||
requestId?: string
|
||||
): Promise<string[]> => {
|
||||
if (!timeRange) {
|
||||
timeRange = getDefaultTimeRange();
|
||||
}
|
||||
|
||||
const url = '/suggestions';
|
||||
const timeParams = this.datasource.getAdjustedInterval(timeRange);
|
||||
const value = await this.request(
|
||||
url,
|
||||
{
|
||||
labelName,
|
||||
queries: queries?.map((q) =>
|
||||
this.datasource.interpolateString(q.expr, {
|
||||
...this.datasource.getIntervalVars(),
|
||||
...this.datasource.getRangeScopedVars(timeRange),
|
||||
})
|
||||
),
|
||||
scopes: scopes?.reduce<ScopeSpecFilter[]>((acc, scope) => {
|
||||
if (scope.spec.filters) {
|
||||
acc.push(...scope.spec.filters);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
adhocFilters: adhocFilters?.map((filter) => ({
|
||||
key: filter.key,
|
||||
operator: scopeFilterOperatorMap[filter.operator],
|
||||
value: filter.value,
|
||||
values: filter.values,
|
||||
})),
|
||||
limit,
|
||||
...timeParams,
|
||||
},
|
||||
{
|
||||
...(requestId && { requestId }),
|
||||
headers: {
|
||||
...getDefaultCacheHeaders(this.datasource.cacheLevel)?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
|
||||
return value ?? [];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern implementation of the Prometheus language provider that abstracts API endpoint selection.
|
||||
*
|
||||
@@ -531,9 +58,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
* @see LabelsApiClient For modern Prometheus versions using the labels API
|
||||
* @see SeriesApiClient For legacy Prometheus versions using the series API
|
||||
*/
|
||||
export interface PrometheusLanguageProviderInterface
|
||||
extends PrometheusBaseLanguageProvider,
|
||||
PrometheusLegacyLanguageProvider {
|
||||
export interface PrometheusLanguageProviderInterface extends PrometheusBaseLanguageProvider {
|
||||
/**
|
||||
* Initializes the language provider by fetching metrics, label keys, and metrics metadata using Resource Clients.
|
||||
* All calls use the limit parameter from datasource configuration (default: 40,000 if not set).
|
||||
@@ -541,7 +66,7 @@ export interface PrometheusLanguageProviderInterface
|
||||
* For backward compatibility, it calls _backwardCompatibleStart.
|
||||
* Some places still rely on deprecated fields. Until we replace them, we need _backwardCompatibleStart method.
|
||||
*/
|
||||
start: (timeRange?: TimeRange) => Promise<any[]>;
|
||||
start: (timeRange?: TimeRange) => Promise<unknown[]>;
|
||||
|
||||
/**
|
||||
* Returns already cached metrics metadata including type and help information.
|
||||
@@ -591,14 +116,29 @@ export interface PrometheusLanguageProviderInterface
|
||||
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise<string[]>;
|
||||
}
|
||||
|
||||
export class PrometheusLanguageProvider extends PromQlLanguageProvider implements PrometheusLanguageProviderInterface {
|
||||
export class PrometheusLanguageProvider implements PrometheusLanguageProviderInterface {
|
||||
public datasource: PrometheusDatasource;
|
||||
|
||||
private _metricsMetadata?: PromMetricsMetadata;
|
||||
private _resourceClient?: ResourceApiClient;
|
||||
|
||||
constructor(datasource: PrometheusDatasource) {
|
||||
super(datasource);
|
||||
this.datasource = datasource;
|
||||
}
|
||||
|
||||
request = async (url: string, params = {}, options?: Partial<BackendSrvRequest>) => {
|
||||
try {
|
||||
const res = await this.datasource.metadataRequest(url, params, options);
|
||||
return res.data.data;
|
||||
} catch (error) {
|
||||
if (!isCancelledError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazily initializes and returns the appropriate resource client based on Prometheus version.
|
||||
*
|
||||
@@ -624,24 +164,14 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement
|
||||
* Same start logic but it uses resource clients. Backward compatibility it calls _backwardCompatibleStart.
|
||||
* Some places still relies on deprecated fields. Until we replace them we need _backwardCompatibleStart method
|
||||
*/
|
||||
start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise<any[]> => {
|
||||
start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise<unknown[]> => {
|
||||
if (this.datasource.lookupsDisabled) {
|
||||
return [];
|
||||
}
|
||||
await Promise.all([this.resourceClient.start(timeRange), this.queryMetricsMetadata(this.datasource.seriesLimit)]);
|
||||
return this._backwardCompatibleStart();
|
||||
};
|
||||
|
||||
/**
|
||||
* This private method exists to make sure the old class will be functional until we remove it.
|
||||
* When we remove old class (PromQlLanguageProvider) we should remove this method too.
|
||||
*/
|
||||
private _backwardCompatibleStart = async () => {
|
||||
this.metricsMetadata = this.retrieveMetricsMetadata();
|
||||
this.metrics = this.retrieveMetrics();
|
||||
this.histogramMetrics = this.retrieveHistogramMetrics();
|
||||
this.labelKeys = this.retrieveLabelKeys();
|
||||
return [];
|
||||
return await Promise.all([
|
||||
this.resourceClient.start(timeRange),
|
||||
this.queryMetricsMetadata(this.datasource.seriesLimit),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -654,7 +184,7 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement
|
||||
const secondsInDay = 86400;
|
||||
const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay);
|
||||
const metadata = await this.request(
|
||||
API_V1.METADATA,
|
||||
`/api/v1/metadata`,
|
||||
{ limit: limit ?? this.datasource.seriesLimit },
|
||||
{
|
||||
showErrorAlert: false,
|
||||
@@ -776,6 +306,63 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement
|
||||
limit
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch labels or values for a label based on the queries, scopes, filters and time range
|
||||
*/
|
||||
fetchSuggestions = async (
|
||||
timeRange?: TimeRange,
|
||||
queries?: PromQuery[],
|
||||
scopes?: Scope[],
|
||||
adhocFilters?: AdHocVariableFilter[],
|
||||
labelName?: string,
|
||||
limit?: number,
|
||||
requestId?: string
|
||||
): Promise<string[]> => {
|
||||
if (!timeRange) {
|
||||
timeRange = getDefaultTimeRange();
|
||||
}
|
||||
|
||||
const url = '/suggestions';
|
||||
const timeParams = this.datasource.getAdjustedInterval(timeRange);
|
||||
const value = await this.request(
|
||||
url,
|
||||
{
|
||||
labelName,
|
||||
queries: queries?.map((q) =>
|
||||
this.datasource.interpolateString(q.expr, {
|
||||
...this.datasource.getIntervalVars(),
|
||||
...this.datasource.getRangeScopedVars(timeRange),
|
||||
})
|
||||
),
|
||||
scopes: scopes?.reduce<ScopeSpecFilter[]>((acc, scope) => {
|
||||
if (scope.spec.filters) {
|
||||
acc.push(...scope.spec.filters);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
adhocFilters: adhocFilters?.map((filter) => ({
|
||||
key: filter.key,
|
||||
operator: scopeFilterOperatorMap[filter.operator],
|
||||
value: filter.value,
|
||||
values: filter.values,
|
||||
})),
|
||||
limit,
|
||||
...timeParams,
|
||||
},
|
||||
{
|
||||
...(requestId && { requestId }),
|
||||
headers: {
|
||||
...getDefaultCacheHeaders(this.datasource.cacheLevel)?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
|
||||
return value ?? [];
|
||||
};
|
||||
}
|
||||
|
||||
export const importFromAbstractQuery = (labelBasedQuery: AbstractQuery): PromQuery => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
fixSummariesMetadata,
|
||||
getPrometheusTime,
|
||||
getRangeSnapInterval,
|
||||
processLabels,
|
||||
removeQuotesIfExist,
|
||||
toPromLikeQuery,
|
||||
truncateResult,
|
||||
@@ -507,57 +506,6 @@ describe('truncateResult', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('processLabels', () => {
|
||||
it('export abstract query to expr', () => {
|
||||
const labels: Array<{ [key: string]: string }> = [
|
||||
{ label1: 'value1' },
|
||||
{ label2: 'value2' },
|
||||
{ label3: 'value3' },
|
||||
{ label1: 'value1' },
|
||||
{ label1: 'value1b' },
|
||||
];
|
||||
|
||||
expect(processLabels(labels)).toEqual({
|
||||
keys: ['label1', 'label2', 'label3'],
|
||||
values: { label1: ['value1', 'value1b'], label2: ['value2'], label3: ['value3'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('dont wrap utf8 label values with quotes', () => {
|
||||
const labels: Array<{ [key: string]: string }> = [
|
||||
{ label1: 'value1' },
|
||||
{ label2: 'value2' },
|
||||
{ label3: 'value3 with space' },
|
||||
{ label4: 'value4.with.dot' },
|
||||
];
|
||||
|
||||
expect(processLabels(labels)).toEqual({
|
||||
keys: ['label1', 'label2', 'label3', 'label4'],
|
||||
values: {
|
||||
label1: ['value1'],
|
||||
label2: ['value2'],
|
||||
label3: [`value3 with space`],
|
||||
label4: [`value4.with.dot`],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dont wrap utf8 labels with quotes', () => {
|
||||
const labels: Array<{ [key: string]: string }> = [
|
||||
{ 'label1 with space': 'value1' },
|
||||
{ 'label2.with.dot': 'value2' },
|
||||
];
|
||||
|
||||
expect(processLabels(labels)).toEqual({
|
||||
keys: ['label1 with space', 'label2.with.dot'],
|
||||
values: {
|
||||
'label1 with space': ['value1'],
|
||||
'label2.with.dot': ['value2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeQuotesIfExist', () => {
|
||||
it('removes quotes from a string with double quotes', () => {
|
||||
const input = '"hello"';
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { getCacheDurationInMinutes } from './caching';
|
||||
import { SUGGESTIONS_LIMIT, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants';
|
||||
import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants';
|
||||
import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem, RecordingRuleIdentifier } from './types';
|
||||
|
||||
export const processHistogramMetrics = (metrics: string[]) => {
|
||||
@@ -31,38 +31,6 @@ export const processHistogramMetrics = (metrics: string[]) => {
|
||||
return [...resultSet];
|
||||
};
|
||||
|
||||
export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) {
|
||||
// For processing we are going to use sets as they have significantly better performance than arrays
|
||||
// After we process labels, we will convert sets to arrays and return object with label values in arrays
|
||||
const valueSet: { [key: string]: Set<string> } = {};
|
||||
labels.forEach((label) => {
|
||||
const { __name__, ...rest } = label;
|
||||
if (withName) {
|
||||
valueSet['__name__'] = valueSet['__name__'] || new Set();
|
||||
if (!valueSet['__name__'].has(__name__)) {
|
||||
valueSet['__name__'].add(__name__);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(rest).forEach((key) => {
|
||||
if (!valueSet[key]) {
|
||||
valueSet[key] = new Set();
|
||||
}
|
||||
if (!valueSet[key].has(rest[key])) {
|
||||
valueSet[key].add(rest[key]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// valueArray that we are going to return in the object
|
||||
const valueArray: { [key: string]: string[] } = {};
|
||||
limitSuggestions(Object.keys(valueSet)).forEach((key) => {
|
||||
valueArray[key] = limitSuggestions(Array.from(valueSet[key]));
|
||||
});
|
||||
|
||||
return { values: valueArray, keys: Object.keys(valueArray) };
|
||||
}
|
||||
|
||||
// This will capture 4 groups. Example label filter => {instance="10.4.11.4:9003"}
|
||||
// 1. label: instance
|
||||
// 2. operator: =
|
||||
@@ -276,10 +244,6 @@ function roundSecToNextMin(seconds: number, secondsToRound = 1): number {
|
||||
return Math.ceil(seconds / 60) - (Math.ceil(seconds / 60) % secondsToRound);
|
||||
}
|
||||
|
||||
function limitSuggestions(items: string[]) {
|
||||
return items.slice(0, SUGGESTIONS_LIMIT);
|
||||
}
|
||||
|
||||
const FromPromLikeMap: Record<string, AbstractLabelOperator> = {
|
||||
'=': AbstractLabelOperator.Equal,
|
||||
'!=': AbstractLabelOperator.NotEqual,
|
||||
|
||||
Reference in New Issue
Block a user