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:
ismail simsek
2025-12-10 21:07:18 +01:00
committed by GitHub
parent b3f98d4cc3
commit 30d3bb39c0
8 changed files with 214 additions and 1223 deletions
+1 -1
View File
@@ -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';
-1
View File
@@ -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,