diff --git a/.betterer.results b/.betterer.results index 0f0c8c76b2b..a3d021c4c4e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6406,9 +6406,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/plugins/datasource/prometheus/components/PromLink.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -6487,16 +6484,7 @@ exports[`better eslint`] = { "public/app/plugins/datasource/prometheus/datasource.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/prometheus/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6511,14 +6499,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"] + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "public/app/plugins/datasource/prometheus/language_provider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6547,7 +6528,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/plugins/datasource/prometheus/query_hints.ts:5381": [ @@ -6686,44 +6667,12 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/prometheus/result_transformer.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"], - [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], - [0, 0, 0, "Unexpected any. Specify a different type.", "23"], - [0, 0, 0, "Unexpected any. Specify a different type.", "24"], - [0, 0, 0, "Unexpected any. Specify a different type.", "25"], - [0, 0, 0, "Unexpected any. Specify a different type.", "26"], - [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Unexpected any. Specify a different type.", "28"], - [0, 0, 0, "Unexpected any. Specify a different type.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"], - [0, 0, 0, "Unexpected any. Specify a different type.", "31"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/prometheus/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/tempo/LokiSearch.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/public/app/plugins/datasource/prometheus/components/PromLink.test.tsx b/public/app/plugins/datasource/prometheus/components/PromLink.test.tsx deleted file mode 100644 index bb115283ada..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromLink.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { dateTime, PanelData, TimeRange } from '@grafana/data'; - -import { PrometheusDatasource } from '../datasource'; -import { PromQuery } from '../types'; - -import PromLink from './PromLink'; - -jest.mock('@grafana/data', () => ({ - ...jest.requireActual('@grafana/data'), - rangeUtil: { - intervalToSeconds: jest.fn(() => 15), - }, -})); - -const now = dateTime().valueOf(); -const intervalInSeconds = 60 * 5; -const endInput = encodeURIComponent(dateTime(now).add(5, 'hours').format('Y-MM-DD HH:mm')); - -const getPanelData = (panelDataOverrides?: Partial) => { - const panelData = { - request: { - scopedVars: [{ __interval: { text: '15s', value: '15s' } }], - targets: [ - { refId: 'A', datasource: 'prom1' }, - { refId: 'B', datasource: 'prom2' }, - ], - range: { - raw: {}, - to: dateTime(now), // "now" - from: dateTime(now - 1000 * intervalInSeconds), // 5 minutes ago from "now" - } as TimeRange, - }, - }; - - return Object.assign(panelData, panelDataOverrides) as PanelData; -}; - -const getDataSource = (datasourceOverrides?: Partial) => { - const datasource = { - createQuery: () => ({ expr: 'up', step: 15 }), - directUrl: 'prom1', - getRateIntervalScopedVariable: jest.fn(() => ({ __rate_interval: { text: '60s', value: '60s' } })), - }; - - return Object.assign(datasource, datasourceOverrides) as unknown as PrometheusDatasource; -}; - -const getDataSourceWithCustomQueryParameters = (datasourceOverrides?: Partial) => { - const datasource = { - getPrometheusTime: () => 1677870470, - createQuery: () => ({ expr: 'up', step: 20 }), - directUrl: 'prom3', - getRateIntervalScopedVariable: jest.fn(() => ({ __rate_interval: { text: '60s', value: '60s' } })), - customQueryParameters: new URLSearchParams('g0.foo=1'), - }; - - return Object.assign(datasource, datasourceOverrides) as unknown as PrometheusDatasource; -}; - -describe('PromLink', () => { - it('should show correct link for 1 component', async () => { - render( -
- -
- ); - expect(screen.getByText('Prometheus')).toHaveAttribute( - 'href', - `prom1/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0` - ); - }); - it('should show different link when there are 2 components with the same panel data', () => { - render( -
- - -
- ); - const promLinkButtons = screen.getAllByText('Prometheus'); - expect(promLinkButtons[0]).toHaveAttribute( - 'href', - `prom1/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0` - ); - expect(promLinkButtons[1]).toHaveAttribute( - 'href', - `prom2/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0` - ); - }); - it('should create sanitized link', async () => { - render( -
- -
- ); - expect(screen.getByText('Prometheus')).toHaveAttribute('href', 'about:blank'); - }); - it('should add custom query parameters when it is configured', async () => { - render( -
- -
- ); - expect(screen.getByText('Prometheus')).toHaveAttribute( - 'href', - `prom3/graph?g0.foo=1&g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=20&g0.tab=0` - ); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/components/PromLink.tsx b/public/app/plugins/datasource/prometheus/components/PromLink.tsx deleted file mode 100644 index 5b01ed63837..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromLink.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { map } from 'lodash'; -import React, { useEffect, useState, memo } from 'react'; - -import { DataQueryRequest, PanelData, ScopedVars, textUtil, rangeUtil } from '@grafana/data'; - -import { PrometheusDatasource } from '../datasource'; -import { getPrometheusTime } from '../language_utils'; -import { PromQuery } from '../types'; - -interface Props { - datasource: PrometheusDatasource; - query: PromQuery; - panelData?: PanelData; -} - -const PromLink = ({ panelData, query, datasource }: Props) => { - const [href, setHref] = useState(''); - - useEffect(() => { - if (panelData) { - const getExternalLink = () => { - if (!panelData.request) { - return ''; - } - - const { - request: { range, interval, scopedVars }, - } = panelData; - - const start = getPrometheusTime(range.from, false); - const end = getPrometheusTime(range.to, true); - const rangeDiff = Math.ceil(end - start); - const endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); - - const enrichedScopedVars: ScopedVars = { - ...scopedVars, - // As we support $__rate_interval variable in min step, we need add it to scopedVars - ...datasource.getRateIntervalScopedVariable( - rangeUtil.intervalToSeconds(interval), - rangeUtil.intervalToSeconds(datasource.interval) - ), - }; - - const options = { - interval, - scopedVars: enrichedScopedVars, - } as DataQueryRequest; - - const customQueryParameters: { [key: string]: string } = {}; - if (datasource.customQueryParameters) { - for (const [k, v] of datasource.customQueryParameters) { - customQueryParameters[k] = v; - } - } - - const queryOptions = datasource.createQuery(query, options, start, end); - - const expr = { - ...customQueryParameters, - 'g0.expr': queryOptions.expr, - 'g0.range_input': rangeDiff + 's', - 'g0.end_input': endTime, - 'g0.step_input': queryOptions.step, - 'g0.tab': 0, - }; - - const args = map(expr, (v: string, k: string) => { - return k + '=' + encodeURIComponent(v); - }).join('&'); - return `${datasource.directUrl}/graph?${args}`; - }; - - setHref(getExternalLink()); - } - }, [datasource, panelData, query]); - - return ( - - Prometheus - - ); -}; - -export default memo(PromLink); diff --git a/public/app/plugins/datasource/prometheus/configuration/mocks.ts b/public/app/plugins/datasource/prometheus/configuration/mocks.ts index 8a78cdbd7dc..3e5d98f3895 100644 --- a/public/app/plugins/datasource/prometheus/configuration/mocks.ts +++ b/public/app/plugins/datasource/prometheus/configuration/mocks.ts @@ -9,7 +9,6 @@ export function createDefaultConfigOptions(): DataSourceSettings { timeInterval: '1m', queryTimeout: '1m', httpMethod: 'GET', - directUrl: 'url', }, }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 60c946dcf5c..32a1e4d968b 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -1,25 +1,18 @@ import { cloneDeep } from 'lodash'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { lastValueFrom, of } from 'rxjs'; import { AnnotationEvent, AnnotationQueryRequest, CoreApp, DataQueryRequest, - DataQueryResponse, - DataQueryResponseData, DataSourceInstanceSettings, dateTime, - Field, - getFieldDisplayName, - LoadingState, rangeUtil, TimeRange, - toDataFrame, VariableHide, } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; -import { QueryOptions } from 'app/types'; import { alignRange, @@ -34,6 +27,7 @@ import { PrometheusCacheLevel, PromOptions, PromQuery, PromQueryRequest } from ' const fetchMock = jest.fn().mockReturnValue(of(createDefaultPromResponse())); jest.mock('./metric_find_query'); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ @@ -79,9 +73,9 @@ describe('PrometheusDatasource', () => { url: 'proxied', id: 1, uid: 'ABCDEF', - directUrl: 'direct', user: 'test', password: 'mupp', + access: 'proxy', jsonData: { customQueryParameters: '', cacheLevel: PrometheusCacheLevel.Low, @@ -103,35 +97,6 @@ describe('PrometheusDatasource', () => { }); describe('Query', () => { - it('returns empty array when no queries', async () => { - await expect(ds.query(createDataRequest([]))).toEmitValuesWith((response) => { - expect(response[0].data).toEqual([]); - expect(response[0].state).toBe(LoadingState.Done); - }); - }); - - it('performs time series queries', async () => { - await expect(ds.query(createDataRequest([{}]))).toEmitValuesWith((response) => { - expect(response[0].data.length).not.toBe(0); - expect(response[0].state).toBe(LoadingState.Done); - }); - }); - - it('with 2 queries and used from Explore, sends results as they arrive', async () => { - await expect(ds.query(createDataRequest([{}, {}], { app: CoreApp.Explore }))).toEmitValuesWith((response) => { - expect(response[0].data.length).not.toBe(0); - expect(response[0].state).toBe(LoadingState.Loading); - expect(response[1].state).toBe(LoadingState.Done); - }); - }); - - it('with 2 queries and used from Panel, waits for all to finish until sending Done status', async () => { - await expect(ds.query(createDataRequest([{}, {}], { app: CoreApp.Dashboard }))).toEmitValuesWith((response) => { - expect(response[0].data.length).not.toBe(0); - expect(response[0].state).toBe(LoadingState.Done); - }); - }); - it('throws if using direct access', async () => { const instanceSettings = { url: 'proxied', @@ -147,7 +112,20 @@ describe('PrometheusDatasource', () => { const directDs = new PrometheusDatasource(instanceSettings, templateSrvStub); await expect( - lastValueFrom(directDs.query(createDataRequest([{}, {}], { app: CoreApp.Dashboard }))) + lastValueFrom( + directDs.query( + createDataRequest( + [ + { + expr: '', + refId: 'A', + }, + { expr: '', refId: 'B' }, + ], + { app: CoreApp.Dashboard } + ) + ) + ) ).rejects.toMatchObject({ message: expect.stringMatching('Browser access') }); // Cannot test because some other tests need "./metric_find_query" to be mocked and that prevents this to be @@ -211,16 +189,6 @@ describe('PrometheusDatasource', () => { }); describe('customQueryParams', () => { - const target: PromQuery = { expr: 'test{job="testjob"}', format: 'time_series', refId: '' }; - - function makeQuery(target: PromQuery) { - return { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [target], - interval: '60s', - } as DataQueryRequest; - } - describe('with GET http method', () => { const promDs = new PrometheusDatasource( { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'GET' } }, @@ -232,27 +200,6 @@ describe('PrometheusDatasource', () => { expect(fetchMock.mock.calls.length).toBe(1); expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/uid/ABCDEF/resources/foo?customQuery=123'); }); - - it('adds params to timeseries query', () => { - promDs.query(makeQuery(target)); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe( - 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123' - ); - }); - it('adds params to exemplars query', () => { - promDs.query(makeQuery({ ...target, exemplar: true })); - // We do also range query for single exemplars target - expect(fetchMock.mock.calls.length).toBe(2); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); - expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123'); - }); - - it('adds params to instant query', () => { - promDs.query(makeQuery({ ...target, instant: true })); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); - }); }); describe('with POST http method', () => { @@ -273,32 +220,6 @@ describe('PrometheusDatasource', () => { expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/uid/ABCDEF/resources/api/v1/labels'); expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); }); - - it('adds params to timeseries query', () => { - promDs.query(makeQuery(target)); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/query_range'); - expect(fetchMock.mock.calls[0][0].data).toEqual({ - customQuery: '123', - query: 'test{job="testjob"}', - step: 60, - end: 180, - start: 60, - }); - }); - it('adds params to exemplars query', () => { - promDs.query(makeQuery({ ...target, exemplar: true })); - // We do also range query for single exemplars target - expect(fetchMock.mock.calls.length).toBe(2); - expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); - expect(fetchMock.mock.calls[1][0].data.customQuery).toBe('123'); - }); - - it('adds params to instant query', () => { - promDs.query(makeQuery({ ...target, instant: true })); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); - }); }); }); @@ -364,95 +285,6 @@ describe('PrometheusDatasource', () => { }); }); - describe('When converting prometheus histogram to heatmap format', () => { - let query: DataQueryRequest; - - beforeEach(() => { - query = { - range: { from: dateTime(1443454528000), to: dateTime(1443454528000) }, - targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }], - interval: '1s', - } as DataQueryRequest; - }); - - it('should convert cumulative histogram to ordinary', async () => { - const resultMock = [ - { - metric: { __name__: 'metric', job: 'testjob', le: '10' }, - values: [ - [1443454528.0, '10'], - [1443454528.0, '10'], - ], - }, - { - metric: { __name__: 'metric', job: 'testjob', le: '20' }, - values: [ - [1443454528.0, '20'], - [1443454528.0, '10'], - ], - }, - { - metric: { __name__: 'metric', job: 'testjob', le: '30' }, - values: [ - [1443454528.0, '25'], - [1443454528.0, '10'], - ], - }, - ]; - const responseMock = { data: { data: { result: resultMock } } }; - - ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock)); - await expect(ds.query(query)).toEmitValuesWith((result) => { - const results = result[0].data; - expect(results[0].fields[1].values).toEqual([10, 10]); - expect(results[0].fields[2].values).toEqual([10, 0]); - expect(results[0].fields[3].values).toEqual([5, 0]); - }); - }); - - it('should sort series by label value', async () => { - const resultMock = [ - { - metric: { __name__: 'metric', job: 'testjob', le: '2' }, - values: [ - [1443454528.0, '10'], - [1443454528.0, '10'], - ], - }, - { - metric: { __name__: 'metric', job: 'testjob', le: '4' }, - values: [ - [1443454528.0, '20'], - [1443454528.0, '10'], - ], - }, - { - metric: { __name__: 'metric', job: 'testjob', le: '+Inf' }, - values: [ - [1443454528.0, '25'], - [1443454528.0, '10'], - ], - }, - { - metric: { __name__: 'metric', job: 'testjob', le: '1' }, - values: [ - [1443454528.0, '25'], - [1443454528.0, '10'], - ], - }, - ]; - const responseMock = { data: { data: { result: resultMock } } }; - - const expected = ['1', '2', '4', '+Inf']; - - ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock)); - await expect(ds.query(query)).toEmitValuesWith((result) => { - const seriesLabels = result[0].data[0].fields.slice(1).map((field: Field) => getFieldDisplayName(field)); - expect(seriesLabels).toEqual(expected); - }); - }); - }); - describe('Test query range snapping', () => { it('test default 1 minute quantization', () => { const dataSource = new PrometheusDatasource( @@ -890,7 +722,6 @@ describe('PrometheusDatasource2', () => { url: 'proxied', id: 1, uid: 'ABCDEF', - directUrl: 'direct', user: 'test', password: 'mupp', jsonData: { httpMethod: 'GET', cacheLevel: PrometheusCacheLevel.None }, @@ -901,202 +732,6 @@ describe('PrometheusDatasource2', () => { ds = new PrometheusDatasource(instanceSettings, templateSrvStub); }); - describe('When querying prometheus with one target using query editor target spec', () => { - describe('and query syntax is valid', () => { - let results: DataQueryResponse; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - } as DataQueryRequest; - - // Interval alignment with step - const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent( - 'test{job="testjob"}' - )}&start=60&end=180&step=60`; - - beforeEach(async () => { - const response = { - data: { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[60, '3846']], - }, - ], - }, - }, - }; - fetchMock.mockImplementation(() => of(response)); - ds.query(query).subscribe((data) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should return series list', async () => { - const frame = toDataFrame(results.data[0]); - expect(results.data.length).toBe(1); - expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}'); - }); - }); - - describe('and query syntax is invalid', () => { - let results: string; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [{ expr: 'tes;;t{job="testjob"}', format: 'time_series' }], - interval: '60s', - } as DataQueryRequest; - - const errMessage = 'parse error at char 25: could not parse remaining input'; - const response = { - data: { - status: 'error', - errorType: 'bad_data', - error: errMessage, - }, - }; - - it('should generate an error', () => { - fetchMock.mockImplementation(() => throwError(response)); - ds.query(query).subscribe((e: any) => { - results = e.message; - expect(results).toBe(`"${errMessage}"`); - }); - }); - }); - }); - - describe('When querying prometheus with one target which returns multiple series', () => { - let results: DataQueryResponse; - const start = 60; - const end = 360; - const step = 60; - - const query = { - range: { from: time({ seconds: start }), to: time({ seconds: end }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - } as DataQueryRequest; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, - values: [ - [start + step * 1, '3846'], - [start + step * 3, '3847'], - [end - step * 1, '3848'], - ], - }, - { - metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, - values: [[start + step * 2, '4846']], - }, - ], - }, - }, - }; - - fetchMock.mockImplementation(() => of(response)); - - ds.query(query).subscribe((data) => { - results = data; - }); - }); - - it('should be same length', () => { - expect(results.data.length).toBe(2); - expect(results.data[0].length).toBe((end - start) / step + 1); - expect(results.data[1].length).toBe((end - start) / step + 1); - }); - - it('should fill null until first datapoint in response', () => { - expect(results.data[0].fields[0].values[0]).toBe(start * 1000); - expect(results.data[0].fields[1].values[0]).toBe(null); - expect(results.data[0].fields[0].values[1]).toBe((start + step * 1) * 1000); - expect(results.data[0].fields[1].values[1]).toBe(3846); - }); - - it('should fill null after last datapoint in response', () => { - const length = (end - start) / step + 1; - expect(results.data[0].fields[0].values[length - 2]).toBe((end - step * 1) * 1000); - expect(results.data[0].fields[1].values[length - 2]).toBe(3848); - expect(results.data[0].fields[0].values[length - 1]).toBe(end * 1000); - expect(results.data[0].fields[1].values[length - 1]).toBe(null); - }); - - it('should fill null at gap between series', () => { - expect(results.data[0].fields[0].values[2]).toBe((start + step * 2) * 1000); - expect(results.data[0].fields[1].values[2]).toBe(null); - expect(results.data[1].fields[0].values[1]).toBe((start + step * 1) * 1000); - expect(results.data[1].fields[1].values[1]).toBe(null); - expect(results.data[1].fields[0].values[3]).toBe((start + step * 3) * 1000); - expect(results.data[1].fields[1].values[3]).toBe(null); - }); - }); - - describe('When querying prometheus with one target and instant = true', () => { - let results: DataQueryResponse; - const urlExpected = `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=${encodeURIComponent( - 'test{job="testjob"}' - )}&time=123`; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - } as DataQueryRequest; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - fetchMock.mockImplementation(() => of(response)); - ds.query(query).subscribe((data) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should return series list', () => { - const frame = toDataFrame(results.data[0]); - expect(results.data.length).toBe(1); - expect(frame.name).toBe('test{job="testjob"}'); - expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}'); - }); - }); - describe('annotationQuery', () => { let results: AnnotationEvent[]; const options = { @@ -1311,569 +946,6 @@ describe('PrometheusDatasource2', () => { }); }); - describe('When resultFormat is table and instant = true', () => { - let results: DataQueryResponse; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - } as DataQueryRequest; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - fetchMock.mockImplementation(() => of(response)); - ds.query(query).subscribe((data: any) => { - results = data; - }); - }); - - it('should return result', () => { - expect(results).not.toBe(null); - }); - }); - - describe('The "step" query parameter', () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [] as DataQueryResponseData[], - }, - }, - }; - - it('should be min interval when greater than auto interval', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - }, - ], - interval: '5s', - } as DataQueryRequest; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('step should be fractional for sub second intervals', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [{ expr: 'test' }], - interval: '100ms', - } as DataQueryRequest; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=0.1'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should be auto interval when greater than min interval', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - }, - ], - interval: '10s', - } as DataQueryRequest; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should result in querying fewer than 11000 data points', async () => { - const query = { - // 6 hour range - range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, - targets: [{ expr: 'test' }], - interval: '1s', - } as DataQueryRequest; - const end = 7 * 60 * 60; - const start = 60 * 60; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should not apply min interval when interval * intervalFactor greater', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - } as DataQueryRequest; - // times get rounded up to interval - const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should apply min interval when interval * intervalFactor smaller', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - } as DataQueryRequest; - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should apply intervalFactor to auto interval when greater', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - } as DataQueryRequest; - // times get aligned to interval - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should not not be affected by the 11000 data points limit when large enough', async () => { - const query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '10s', - } as DataQueryRequest; - const end = 7 * 24 * 60 * 60; - const start = 0; - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should be determined by the 11000 data points limit when too small', async () => { - const query = { - // 1 week range - range: { from: time({ minutes: 1 }), to: time({ hours: 7 * 24, minutes: 1 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '5s', - } as DataQueryRequest; - let end = 7 * 24 * 60 * 60; - end -= end % 55; - const start = 60; - const step = 55; - const adjusted = alignRange(start, end, step, query.range.to.utcOffset() * 60); - const urlExpected = - 'proxied/api/v1/query_range?query=test' + - '&start=' + - adjusted.start + - '&end=' + - (adjusted.end + step) + - '&step=' + - step; - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - }); - - describe('The __interval and __interval_ms template variables', () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [] as DataQueryResponseData[], - }, - }, - }; - - it('should be unchanged when auto interval is greater than min interval', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=10'; - - replaceMock.mockImplementation((str) => str); - fetchMock.mockImplementation(() => of(response)); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '10s', - value: '10s', - }, - __interval_ms: { - text: 10000, - value: 10000, - }, - }); - }); - - it('should be min interval when it is greater than auto interval', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=10'; - fetchMock.mockImplementation(() => of(response)); - replaceMock.mockImplementation((str) => str); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - - it('should account for intervalFactor', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=0&end=400&step=100'; - fetchMock.mockImplementation(() => of(response)); - replaceMock.mockImplementation((str) => str); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '10s', - value: '10s', - }, - __interval_ms: { - text: 10000, - value: 10000, - }, - }); - - expect(query.scopedVars.__interval.text).toBe('10s'); - expect(query.scopedVars.__interval.value).toBe('10s'); - expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); - }); - - it('should be interval * intervalFactor when greater than min interval', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=50&end=400&step=50'; - - replaceMock.mockImplementation((str) => str); - fetchMock.mockImplementation(() => of(response)); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - - it('should be min interval when greater than interval * intervalFactor', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=15'; - - fetchMock.mockImplementation(() => of(response)); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - - it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { - const query = { - // 1 week range - range: { from: time({ minutes: 1 }), to: time({ hours: 7 * 24, minutes: 1 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - let end = 7 * 24 * 60 * 60; - end -= end % 55; - const start = 60; - const step = 55; - const adjusted = alignRange(start, end, step, query.range.to.utcOffset() * 60); - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=' + - adjusted.start + - '&end=' + - (adjusted.end + step) + - '&step=' + - step; - fetchMock.mockImplementation(() => of(response)); - replaceMock.mockImplementation((str) => str); - ds.query(query as any); - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - }); - - describe('The __range, __range_s and __range_ms variables', () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [] as DataQueryResponseData[], - }, - }, - }; - - it('should use overridden ranges, not dashboard ranges', async () => { - const expectedRangeSecond = 3600; - const expectedRangeString = '3600s'; - const query = { - range: { - from: time({}), - to: time({ hours: 1 }), - }, - targets: [ - { - expr: 'test[${__range_s}s]', - }, - ], - interval: '60s', - } as DataQueryRequest; - const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent( - query.targets[0].expr - )}&start=0&end=3600&step=60`; - - replaceMock.mockImplementation((str) => str); - fetchMock.mockImplementation(() => of(response)); - ds.query(query); - const res = fetchMock.mock.calls[0][0]; - expect(res.url).toBe(urlExpected); - - expect(replaceMock.mock.calls[1][1]).toEqual({ - __range_s: { - text: expectedRangeSecond, - value: expectedRangeSecond, - }, - __range: { - text: expectedRangeString, - value: expectedRangeString, - }, - __range_ms: { - text: expectedRangeSecond * 1000, - value: expectedRangeSecond * 1000, - }, - __rate_interval: { - text: '75s', - value: '75s', - }, - }); - }); - }); - describe('The __rate_interval variable', () => { const target = { expr: 'rate(process_cpu_seconds_total[$__rate_interval])', refId: 'A' }; @@ -1981,440 +1053,45 @@ describe('PrometheusDatasource2', () => { }); }); -describe('PrometheusDatasource for POST', () => { +describe('When querying prometheus via check headers X-Dashboard-Id X-Panel-Id and X-Dashboard-UID', () => { + const options = { panelId: 2, dashboardUID: 'WFlOM-jM1' } as DataQueryRequest; + const httpOptions = { + headers: {} as { [key: string]: number | undefined }, + } as PromQueryRequest; const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp', + access: 'proxy', jsonData: { httpMethod: 'POST' }, } as unknown as DataSourceInstanceSettings; let ds: PrometheusDatasource; beforeEach(() => { - ds = new PrometheusDatasource(instanceSettings, templateSrvStub); + ds = new PrometheusDatasource(instanceSettings, templateSrvStub as unknown as TemplateSrv); }); - describe('When querying prometheus with one target using query editor target spec', () => { - let results: DataQueryResponse; - const urlExpected = 'proxied/api/v1/query_range'; - const dataExpected = { - query: 'test{job="testjob"}', - start: 1 * 60, - end: 2 * 60, - step: 60, - }; - const query = { - range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - } as DataQueryRequest; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[2 * 60, '3846']], - }, - ], - }, - }, - }; - fetchMock.mockImplementation(() => of(response)); - ds.query(query).subscribe((data) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = fetchMock.mock.calls[0][0]; - expect(res.method).toBe('POST'); - expect(res.url).toBe(urlExpected); - expect(res.data).toEqual(dataExpected); - }); - - it('should return series list', () => { - const frame = toDataFrame(results.data[0]); - expect(results.data.length).toBe(1); - expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}'); - }); + it('with proxy access tracing headers should be added', () => { + ds._addTracingHeaders(httpOptions, options); + expect(httpOptions.headers['X-Panel-Id']).toBe(options.panelId); + expect(httpOptions.headers['X-Dashboard-UID']).toBe(options.dashboardUID); }); - describe('When querying prometheus via check headers X-Dashboard-Id X-Panel-Id and X-Dashboard-UID', () => { - const options = { panelId: 2, dashboardUID: 'WFlOM-jM1' } as DataQueryRequest; - const httpOptions = { - headers: {} as { [key: string]: number | undefined }, - } as PromQueryRequest; + it('with direct access tracing headers should not be added', () => { const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp', - access: 'proxy', jsonData: { httpMethod: 'POST' }, } as unknown as DataSourceInstanceSettings; - let ds: PrometheusDatasource; - beforeEach(() => { - ds = new PrometheusDatasource(instanceSettings, templateSrvStub as unknown as TemplateSrv); - }); - - it('with proxy access tracing headers should be added', () => { - ds._addTracingHeaders(httpOptions, options); - expect(httpOptions.headers['X-Panel-Id']).toBe(options.panelId); - expect(httpOptions.headers['X-Dashboard-UID']).toBe(options.dashboardUID); - }); - - it('with direct access tracing headers should not be added', () => { - const instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - } as unknown as DataSourceInstanceSettings; - - const mockDs = new PrometheusDatasource({ ...instanceSettings, url: 'http://127.0.0.1:8000' }, templateSrvStub); - mockDs._addTracingHeaders(httpOptions, options); - expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); - expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); - expect(httpOptions.headers['X-Dashboard-UID']).toBe(undefined); - }); - }); -}); - -function getPrepareTargetsContext({ - targets, - app, - queryOptions, - languageProvider, -}: { - targets: PromQuery[]; - app?: CoreApp; - queryOptions?: Partial; - languageProvider?: PromQlLanguageProvider; -}) { - const instanceSettings = { - url: 'proxied', - directUrl: 'direct', - access: 'proxy', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - } as unknown as DataSourceInstanceSettings; - const start = 0; - const end = 1; - const panelId = '2'; - const options = { - targets, - interval: '1s', - panelId, - app, - range: getMockTimeRange(), - ...queryOptions, - } as unknown as DataQueryRequest; - - const ds = new PrometheusDatasource(instanceSettings, templateSrvStub); - if (languageProvider) { - ds.languageProvider = languageProvider; - } - const { queries, activeTargets } = ds.prepareTargets(options, start, end); - - return { - queries, - activeTargets, - start, - end, - panelId, - }; -} - -describe('prepareTargets', () => { - describe('when run from a Panel', () => { - it('then it should just add targets', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ targets: [target] }); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Dashboard-UID': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: undefined, - refId: target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual(target); - }); - - it('should give back 3 targets when multiple queries with exemplar enabled and same metric', () => { - const targetA: PromQuery = { - refId: 'A', - expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - exemplar: true, - }; - const targetB: PromQuery = { - refId: 'B', - expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - exemplar: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ - targets: [targetA, targetB], - languageProvider: { - histogramMetrics: ['tns_request_duration_seconds_bucket'], - } as PromQlLanguageProvider, - }); - expect(queries).toHaveLength(3); - expect(activeTargets).toHaveLength(3); - }); - - it('should give back 4 targets when multiple queries with exemplar enabled', () => { - const targetA: PromQuery = { - refId: 'A', - expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - exemplar: true, - }; - const targetB: PromQuery = { - refId: 'B', - expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))', - exemplar: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ - targets: [targetA, targetB], - languageProvider: { - histogramMetrics: ['tns_request_duration_seconds_bucket'], - } as PromQlLanguageProvider, - }); - expect(queries).toHaveLength(4); - expect(activeTargets).toHaveLength(4); - }); - - it('should give back 2 targets when exemplar enabled', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - exemplar: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] }); - expect(queries).toHaveLength(2); - expect(activeTargets).toHaveLength(2); - expect(activeTargets[0].exemplar).toBe(true); - expect(activeTargets[1].exemplar).toBe(false); - }); - it('should give back 1 target when exemplar and instant are enabled', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - exemplar: true, - instant: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] }); - expect(queries).toHaveLength(1); - expect(activeTargets).toHaveLength(1); - expect(activeTargets[0].instant).toBe(true); - }); - }); - - describe('when run from Explore', () => { - describe('when query type Both is selected', () => { - it('should give back 6 targets when multiple queries with exemplar enabled', () => { - const targetA: PromQuery = { - refId: 'A', - expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - instant: true, - range: true, - exemplar: true, - }; - const targetB: PromQuery = { - refId: 'B', - expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))', - exemplar: true, - instant: true, - range: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ - targets: [targetA, targetB], - app: CoreApp.Explore, - languageProvider: { - histogramMetrics: ['tns_request_duration_seconds_bucket'], - } as PromQlLanguageProvider, - }); - expect(queries).toHaveLength(6); - expect(activeTargets).toHaveLength(6); - }); - - it('should give back 5 targets when multiple queries with exemplar enabled and same metric', () => { - const targetA: PromQuery = { - refId: 'A', - expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - instant: true, - range: true, - exemplar: true, - }; - const targetB: PromQuery = { - refId: 'B', - expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))', - exemplar: true, - instant: true, - range: true, - }; - - const { queries, activeTargets } = getPrepareTargetsContext({ - targets: [targetA, targetB], - app: CoreApp.Explore, - languageProvider: { - histogramMetrics: ['tns_request_duration_seconds_bucket'], - } as PromQlLanguageProvider, - }); - expect(queries).toHaveLength(5); - expect(activeTargets).toHaveLength(5); - }); - - it('then it should return both instant and time series related objects', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - range: true, - instant: true, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ - targets: [target], - app: CoreApp.Explore, - }); - - expect(queries.length).toBe(2); - expect(activeTargets.length).toBe(2); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Dashboard-UID': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: true, - refId: target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual({ - ...target, - format: 'table', - instant: true, - valueWithRefId: true, - }); - expect(queries[1]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Dashboard-UID': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: false, - refId: target.refId, - start, - step: 1, - }); - expect(activeTargets[1]).toEqual({ - ...target, - format: 'time_series', - instant: false, - }); - }); - }); - - describe('when query type Instant is selected', () => { - it('then it should target and modify its format to table', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - instant: true, - range: false, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ - targets: [target], - app: CoreApp.Explore, - }); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Dashboard-UID': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: true, - refId: target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual({ ...target, format: 'table' }); - }); - }); - }); - - describe('when query type Range is selected', () => { - it('then it should just add targets', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - range: true, - instant: false, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ - targets: [target], - app: CoreApp.Explore, - }); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Dashboard-UID': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: false, - refId: target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual(target); - }); + const mockDs = new PrometheusDatasource({ ...instanceSettings, url: 'http://127.0.0.1:8000' }, templateSrvStub); + mockDs._addTracingHeaders(httpOptions, options); + expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); + expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); + expect(httpOptions.headers['X-Dashboard-UID']).toBe(undefined); }); }); @@ -2480,24 +1157,29 @@ describe('modifyQuery', () => { }); }); -function createDataRequest(targets: any[], overrides?: Partial): DataQueryRequest { - const defaults = { +function createDataRequest(targets: PromQuery[], overrides?: Partial): DataQueryRequest { + const defaults: DataQueryRequest = { + intervalMs: 15000, + requestId: 'createDataRequest', + startTime: 0, + timezone: 'browser', app: CoreApp.Dashboard, - targets: targets.map((t) => { - return { - instant: false, - start: dateTime().subtract(5, 'minutes'), - end: dateTime(), - expr: 'test', - ...t, - }; - }), + targets: targets.map((t, i) => ({ + instant: false, + start: dateTime().subtract(5, 'minutes'), + end: dateTime(), + ...t, + })), range: { from: dateTime(), to: dateTime(), + raw: { + from: '', + to: '', + }, }, interval: '15s', - showingGraph: true, + scopedVars: {}, }; return Object.assign(defaults, overrides || {}) as DataQueryRequest; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 02218a579e0..92869351a1e 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -1,6 +1,6 @@ import { cloneDeep, defaults } from 'lodash'; -import { forkJoin, lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs'; -import { catchError, filter, map, tap } from 'rxjs/operators'; +import { lastValueFrom, Observable, throwError } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; import semver from 'semver/preload'; import { @@ -10,7 +10,6 @@ import { AnnotationQueryRequest, CoreApp, DataFrame, - DataQueryError, DataQueryRequest, DataQueryResponse, DataSourceGetTagKeysOptions, @@ -21,7 +20,6 @@ import { dateTime, getDefaultTimeRange, LegacyMetricFindQueryOptions, - LoadingState, MetricFindValue, QueryFixAction, rangeUtil, @@ -33,15 +31,13 @@ import { BackendDataSourceResponse, BackendSrvRequest, DataSourceWithBackend, - FetchError, FetchResponse, getBackendSrv, - isFetchError, - toDataQueryResponse, getTemplateSrv, + isFetchError, TemplateSrv, + toDataQueryResponse, } from '@grafana/runtime'; -import { safeStringifyValue } from 'app/core/utils/explore'; import { addLabelToQuery } from './add_label_to_query'; import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; @@ -57,21 +53,15 @@ import { getInitHints, getQueryHints } from './query_hints'; import { promQueryModeller } from './querybuilder/PromQueryModeller'; import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types'; import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache'; -import { getOriginalMetricName, transform, transformV2 } from './result_transformer'; +import { getOriginalMetricName, transformV2 } from './result_transformer'; import { trackQuery } from './tracking'; import { ExemplarTraceIdDestination, PromApplication, - PromDataErrorResponse, - PromDataSuccessResponse, PrometheusCacheLevel, - PromExemplarData, - PromMatrixData, PromOptions, PromQuery, PromQueryRequest, - PromScalarData, - PromVectorData, } from './types'; import { PrometheusVariableSupport } from './variables'; @@ -89,7 +79,6 @@ export class PrometheusDatasource hasIncrementalQuery: boolean; url: string; id: number; - directUrl: string; access: 'direct' | 'proxy'; basicAuth: any; withCredentials: any; @@ -124,9 +113,6 @@ export class PrometheusDatasource this.interval = instanceSettings.jsonData.timeInterval || '15s'; this.queryTimeout = instanceSettings.jsonData.queryTimeout; this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; - // `directUrl` is never undefined, we set it at https://github.com/grafana/grafana/blob/main/pkg/api/frontendsettings.go#L108 - // here we "fall back" to this.url to make typescript happy, but it should never happen - this.directUrl = instanceSettings.jsonData.directUrl ?? this.url; this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; this.hasIncrementalQuery = instanceSettings.jsonData.incrementalQuerying ?? false; this.ruleMappings = {}; @@ -222,6 +208,15 @@ export class PrometheusDatasource } } + directAccessError() { + return throwError( + () => + new Error( + 'Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.' + ) + ); + } + /** * Any request done from this data source should go through here as it contains some common processing for the * request. Any processing done here needs to be also copied on the backend as this goes through data source proxy @@ -233,10 +228,7 @@ export class PrometheusDatasource overrides: Partial = {} ): Observable> { if (this.access === 'direct') { - const error = new Error( - 'Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.' - ); - return throwError(() => error); + return this.directAccessError(); } data = data || {}; @@ -346,86 +338,6 @@ export class PrometheusDatasource return this.templateSrv.containsTemplate(target.expr); } - prepareTargets = (options: DataQueryRequest, start: number, end: number) => { - const queries: PromQueryRequest[] = []; - const activeTargets: PromQuery[] = []; - const clonedTargets = cloneDeep(options.targets); - - for (const target of clonedTargets) { - if (!target.expr || target.hide) { - continue; - } - - const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m)); - - // In Explore, we run both (instant and range) queries if both are true (selected) or both are undefined (legacy Explore queries) - if (options.app === CoreApp.Explore && target.range === target.instant) { - // Create instant target - const instantTarget: any = cloneDeep(target); - instantTarget.format = 'table'; - instantTarget.instant = true; - instantTarget.range = false; - instantTarget.valueWithRefId = true; - delete instantTarget.maxDataPoints; - - // Create range target - const rangeTarget = cloneDeep(target); - rangeTarget.format = 'time_series'; - rangeTarget.instant = false; - instantTarget.range = true; - - // Create exemplar query - if (target.exemplar) { - // Only create exemplar target for different metric names - if ( - !metricName || - (metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName))) - ) { - const exemplarTarget = cloneDeep(target); - exemplarTarget.instant = false; - queries.push(this.createQuery(exemplarTarget, options, start, end)); - activeTargets.push(exemplarTarget); - } - instantTarget.exemplar = false; - rangeTarget.exemplar = false; - } - - // Add both targets to activeTargets and queries arrays - activeTargets.push(instantTarget, rangeTarget); - queries.push( - this.createQuery(instantTarget, options, start, end), - this.createQuery(rangeTarget, options, start, end) - ); - // If running only instant query in Explore, format as table - } else if (target.instant && options.app === CoreApp.Explore) { - const instantTarget = cloneDeep(target); - instantTarget.format = 'table'; - queries.push(this.createQuery(instantTarget, options, start, end)); - activeTargets.push(instantTarget); - } else { - // It doesn't make sense to query for exemplars in dashboard if only instant is selected - if (target.exemplar && !target.instant) { - if ( - !metricName || - (metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName))) - ) { - const exemplarTarget = cloneDeep(target); - queries.push(this.createQuery(exemplarTarget, options, start, end)); - activeTargets.push(exemplarTarget); - } - target.exemplar = false; - } - queries.push(this.createQuery(target, options, start, end)); - activeTargets.push(target); - } - } - - return { - queries, - activeTargets, - }; - }; - shouldRunExemplarQuery(target: PromQuery, request: DataQueryRequest): boolean { if (target.exemplar) { // We check all already processed targets and only create exemplar target for not used metric names @@ -474,151 +386,40 @@ export class PrometheusDatasource } query(request: DataQueryRequest): Observable { - if (this.access === 'proxy') { - let fullOrPartialRequest: DataQueryRequest; - let requestInfo: CacheRequestInfo | undefined = undefined; - const hasInstantQuery = request.targets.some((target) => target.instant); - - // Don't cache instant queries - if (this.hasIncrementalQuery && !hasInstantQuery) { - requestInfo = this.cache.requestInfo(request); - fullOrPartialRequest = requestInfo.requests[0]; - } else { - fullOrPartialRequest = request; - } - - const targets = fullOrPartialRequest.targets.map((target) => this.processTargetV2(target, fullOrPartialRequest)); - const startTime = new Date(); - return super.query({ ...fullOrPartialRequest, targets: targets.flat() }).pipe( - map((response) => { - const amendedResponse = { - ...response, - data: this.cache.procFrames(request, requestInfo, response.data), - }; - return transformV2(amendedResponse, request, { - exemplarTraceIdDestinations: this.exemplarTraceIdDestinations, - }); - }), - tap((response: DataQueryResponse) => { - trackQuery(response, request, startTime); - }) - ); - // Run queries through browser/proxy - } else { - const start = getPrometheusTime(request.range.from, false); - const end = getPrometheusTime(request.range.to, true); - const { queries, activeTargets } = this.prepareTargets(request, start, end); - - // No valid targets, return the empty result to save a round trip. - if (!queries || !queries.length) { - return of({ - data: [], - state: LoadingState.Done, - }); - } - - if (request.app === CoreApp.Explore) { - return this.exploreQuery(queries, activeTargets, end); - } - - return this.panelsQuery(queries, activeTargets, end, request.requestId, request.scopedVars); + if (this.access === 'direct') { + return this.directAccessError(); } - } - - private exploreQuery(queries: PromQueryRequest[], activeTargets: PromQuery[], end: number) { - let runningQueriesCount = queries.length; - - const subQueries = queries.map((query, index) => { - const target = activeTargets[index]; - - const filterAndMapResponse = pipe( - // Decrease the counter here. We assume that each request returns only single value and then completes - // (should hold until there is some streaming requests involved). - tap(() => runningQueriesCount--), - filter((response: any) => (response.cancelled ? false : true)), - map((response) => { - const data = transform(response, { - query, - target, - responseListLength: queries.length, - exemplarTraceIdDestinations: this.exemplarTraceIdDestinations, - }); - const result: DataQueryResponse = { - data, - key: query.requestId, - state: runningQueriesCount === 0 ? LoadingState.Done : LoadingState.Loading, - }; - return result; - }) - ); - - return this.runQuery(query, end, filterAndMapResponse); - }); - return merge(...subQueries); - } + let fullOrPartialRequest: DataQueryRequest; + let requestInfo: CacheRequestInfo | undefined = undefined; + const hasInstantQuery = request.targets.some((target) => target.instant); - private panelsQuery( - queries: PromQueryRequest[], - activeTargets: PromQuery[], - end: number, - requestId: string, - scopedVars: ScopedVars - ) { - const observables = queries.map((query, index) => { - const target = activeTargets[index]; - - const filterAndMapResponse = pipe( - filter((response: any) => (response.cancelled ? false : true)), - map((response) => { - const data = transform(response, { - query, - target, - responseListLength: queries.length, - scopedVars, - exemplarTraceIdDestinations: this.exemplarTraceIdDestinations, - }); - return data; - }) - ); - - return this.runQuery(query, end, filterAndMapResponse); - }); + // Don't cache instant queries + if (this.hasIncrementalQuery && !hasInstantQuery) { + requestInfo = this.cache.requestInfo(request); + fullOrPartialRequest = requestInfo.requests[0]; + } else { + fullOrPartialRequest = request; + } - return forkJoin(observables).pipe( - map((results) => { - const data = results.reduce((result, current) => { - return [...result, ...current]; - }, []); - return { - data, - key: requestId, - state: LoadingState.Done, + const targets = fullOrPartialRequest.targets.map((target) => this.processTargetV2(target, fullOrPartialRequest)); + const startTime = new Date(); + return super.query({ ...fullOrPartialRequest, targets: targets.flat() }).pipe( + map((response) => { + const amendedResponse = { + ...response, + data: this.cache.procFrames(request, requestInfo, response.data), }; + return transformV2(amendedResponse, request, { + exemplarTraceIdDestinations: this.exemplarTraceIdDestinations, + }); + }), + tap((response: DataQueryResponse) => { + trackQuery(response, request, startTime); }) ); } - private runQuery(query: PromQueryRequest, end: number, filter: OperatorFunction): Observable { - if (query.instant) { - return this.performInstantQuery(query, end).pipe(filter); - } - - if (query.exemplar) { - return this.getExemplars(query).pipe( - catchError(() => { - return of({ - data: [], - state: LoadingState.Done, - }); - }), - filter - ); - } - - return this.performTimeSeriesQuery(query, query.start, query.end).pipe(filter); - } - createQuery(target: PromQuery, options: DataQueryRequest, start: number, end: number) { const query: PromQueryRequest = { hinting: target.hinting, @@ -704,93 +505,6 @@ export class PrometheusDatasource return Math.max(interval * intervalFactor, minInterval, safeInterval); } - performTimeSeriesQuery(query: PromQueryRequest, start: number, end: number) { - if (start > end) { - throw { message: 'Invalid time range' }; - } - - const url = '/api/v1/query_range'; - const data: any = { - query: query.expr, - start, - end, - step: query.step, - }; - - if (this.queryTimeout) { - data['timeout'] = this.queryTimeout; - } - - return this._request>(url, data, { - requestId: query.requestId, - headers: query.headers, - }).pipe( - catchError((err: FetchError>) => { - if (err.cancelled) { - return of(err); - } - - return throwError(this.handleErrors(err, query)); - }) - ); - } - - performInstantQuery( - query: PromQueryRequest, - time: number - ): Observable> | FetchError> { - const url = '/api/v1/query'; - const data: any = { - query: query.expr, - time, - }; - - if (this.queryTimeout) { - data['timeout'] = this.queryTimeout; - } - - return this._request>( - `/api/datasources/uid/${this.uid}/resources${url}`, - data, - { - requestId: query.requestId, - headers: query.headers, - } - ).pipe( - catchError((err: FetchError>) => { - if (err.cancelled) { - return of(err); - } - - return throwError(this.handleErrors(err, query)); - }) - ); - } - - handleErrors = (err: any, target: PromQuery) => { - const error: DataQueryError = { - message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.', - refId: target.refId, - }; - - if (err.data) { - if (typeof err.data === 'string') { - error.message = err.data; - } else if (err.data.error) { - error.message = safeStringifyValue(err.data.error); - } - } else if (err.message) { - error.message = err.message; - } else if (typeof err === 'string') { - error.message = err; - } - - error.status = err.status; - error.statusText = err.statusText; - - return error; - }; - metricFindQuery(query: string, options?: LegacyMetricFindQueryOptions) { if (!query) { return Promise.resolve([]); @@ -950,15 +664,6 @@ export class PrometheusDatasource return eventList; }; - getExemplars(query: PromQueryRequest) { - const url = '/api/v1/query_exemplars'; - return this._request>( - url, - { query: query.expr, start: query.start.toString(), end: query.end.toString() }, - { requestId: query.requestId, headers: query.headers } - ); - } - // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality // this is used to get label keys, a.k.a label names // it is used in metric_find_query.ts diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts index 1a9d8e9179f..3a9a777ad8b 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts @@ -20,7 +20,6 @@ const instanceSettings = { url: 'proxied', id: 1, uid: 'ABCDEF', - directUrl: 'direct', user: 'test', password: 'mupp', jsonData: { httpMethod: 'GET' }, @@ -226,9 +225,10 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric&time=${raw.to.unix()}`, - requestId: undefined, + url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric`, headers: {}, + hideFromInspector: true, + showErrorAlert: false, }); }); @@ -248,9 +248,10 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=1%2B1&time=${raw.to.unix()}`, - requestId: undefined, + url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=1%2B1`, headers: {}, + hideFromInspector: true, + showErrorAlert: false, }); }); diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index bc52e9ed7ac..c4383540212 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -1,6 +1,4 @@ import { chain, map as _map, uniq } from 'lodash'; -import { lastValueFrom } from 'rxjs'; -import { map } from 'rxjs/operators'; import { getDefaultTimeRange, MetricFindValue, TimeRange } from '@grafana/data'; @@ -12,7 +10,6 @@ import { PrometheusMetricNamesRegex, PrometheusQueryResultRegex, } from './migrations/variableMigration'; -import { PromQueryRequest } from './types'; export default class PrometheusMetricFindQuery { range: TimeRange; @@ -65,7 +62,7 @@ export default class PrometheusMetricFindQuery { const queryResultQuery = this.query.match(queryResultRegex); if (queryResultQuery) { - return lastValueFrom(this.queryResultQuery(queryResultQuery[1])); + return this.queryResultQuery(queryResultQuery[1]); } // if query contains full metric name, return metric name and label list @@ -136,41 +133,41 @@ export default class PrometheusMetricFindQuery { } queryResultQuery(query: string) { - const end = getPrometheusTime(this.range.to, true); - const instantQuery: PromQueryRequest = { expr: query } as PromQueryRequest; - return this.datasource.performInstantQuery(instantQuery, end).pipe( - map((result) => { - switch (result.data.data.resultType) { - case 'scalar': // [ , "" ] - case 'string': // [ , "" ] - return [ - { - text: result.data.data.result[1] || '', - expandable: false, - }, - ]; - case 'vector': - return _map(result.data.data.result, (metricData) => { - let text = metricData.metric.__name__ || ''; - delete metricData.metric.__name__; - text += - '{' + - _map(metricData.metric, (v, k) => { - return k + '="' + v + '"'; - }).join(',') + - '}'; - text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; - - return { - text: text, - expandable: true, - }; - }); - default: - throw Error(`Unknown/Unhandled result type: [${result.data.data.resultType}]`); - } - }) - ); + const url = '/api/v1/query'; + const params = { + query, + }; + return this.datasource.metadataRequest(url, params).then((result: any) => { + switch (result.data.data.resultType) { + case 'scalar': // [ , "" ] + case 'string': // [ , "" ] + return [ + { + text: result.data.data.result[1] || '', + expandable: false, + }, + ]; + case 'vector': + return _map(result.data.data.result, (metricData) => { + let text = metricData.metric.__name__ || ''; + delete metricData.metric.__name__; + text += + '{' + + _map(metricData.metric, (v, k) => { + return k + '="' + v + '"'; + }).join(',') + + '}'; + text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; + + return { + text: text, + expandable: true, + }; + }); + default: + throw Error(`Unknown/Unhandled result type: [${result.data.data.resultType}]`); + } + }); } metricNameAndLabelsQuery(query: string): Promise { diff --git a/public/app/plugins/datasource/prometheus/query_hints.ts b/public/app/plugins/datasource/prometheus/query_hints.ts index 82498f7f4c7..aa08db4ae9d 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.ts @@ -138,13 +138,6 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet export function getInitHints(datasource: PrometheusDatasource): QueryHint[] { const hints = []; - // Hint if using Loki as Prometheus data source - if (datasource.directUrl.includes('/loki') && !datasource.languageProvider.metrics.length) { - hints.push({ - label: `Using Loki as a Prometheus data source is no longer supported. You must use the Loki data source for your Loki instance.`, - type: 'INFO', - }); - } // Hint for big disabled lookups if (datasource.lookupsDisabled) { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx index 1d9a62cc7f6..811d3da8aa4 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx @@ -17,7 +17,6 @@ import { const instanceSettings = { url: 'proxied', id: 1, - directUrl: 'direct', user: 'test', password: 'mupp', jsonData: { httpMethod: 'GET' }, diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index 76240c37803..1c18dce5425 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -1,14 +1,13 @@ import { cacheFieldDisplayNames, createDataFrame, - DataFrame, DataQueryRequest, DataQueryResponse, FieldType, PreferredVisualisationType, } from '@grafana/data'; -import { parseSampleValue, sortSeriesByLabel, transform, transformDFToTable, transformV2 } from './result_transformer'; +import { parseSampleValue, sortSeriesByLabel, transformDFToTable, transformV2 } from './result_transformer'; import { PromQuery } from './types'; jest.mock('@grafana/runtime', () => ({ @@ -30,22 +29,6 @@ jest.mock('@grafana/runtime', () => ({ }, })); -const matrixResponse = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [ - [1, '10'], - [2, '0'], - ], - }, - ], - }, -}; - describe('Prometheus Result Transformer', () => { describe('parse variants of "+Inf" and "-Inf" strings', () => { it('+Inf', () => { @@ -1088,580 +1071,4 @@ describe('Prometheus Result Transformer', () => { expect(transformedTableDataFrames[1].meta?.executedQueryString).toEqual(executedQueryForRefB); }); }); - - describe('transform', () => { - const options: any = { target: {}, query: {} }; - describe('When nothing is returned', () => { - it('should return empty array', () => { - const response = { - status: 'success', - data: { - resultType: '', - result: null, - }, - }; - const series = transform({ data: response } as any, options); - expect(series).toEqual([]); - }); - it('should return empty array', () => { - const response = { - status: 'success', - data: { - resultType: '', - result: null, - }, - }; - const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); - expect(result).toHaveLength(0); - }); - }); - - describe('When resultFormat is table', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [ - [1443454528, '3846'], - [1443454530, '3848'], - ], - }, - { - metric: { - __name__: 'test2', - instance: 'localhost:8080', - job: 'otherjob', - }, - values: [ - [1443454529, '3847'], - [1443454531, '3849'], - ], - }, - ], - }, - }; - - it('should return data frame', () => { - const result = transform({ data: response } as any, { - ...options, - target: { - responseListLength: 0, - refId: 'A', - format: 'table', - }, - }); - expect(result[0].fields[0].values).toEqual([1443454528000, 1443454530000, 1443454529000, 1443454531000]); - expect(result[0].fields[0].name).toBe('Time'); - expect(result[0].fields[0].type).toBe(FieldType.time); - expect(result[0].fields[1].values).toEqual(['test', 'test', 'test2', 'test2']); - expect(result[0].fields[1].name).toBe('__name__'); - expect(result[0].fields[1].config.filterable).toBe(true); - expect(result[0].fields[1].type).toBe(FieldType.string); - expect(result[0].fields[2].values).toEqual(['', '', 'localhost:8080', 'localhost:8080']); - expect(result[0].fields[2].name).toBe('instance'); - expect(result[0].fields[2].type).toBe(FieldType.string); - expect(result[0].fields[3].values).toEqual(['testjob', 'testjob', 'otherjob', 'otherjob']); - expect(result[0].fields[3].name).toBe('job'); - expect(result[0].fields[3].type).toBe(FieldType.string); - expect(result[0].fields[4].values).toEqual([3846, 3848, 3847, 3849]); - expect(result[0].fields[4].name).toEqual('Value'); - expect(result[0].fields[4].type).toBe(FieldType.number); - expect(result[0].refId).toBe('A'); - }); - - it('should include refId if response count is more than 2', () => { - const result = transform({ data: response } as any, { - ...options, - target: { - refId: 'B', - format: 'table', - }, - responseListLength: 2, - }); - - expect(result[0].fields[4].name).toEqual('Value #B'); - }); - }); - - describe('When resultFormat is table and instant = true', () => { - const response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [1443454528, '3846'], - }, - ], - }, - }; - - it('should return data frame', () => { - const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); - expect(result[0].fields[0].values).toEqual([1443454528000]); - expect(result[0].fields[0].name).toBe('Time'); - expect(result[0].fields[1].values).toEqual(['test']); - expect(result[0].fields[1].name).toBe('__name__'); - expect(result[0].fields[2].values).toEqual(['testjob']); - expect(result[0].fields[2].name).toBe('job'); - expect(result[0].fields[3].values).toEqual([3846]); - expect(result[0].fields[3].name).toEqual('Value'); - }); - - it('should return le label values parsed as numbers', () => { - const response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { le: '102' }, - value: [1594908838, '0'], - }, - ], - }, - }; - const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); - expect(result[0].fields[1].values).toEqual([102]); - expect(result[0].fields[1].type).toEqual(FieldType.number); - }); - }); - - describe('When instant = true', () => { - const response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [1443454528, '3846'], - }, - ], - }, - }; - - it('should return data frame', () => { - const result: DataFrame[] = transform({ data: response } as any, { ...options, query: { instant: true } }); - expect(result[0].name).toBe('test{job="testjob"}'); - }); - }); - - describe('When resultFormat is heatmap', () => { - const getResponse = (result: any) => ({ - status: 'success', - data: { - resultType: 'matrix', - result, - }, - }); - - const options = { - format: 'heatmap', - start: 1445000010, - end: 1445000030, - legendFormat: '{{le}}', - }; - - it('should convert cumulative histogram to regular', () => { - const response = getResponse([ - { - metric: { __name__: 'test', job: 'testjob', le: '1' }, - values: [ - [1445000010, '10'], - [1445000020, '10'], - [1445000030, '0'], - ], - }, - { - metric: { __name__: 'test', job: 'testjob', le: '2' }, - values: [ - [1445000010, '20'], - [1445000020, '10'], - [1445000030, '30'], - ], - }, - { - metric: { __name__: 'test', job: 'testjob', le: '3' }, - values: [ - [1445000010, '30'], - [1445000020, '10'], - [1445000030, '40'], - ], - }, - ]); - - const result = transform({ data: response } as any, { query: options, target: options } as any); - expect(result[0].fields[0].values).toEqual([1445000010000, 1445000020000, 1445000030000]); - expect(result[0].fields[1].values).toEqual([10, 10, 0]); - expect(result[0].fields[2].values).toEqual([10, 0, 30]); - expect(result[0].fields[3].values).toEqual([10, 0, 10]); - }); - - it('should handle missing datapoints', () => { - const response = getResponse([ - { - metric: { __name__: 'test', job: 'testjob', le: '1' }, - values: [ - [1445000010, '1'], - [1445000020, '2'], - ], - }, - { - metric: { __name__: 'test', job: 'testjob', le: '2' }, - values: [ - [1445000010, '2'], - [1445000020, '5'], - [1445000030, '1'], - ], - }, - { - metric: { __name__: 'test', job: 'testjob', le: '3' }, - values: [ - [1445000010, '3'], - [1445000020, '7'], - ], - }, - ]); - const result = transform({ data: response } as any, { query: options, target: options } as any); - expect(result[0].fields[1].values).toEqual([1, 2]); - expect(result[0].fields[2].values).toEqual([1, 3, 1]); - expect(result[0].fields[3].values).toEqual([1, 2]); - }); - }); - - describe('When the response is a matrix', () => { - it('should have labels with the value field', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob', instance: 'testinstance' }, - values: [ - [0, '10'], - [1, '10'], - [2, '0'], - ], - }, - ], - }, - }; - - const result: DataFrame[] = transform({ data: response } as any, { - ...options, - }); - - expect(result[0].fields[1].labels).toBeDefined(); - expect(result[0].fields[1].labels?.instance).toBe('testinstance'); - expect(result[0].fields[1].labels?.job).toBe('testjob'); - }); - - it('should transform into a data frame', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [ - [0, '10'], - [1, '10'], - [2, '0'], - ], - }, - ], - }, - }; - - const result: DataFrame[] = transform({ data: response } as any, { - ...options, - query: { - start: 0, - end: 2, - }, - }); - expect(result[0].fields[0].values).toEqual([0, 1000, 2000]); - expect(result[0].fields[1].values).toEqual([10, 10, 0]); - expect(result[0].name).toBe('test{job="testjob"}'); - }); - - it('should fill null values', () => { - const result = transform({ data: matrixResponse } as any, { - ...options, - query: { step: 1, start: 0, end: 2 }, - }); - - expect(result[0].fields[0].values).toEqual([0, 1000, 2000]); - expect(result[0].fields[1].values).toEqual([null, 10, 0]); - }); - - it('should use __name__ label as series name', () => { - const result = transform({ data: matrixResponse } as any, { - ...options, - query: { - step: 1, - start: 0, - end: 2, - }, - }); - expect(result[0].name).toEqual('test{job="testjob"}'); - }); - - it('should use query as series name when __name__ is not available and metric is empty', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: {}, - values: [[0, '10']], - }, - ], - }, - }; - const expr = 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))'; - const result = transform({ data: response } as any, { - ...options, - query: { - step: 1, - start: 0, - end: 2, - expr, - }, - }); - expect(result[0].name).toEqual(expr); - }); - - it('should set frame name to undefined if no __name__ label but there are other labels', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { job: 'testjob' }, - values: [ - [1, '10'], - [2, '0'], - ], - }, - ], - }, - }; - - const result = transform({ data: response } as any, { - ...options, - query: { - step: 1, - start: 0, - end: 2, - }, - }); - expect(result[0].name).toBe('{job="testjob"}'); - }); - - it('should not set displayName for ValueFields', () => { - const result = transform({ data: matrixResponse } as any, options); - expect(result[0].fields[1].config.displayName).toBeUndefined(); - expect(result[0].fields[1].config.displayNameFromDS).toBe('test{job="testjob"}'); - }); - - it('should align null values with step', () => { - const response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [ - [4, '10'], - [8, '10'], - ], - }, - ], - }, - }; - - const result = transform({ data: response } as any, { ...options, query: { step: 2, start: 0, end: 8 } }); - expect(result[0].fields[0].values).toEqual([0, 2000, 4000, 6000, 8000]); - expect(result[0].fields[1].values).toEqual([null, null, 10, null, 10]); - }); - }); - - describe('When infinity values are returned', () => { - describe('When resultType is scalar', () => { - const response = { - status: 'success', - data: { - resultType: 'scalar', - result: [1443454528, '+Inf'], - }, - }; - - it('should correctly parse values', () => { - const result: DataFrame[] = transform({ data: response } as any, { - ...options, - target: { format: 'table' }, - }); - expect(result[0].fields[1].values).toEqual([Number.POSITIVE_INFINITY]); - }); - }); - - describe('When resultType is vector', () => { - const response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [1443454528, '+Inf'], - }, - { - metric: { __name__: 'test', job: 'testjob' }, - value: [1443454528, '-Inf'], - }, - ], - }, - }; - - describe('When format is table', () => { - it('should correctly parse values', () => { - const result: DataFrame[] = transform({ data: response } as any, { - ...options, - target: { format: 'table' }, - }); - expect(result[0].fields[3].values).toEqual([Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]); - }); - }); - }); - }); - - const exemplarsResponse = { - status: 'success', - data: [ - { - seriesLabels: { __name__: 'test' }, - exemplars: [ - { - timestamp: 1610449069.957, - labels: { traceID: '5020b5bc45117f07' }, - value: 0.002074123, - }, - ], - }, - ], - }; - - describe('When the response is exemplar data', () => { - it('should return as an data frame with a dataTopic annotations', () => { - const result = transform({ data: exemplarsResponse } as any, options); - - expect(result[0].meta?.dataTopic).toBe('annotations'); - expect(result[0].fields.length).toBe(4); // __name__, traceID, Time, Value - expect(result[0].length).toBe(1); - }); - - it('should return with an empty array when data is empty', () => { - const result = transform( - { - data: { - status: 'success', - data: [], - }, - } as any, - options - ); - - expect(result).toHaveLength(0); - }); - - it('should remove exemplars that are too close to each other', () => { - const response = { - status: 'success', - data: [ - { - exemplars: [ - { - timestamp: 1610449070.0, - value: 5, - }, - { - timestamp: 1610449070.0, - value: 1, - }, - { - timestamp: 1610449070.5, - value: 13, - }, - { - timestamp: 1610449070.3, - value: 20, - }, - ], - }, - ], - }; - /** - * the standard deviation for the above values is 8.4 this means that we show the highest - * value (20) and then the next value should be 2 times the standard deviation which is 1 - **/ - const result = transform({ data: response } as any, options); - expect(result[0].length).toBe(2); - }); - - describe('data link', () => { - it('should be added to the field if found with url', () => { - const result = transform({ data: exemplarsResponse } as any, { - ...options, - exemplarTraceIdDestinations: [{ name: 'traceID', url: 'http://localhost' }], - }); - - expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true); - }); - - it('should be added to the field if found with internal link', () => { - const result = transform({ data: exemplarsResponse } as any, { - ...options, - exemplarTraceIdDestinations: [{ name: 'traceID', datasourceUid: 'jaeger' }], - }); - - expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true); - }); - - it('should not add link if exemplarTraceIdDestinations is not configured', () => { - const result = transform({ data: exemplarsResponse } as any, options); - - expect(result[0].fields.some((f) => f.config.links?.length)).toBe(false); - }); - - it('should not add a datalink with an error when exemplarTraceIdDestinations is not configured', () => { - const testOptions: any = { - target: {}, - query: {}, - exemplarTraceIdDestinations: [ - { - name: 'traceID', - datasourceUid: 'unknown', - }, - ], - }; - - const result = transform({ data: exemplarsResponse } as any, testOptions); - const traceField = result[0].fields.find((f) => f.name === 'traceID'); - expect(traceField).toBeDefined(); - expect(traceField!.config.links?.length).toBe(0); - }); - }); - }); - }); }); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index e8174be5fd2..0fbcfe58806 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -1,8 +1,6 @@ -import { descending, deviation } from 'd3'; import { flatten, forOwn, groupBy, partition } from 'lodash'; import { - ArrayDataFrame, CoreApp, DataFrame, DataFrameType, @@ -12,38 +10,19 @@ import { DataTopic, Field, FieldType, - formatLabels, getDisplayProcessor, getFieldDisplayName, Labels, - renderLegendFormat, - ScopedVars, TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, } from '@grafana/data'; -import { config, FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; +import { config, getDataSourceSrv } from '@grafana/runtime'; -import { - ExemplarTraceIdDestination, - isExemplarData, - isMatrixData, - MatrixOrVectorResult, - PromDataSuccessResponse, - PromMetric, - PromQuery, - PromQueryRequest, - PromValue, - TransformOptions, -} from './types'; +import { ExemplarTraceIdDestination, PromMetric, PromQuery, PromValue } from './types'; // handles case-insensitive Inf, +Inf, -Inf (with optional "inity" suffix) const INFINITY_SAMPLE_REGEX = /^[+-]?inf(?:inity)?$/i; -interface TimeAndValue { - [TIME_SERIES_TIME_FIELD_NAME]: number; - [TIME_SERIES_VALUE_FIELD_NAME]: number; -} - const isTableResult = (dataFrame: DataFrame, options: DataQueryRequest): boolean => { // We want to process vector and scalar results in Explore as table if ( @@ -264,104 +243,6 @@ function getValueText(responseLength: number, refId = '') { return responseLength > 1 ? `Value #${refId}` : 'Value'; } -export function transform( - response: FetchResponse, - transformOptions: { - query: PromQueryRequest; - exemplarTraceIdDestinations?: ExemplarTraceIdDestination[]; - target: PromQuery; - responseListLength: number; - scopedVars?: ScopedVars; - } -) { - // Create options object from transformOptions - const options: TransformOptions = { - format: transformOptions.target.format, - step: transformOptions.query.step, - legendFormat: transformOptions.target.legendFormat, - start: transformOptions.query.start, - end: transformOptions.query.end, - query: transformOptions.query.expr, - responseListLength: transformOptions.responseListLength, - scopedVars: transformOptions.scopedVars, - refId: transformOptions.target.refId, - valueWithRefId: transformOptions.target.valueWithRefId, - meta: { - // Fix for showing of Prometheus results in Explore table - preferredVisualisationType: transformOptions.query.instant ? 'rawPrometheus' : 'graph', - }, - }; - const prometheusResult = response.data.data; - - if (isExemplarData(prometheusResult)) { - const events: TimeAndValue[] = []; - prometheusResult.forEach((exemplarData) => { - const data = exemplarData.exemplars.map((exemplar) => { - return { - [TIME_SERIES_TIME_FIELD_NAME]: exemplar.timestamp * 1000, - [TIME_SERIES_VALUE_FIELD_NAME]: exemplar.value, - ...exemplar.labels, - ...exemplarData.seriesLabels, - }; - }); - events.push(...data); - }); - - // Grouping exemplars by step - const sampledExemplars = sampleExemplars(events, options); - - const dataFrame = new ArrayDataFrame(sampledExemplars); - dataFrame.meta = { dataTopic: DataTopic.Annotations }; - - // Add data links if configured - if (transformOptions.exemplarTraceIdDestinations?.length) { - for (const exemplarTraceIdDestination of transformOptions.exemplarTraceIdDestinations) { - const traceIDField = dataFrame.fields.find((field) => field.name === exemplarTraceIdDestination.name); - if (traceIDField) { - const links = getDataLinks(exemplarTraceIdDestination); - traceIDField.config.links = traceIDField.config.links?.length - ? [...traceIDField.config.links, ...links] - : links; - } - } - } - return [dataFrame]; - } - - if (!prometheusResult?.result) { - return []; - } - - // Return early if result type is scalar - if (prometheusResult.resultType === 'scalar') { - const df: DataFrame = { - meta: options.meta, - refId: options.refId, - length: 1, - fields: [getTimeField([prometheusResult.result]), getValueField({ data: [prometheusResult.result] })], - }; - return [df]; - } - - // Return early again if the format is table, this needs special transformation. - if (options.format === 'table') { - const tableData = transformMetricDataToTable(prometheusResult.result, options); - return [tableData]; - } - - // Process matrix and vector results to DataFrame - const dataFrame: DataFrame[] = []; - prometheusResult.result.forEach((data: MatrixOrVectorResult) => dataFrame.push(transformToDataFrame(data, options))); - - // When format is heatmap use the already created data frames and transform it more - if (options.format === 'heatmap') { - return mergeHeatmapFrames(transformToHistogramOverTime(dataFrame.sort(sortSeriesByLabel))); - } - - // Return matrix or vector result as DataFrame[] - return dataFrame; -} - function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] { const dataLinks: DataLink[] = []; @@ -396,160 +277,6 @@ function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] { return dataLinks; } -/** - * Reduce the density of the exemplars by making sure that the highest value exemplar is included - * and then only the ones that are 2 times the standard deviation of the all the values. - * This makes sure not to show too many dots near each other. - */ -function sampleExemplars(events: TimeAndValue[], options: TransformOptions) { - const step = options.step || 15; - const bucketedExemplars: { [ts: string]: TimeAndValue[] } = {}; - const values: number[] = []; - for (const exemplar of events) { - // Align exemplar timestamp to nearest step second - const alignedTs = String(Math.floor(exemplar[TIME_SERIES_TIME_FIELD_NAME] / 1000 / step) * step * 1000); - if (!bucketedExemplars[alignedTs]) { - // New bucket found - bucketedExemplars[alignedTs] = []; - } - bucketedExemplars[alignedTs].push(exemplar); - values.push(exemplar[TIME_SERIES_VALUE_FIELD_NAME]); - } - - // Getting exemplars from each bucket - const standardDeviation = deviation(values); - const sampledBuckets = Object.keys(bucketedExemplars).sort(); - const sampledExemplars = []; - for (const ts of sampledBuckets) { - const exemplarsInBucket = bucketedExemplars[ts]; - if (exemplarsInBucket.length === 1) { - sampledExemplars.push(exemplarsInBucket[0]); - } else { - // Choose which values to sample - const bucketValues = exemplarsInBucket.map((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME]).sort(descending); - const sampledBucketValues = bucketValues.reduce((acc: number[], curr) => { - if (acc.length === 0) { - // First value is max and is always added - acc.push(curr); - } else { - // Then take values only when at least 2 standard deviation distance to previously taken value - const prev = acc[acc.length - 1]; - if (standardDeviation && prev - curr >= 2 * standardDeviation) { - acc.push(curr); - } - } - return acc; - }, []); - // Find the exemplars for the sampled values - sampledExemplars.push( - ...sampledBucketValues.map( - (value) => exemplarsInBucket.find((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME] === value)! - ) - ); - } - } - return sampledExemplars; -} - -/** - * Transforms matrix and vector result from Prometheus result to DataFrame - */ -function transformToDataFrame(data: MatrixOrVectorResult, options: TransformOptions): DataFrame { - const { name, labels } = createLabelInfo(data.metric, options); - - const fields: Field[] = []; - - if (isMatrixData(data)) { - const stepMs = options.step ? options.step * 1000 : NaN; - let baseTimestamp = options.start * 1000; - const dps: PromValue[] = []; - - for (const value of data.values) { - let dpValue: number | null = parseSampleValue(value[1]); - - if (isNaN(dpValue)) { - dpValue = null; - } - - const timestamp = value[0] * 1000; - for (let t = baseTimestamp; t < timestamp; t += stepMs) { - dps.push([t, null]); - } - baseTimestamp = timestamp + stepMs; - dps.push([timestamp, dpValue]); - } - - const endTimestamp = options.end * 1000; - for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) { - dps.push([t, null]); - } - fields.push(getTimeField(dps, true)); - fields.push(getValueField({ data: dps, parseValue: false, labels, displayNameFromDS: name })); - } else { - fields.push(getTimeField([data.value])); - fields.push(getValueField({ data: [data.value], labels, displayNameFromDS: name })); - } - - return { - meta: options.meta, - refId: options.refId, - length: fields[0].values.length, - fields, - name, - }; -} - -function transformMetricDataToTable(md: MatrixOrVectorResult[], options: TransformOptions): DataFrame { - if (!md || md.length === 0) { - return { - meta: options.meta, - refId: options.refId, - length: 0, - fields: [], - }; - } - - const valueText = options.responseListLength > 1 || options.valueWithRefId ? `Value #${options.refId}` : 'Value'; - - const timeField = getTimeField([]); - const metricFields = Object.keys(md.reduce((acc, series) => ({ ...acc, ...series.metric }), {})) - .sort() - .map((label) => { - // Labels have string field type, otherwise table tries to figure out the type which can result in unexpected results - // Only "le" label has a number field type - const numberField = label === HISTOGRAM_QUANTILE_LABEL_NAME; - const field: Field = { - name: label, - config: { filterable: true }, - type: numberField ? FieldType.number : FieldType.string, - values: [], - }; - return field; - }); - const valueField = getValueField({ data: [], valueName: valueText }); - - md.forEach((d) => { - if (isMatrixData(d)) { - d.values.forEach((val) => { - timeField.values.push(val[0] * 1000); - metricFields.forEach((metricField) => metricField.values.push(getLabelValue(d.metric, metricField.name))); - valueField.values.push(parseSampleValue(val[1])); - }); - } else { - timeField.values.push(d.value[0] * 1000); - metricFields.forEach((metricField) => metricField.values.push(getLabelValue(d.metric, metricField.name))); - valueField.values.push(parseSampleValue(d.value[1])); - } - }); - - return { - meta: options.meta, - refId: options.refId, - length: timeField.values.length, - fields: [timeField, ...metricFields, valueField], - }; -} - function getLabelValue(metric: PromMetric, label: string): string | number { if (metric.hasOwnProperty(label)) { if (label === HISTOGRAM_QUANTILE_LABEL_NAME) { @@ -596,23 +323,6 @@ function getValueField({ }; } -function createLabelInfo(labels: { [key: string]: string }, options: TransformOptions) { - if (options?.legendFormat) { - const title = renderLegendFormat(getTemplateSrv().replace(options.legendFormat, options?.scopedVars), labels); - return { name: title, labels }; - } - - const { __name__, ...labelsWithoutName } = labels; - const labelPart = formatLabels(labelsWithoutName); - let title = `${__name__ ?? ''}${labelPart}`; - - if (!title) { - title = options.query; - } - - return { name: title, labels: labelsWithoutName }; -} - export function getOriginalMetricName(labelData: { [key: string]: string }) { const metricName = labelData.__name__ || ''; delete labelData.__name__; diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 2f0e26f9a97..5ddb69d811b 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -1,4 +1,4 @@ -import { DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data'; +import { DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { Prometheus as GenPromQuery } from './dataquery.gen'; @@ -39,7 +39,6 @@ export interface PromOptions extends DataSourceJsonData { timeInterval?: string; queryTimeout?: string; httpMethod?: string; - directUrl?: string; customQueryParameters?: string; disableMetricsLookup?: boolean; exemplarTraceIdDestinations?: ExemplarTraceIdDestination[]; @@ -79,56 +78,6 @@ export interface PromMetricsMetadata { [metric: string]: PromMetricsMetadataItem; } -export interface PromDataSuccessResponse { - status: 'success'; - data: T; -} - -export interface PromDataErrorResponse { - status: 'error'; - errorType: string; - error: string; - data: T; -} - -export type PromData = PromMatrixData | PromVectorData | PromScalarData | PromExemplarData[]; - -export interface Labels { - [index: string]: any; -} - -export interface Exemplar { - labels: Labels; - value: number; - timestamp: number; -} - -export interface PromExemplarData { - seriesLabels: PromMetric; - exemplars: Exemplar[]; -} - -export interface PromVectorData { - resultType: 'vector'; - result: Array<{ - metric: PromMetric; - value: PromValue; - }>; -} - -export interface PromMatrixData { - resultType: 'matrix'; - result: Array<{ - metric: PromMetric; - values: PromValue[]; - }>; -} - -export interface PromScalarData { - resultType: 'scalar'; - result: PromValue; -} - export type PromValue = [number, any]; export interface PromMetric { @@ -137,33 +86,6 @@ export interface PromMetric { [index: string]: any; } -export function isMatrixData(result: MatrixOrVectorResult): result is PromMatrixData['result'][0] { - return 'values' in result; -} - -export function isExemplarData(result: PromData): result is PromExemplarData[] { - if (result == null || !Array.isArray(result)) { - return false; - } - return result.length ? 'exemplars' in result[0] : false; -} - -export type MatrixOrVectorResult = PromMatrixData['result'][0] | PromVectorData['result'][0]; - -export interface TransformOptions { - format?: string; - step?: number; - legendFormat?: string; - start: number; - end: number; - query: string; - responseListLength: number; - scopedVars?: ScopedVars; - refId: string; - valueWithRefId?: boolean; - meta: QueryResultMeta; -} - export interface PromBuildInfoResponse { data: { application?: string;