diff --git a/eslint-suppressions.json b/eslint-suppressions.json index d1c9af43a95..bc54f1c116e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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": { diff --git a/packages/grafana-prometheus/src/constants.ts b/packages/grafana-prometheus/src/constants.ts index 24bc613de60..053180f4a13 100644 --- a/packages/grafana-prometheus/src/constants.ts +++ b/packages/grafana-prometheus/src/constants.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'; diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index d0539d46591..f6950cc9fb7 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -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'; diff --git a/packages/grafana-prometheus/src/language_provider.mock.ts b/packages/grafana-prometheus/src/language_provider.mock.ts index 09e16e5a9af..adf97211cb6 100644 --- a/packages/grafana-prometheus/src/language_provider.mock.ts +++ b/packages/grafana-prometheus/src/language_provider.mock.ts @@ -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([]); diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index a3c32d7f881..6ddb6188117 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -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): 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) => { - 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' }, + ], + }); + }); + }); }); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 568acddba64..0daf12d7b7e 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -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; + start: (timeRange?: TimeRange) => Promise; request: (url: string, params?: any, options?: Partial) => Promise; @@ -68,455 +44,6 @@ interface PrometheusBaseLanguageProvider { ) => Promise; } -/** - * @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>; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - fetchLabelValues: (range: TimeRange, key: string, limit?: string | number) => Promise; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - getLabelValues: (range: TimeRange, key: string) => Promise; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - fetchLabels: (timeRange: TimeRange, queries?: PromQuery[], limit?: string) => Promise; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - getSeriesValues: (timeRange: TimeRange, labelName: string, selector: string) => Promise; - /** - * @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; - /** - * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings - */ - getSeriesLabels: (timeRange: TimeRange, selector: string, otherLabels: Label[]) => Promise; - /** - * @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>; - /** - * @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>; - /** - * @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>; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - fetchSeries: (timeRange: TimeRange, match: string) => Promise>>; - /** - * @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; - declare labelFetchTs: number; - - datasource: PrometheusDatasource; - - histogramMetrics: string[]; - metrics: string[]; - metricsMetadata?: PromMetricsMetadata; - labelKeys: string[] = []; - - constructor(datasource: PrometheusDatasource, initialValues?: Partial) { - super(); - - this.datasource = datasource; - this.histogramMetrics = []; - this.metrics = []; - - Object.assign(this, initialValues); - } - - request = async (url: string, params = {}, options?: Partial) => { - 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 => { - 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> { - 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 => { - 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 { - return await this.fetchLabelValues(range, key); - } - - /** - * Fetches all label keys - */ - fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[], limit?: string): Promise => { - 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 => { - 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 => { - 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 | 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 => { - let possibleLabelNames, data: Record; - - 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> => { - 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> => { - 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> => { - 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 - 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>> => { - 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 => { - 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((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; + start: (timeRange?: TimeRange) => Promise; /** * 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; } -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) => { + 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 => { + start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { 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 => { + 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((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 => { diff --git a/packages/grafana-prometheus/src/language_utils.test.ts b/packages/grafana-prometheus/src/language_utils.test.ts index 555634f898e..6e69b7844d7 100644 --- a/packages/grafana-prometheus/src/language_utils.test.ts +++ b/packages/grafana-prometheus/src/language_utils.test.ts @@ -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"'; diff --git a/packages/grafana-prometheus/src/language_utils.ts b/packages/grafana-prometheus/src/language_utils.ts index ee8090c9d44..38b5f364c42 100644 --- a/packages/grafana-prometheus/src/language_utils.ts +++ b/packages/grafana-prometheus/src/language_utils.ts @@ -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 } = {}; - 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 = { '=': AbstractLabelOperator.Equal, '!=': AbstractLabelOperator.NotEqual,