diff --git a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts similarity index 99% rename from public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts rename to public/app/plugins/datasource/prometheus/add_label_to_query.test.ts index 2aa8ea357f8..d577baf7bc8 100644 --- a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts @@ -1,4 +1,4 @@ -import { addLabelToQuery, addLabelToSelector, keepSelectorFilters } from '../add_label_to_query'; +import { addLabelToQuery, addLabelToSelector, keepSelectorFilters } from './add_label_to_query'; describe('addLabelToQuery()', () => { it('should add label to simple query', () => { diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 2a96c93d405..e440be233b1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -1,49 +1,72 @@ -import { PrometheusDatasource } from './datasource'; -import { DataSourceInstanceSettings } from '@grafana/data'; -import { PromContext, PromOptions } from './types'; -import { dateTime, LoadingState } from '@grafana/data'; -import { getBackendSrv, backendSrv } from 'app/core/services/__mocks__/backend_srv'; +import _ from 'lodash'; +import { + alignRange, + extractRuleMappingFromGroups, + PrometheusDatasource, + prometheusRegularEscape, + prometheusSpecialRegexEscape, +} from './datasource'; +import { + DataSourceInstanceSettings, + DataQueryResponseData, + DataQueryRequest, + dateTime, + LoadingState, +} from '@grafana/data'; +import { PromOptions, PromQuery, PromContext } from './types'; +import templateSrv from 'app/features/templating/template_srv'; +import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { CustomVariable } from 'app/features/templating/custom_variable'; +import * as Backend from 'app/core/services/backend_srv'; +import { BackendSrv } from '@grafana/runtime/src/services/backendSrv'; +jest.mock('./metric_find_query'); jest.mock('app/core/services/backend_srv'); - +jest.mock('app/features/templating/template_srv', () => { + return { + getAdhocFilters: jest.fn(() => [] as any[]), + replace: jest.fn((a: string) => a), + }; +}); jest.mock('app/features/dashboard/services/TimeSrv', () => ({ __esModule: true, getTimeSrv: jest.fn().mockReturnValue({ timeRange(): any { return { - from: dateTime(), - to: dateTime(), + from: dateTime(1531468681), + to: dateTime(1531489712), }; }, }), })); -jest.mock('app/features/templating/template_srv', () => { - return { - replace: jest.fn(() => null), - getAdhocFilters: jest.fn((): any[] => []), - }; -}); +const getBackendSrvMock = (Backend.getBackendSrv as any) as jest.Mock; +const getAdhocFiltersMock = (templateSrv.getAdhocFilters as any) as jest.Mock; +const replaceMock = (templateSrv.replace as any) as jest.Mock; +const getTimeSrvMock = (getTimeSrv as any) as jest.Mock; beforeEach(() => { - getBackendSrv.mockClear(); - for (const method in backendSrv) { - (backendSrv as any)[method].mockClear(); - } + getBackendSrvMock.mockClear(); + getAdhocFiltersMock.mockClear(); + replaceMock.mockClear(); + getTimeSrvMock.mockClear(); }); -const defaultInstanceSettings: DataSourceInstanceSettings = { - url: 'test_prom', - jsonData: {}, -} as any; +describe('PrometheusDatasource', () => { + let ds: PrometheusDatasource; + const instanceSettings = ({ + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: {} as any, + } as unknown) as DataSourceInstanceSettings; -describe('datasource', () => { - describe('query', () => { - let ds: PrometheusDatasource; - beforeEach(() => { - ds = new PrometheusDatasource(defaultInstanceSettings); - }); + beforeEach(() => { + ds = new PrometheusDatasource(instanceSettings); + }); + describe('Query', () => { it('returns empty array when no queries', done => { expect.assertions(2); @@ -100,6 +123,1640 @@ describe('datasource', () => { }); }); }); + + describe('Datasource metadata requests', () => { + const originalBackendMock = getBackendSrvMock(); + + afterAll(() => { + getBackendSrvMock.mockImplementation(() => originalBackendMock); + }); + + it('should perform a GET request with the default config', () => { + const datasourceRequestMock = jest.fn(); + getBackendSrvMock.mockImplementation( + () => + ({ + datasourceRequest: datasourceRequestMock, + } as any) + ); + + ds.metadataRequest('/foo'); + expect(datasourceRequestMock.mock.calls.length).toBe(1); + expect(datasourceRequestMock.mock.calls[0][0].method).toBe('GET'); + }); + + it('should still perform a GET request with the DS HTTP method set to POST', () => { + const datasourceRequestMock = jest.fn(); + getBackendSrvMock.mockImplementation( + () => + ({ + datasourceRequest: datasourceRequestMock, + } as any) + ); + const postSettings = _.cloneDeep(instanceSettings); + postSettings.jsonData.httpMethod = 'POST'; + const promDs = new PrometheusDatasource(postSettings); + promDs.metadataRequest('/foo'); + expect(datasourceRequestMock.mock.calls.length).toBe(1); + expect(datasourceRequestMock.mock.calls[0][0].method).toBe('GET'); + }); + }); + + describe('When using adhoc filters', () => { + const DEFAULT_QUERY_EXPRESSION = 'metric{job="foo"} - metric'; + const target = { expr: DEFAULT_QUERY_EXPRESSION }; + const originalAdhocFiltersMock = getAdhocFiltersMock(); + + afterAll(() => { + getAdhocFiltersMock.mockReturnValue(originalAdhocFiltersMock); + }); + + it('should not modify expression with no filters', () => { + const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); + expect(result).toMatchObject({ expr: DEFAULT_QUERY_EXPRESSION }); + }); + + it('should add filters to expression', () => { + getAdhocFiltersMock.mockReturnValue([ + { + key: 'k1', + operator: '=', + value: 'v1', + }, + { + key: 'k2', + operator: '!=', + value: 'v2', + }, + ]); + const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); + expect(result).toMatchObject({ expr: 'metric{job="foo",k1="v1",k2!="v2"} - metric{k1="v1",k2!="v2"}' }); + }); + + it('should add escaping if needed to regex filter expressions', () => { + getAdhocFiltersMock.mockReturnValue([ + { + key: 'k1', + operator: '=~', + value: 'v.*', + }, + { + key: 'k2', + operator: '=~', + value: `v'.*`, + }, + ]); + const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); + expect(result).toMatchObject({ + expr: `metric{job="foo",k1=~"v.*",k2=~"v\\\\'.*"} - metric{k1=~"v.*",k2=~"v\\\\'.*"}`, + }); + }); + }); + + describe('When performing performSuggestQuery', () => { + const originalBackendMock = getBackendSrvMock(); + + it('should cache response', async () => { + const datasourceRequestMock = Promise.resolve({ + status: 'success', + data: { data: ['value1', 'value2', 'value3'] }, + }); + + getBackendSrvMock.mockImplementation( + () => + ({ + datasourceRequest: () => datasourceRequestMock, + } as any) + ); + + let results = await ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + + getBackendSrvMock.mockImplementation(() => originalBackendMock); + results = await ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + }); + }); + + describe('When converting prometheus histogram to heatmap format', () => { + let query: any; + beforeEach(() => { + query = { + range: { from: dateTime(1443454528000), to: dateTime(1443454528000) }, + targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }], + interval: '1s', + }; + }); + + it('should convert cumullative histogram to ordinary', () => { + 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 } } }; + + const expected = [ + { + target: '10', + datapoints: [ + [10, 1443454528000], + [10, 1443454528000], + ], + }, + { + target: '20', + datapoints: [ + [10, 1443454528000], + [0, 1443454528000], + ], + }, + { + target: '30', + datapoints: [ + [5, 1443454528000], + [0, 1443454528000], + ], + }, + ]; + + ds.performTimeSeriesQuery = jest.fn().mockReturnValue([responseMock]); + ds.query(query).subscribe((result: any) => { + const results = result.data; + return expect(results).toMatchObject(expected); + }); + }); + + it('should sort series by label value', () => { + 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([responseMock]); + ds.query(query).subscribe((result: any) => { + const seriesLabels = _.map(result.data, 'target'); + return expect(seriesLabels).toEqual(expected); + }); + }); + }); + + describe('alignRange', () => { + it('does not modify already aligned intervals with perfect step', () => { + const range = alignRange(0, 3, 3, 0); + expect(range.start).toEqual(0); + expect(range.end).toEqual(3); + }); + + it('does modify end-aligned intervals to reflect number of steps possible', () => { + const range = alignRange(1, 6, 3, 0); + expect(range.start).toEqual(0); + expect(range.end).toEqual(6); + }); + + it('does align intervals that are a multiple of steps', () => { + const range = alignRange(1, 4, 3, 0); + expect(range.start).toEqual(0); + expect(range.end).toEqual(3); + }); + + it('does align intervals that are not a multiple of steps', () => { + const range = alignRange(1, 5, 3, 0); + expect(range.start).toEqual(0); + expect(range.end).toEqual(3); + }); + + it('does align intervals with local midnight -UTC offset', () => { + //week range, location 4+ hours UTC offset, 24h step time + const range = alignRange(4 * 60 * 60, (7 * 24 + 4) * 60 * 60, 24 * 60 * 60, -4 * 60 * 60); //04:00 UTC, 7 day range + expect(range.start).toEqual(4 * 60 * 60); + expect(range.end).toEqual((7 * 24 + 4) * 60 * 60); + }); + + it('does align intervals with local midnight +UTC offset', () => { + //week range, location 4- hours UTC offset, 24h step time + const range = alignRange(20 * 60 * 60, (8 * 24 - 4) * 60 * 60, 24 * 60 * 60, 4 * 60 * 60); //20:00 UTC on day1, 7 days later is 20:00 on day8 + expect(range.start).toEqual(20 * 60 * 60); + expect(range.end).toEqual((8 * 24 - 4) * 60 * 60); + }); + }); + + describe('extractRuleMappingFromGroups()', () => { + it('returns empty mapping for no rule groups', () => { + expect(extractRuleMappingFromGroups([])).toEqual({}); + }); + + it('returns a mapping for recording rules only', () => { + const groups = [ + { + rules: [ + { + name: 'HighRequestLatency', + query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5', + type: 'alerting', + }, + { + name: 'job:http_inprogress_requests:sum', + query: 'sum(http_inprogress_requests) by (job)', + type: 'recording', + }, + ], + file: '/rules.yaml', + interval: 60, + name: 'example', + }, + ]; + const mapping = extractRuleMappingFromGroups(groups); + expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' }); + }); + }); + + describe('Prometheus regular escaping', () => { + it('should not escape non-string', () => { + expect(prometheusRegularEscape(12)).toEqual(12); + }); + + it('should not escape simple string', () => { + expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression'); + }); + + it("should escape '", () => { + expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass"); + }); + + it('should escape multiple characters', () => { + expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'"); + }); + }); + + describe('Prometheus regexes escaping', () => { + it('should not escape simple string', () => { + expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression'); + }); + + it('should escape $^*+?.()|\\', () => { + expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass"); + expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass'); + expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass'); + expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass'); + expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass'); + expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass'); + expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass'); + expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass'); + expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass'); + expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass'); + expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass'); + expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass'); + expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass'); + expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass'); + expect(prometheusSpecialRegexEscape('looking|glass')).toEqual('looking\\\\|glass'); + }); + + it('should escape multiple special characters', () => { + expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?'); + }); + }); + + describe('When interpolating variables', () => { + let customVariable: CustomVariable; + beforeEach(() => { + customVariable = new CustomVariable({}, {} as any); + }); + + describe('and value is a string', () => { + it('should only escape single quotes', () => { + expect(ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", customVariable)).toEqual("abc\\\\'$^*{}[]+?.()|"); + }); + }); + + describe('and value is a number', () => { + it('should return a number', () => { + expect(ds.interpolateQueryExpr(1000 as any, customVariable)).toEqual(1000); + }); + }); + + describe('and variable allows multi-value', () => { + beforeEach(() => { + customVariable.multi = true; + }); + + it('should regex escape values if the value is a string', () => { + expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass'); + }); + + it('should return pipe separated values if the value is an array of strings', () => { + expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('a\\\\|bc|de\\\\|f'); + }); + }); + + describe('and variable allows all', () => { + beforeEach(() => { + customVariable.includeAll = true; + }); + + it('should regex escape values if the array is a string', () => { + expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass'); + }); + + it('should return pipe separated values if the value is an array of strings', () => { + expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('a\\\\|bc|de\\\\|f'); + }); + }); + }); + + describe('metricFindQuery', () => { + beforeEach(() => { + const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; + replaceMock.mockImplementation(jest.fn); + ds.metricFindQuery(query); + }); + + afterAll(() => { + replaceMock.mockImplementation((a: string) => a); + }); + + it('should call templateSrv.replace with scopedVars', () => { + expect(replaceMock.mock.calls[0][1]).toBeDefined(); + }); + + it('should have the correct range and range_ms', () => { + const range = replaceMock.mock.calls[0][1].__range; + const rangeMs = replaceMock.mock.calls[0][1].__range_ms; + const rangeS = replaceMock.mock.calls[0][1].__range_s; + expect(range).toEqual({ text: '21s', value: '21s' }); + expect(rangeMs).toEqual({ text: 21031, value: 21031 }); + expect(rangeS).toEqual({ text: 21, value: 21 }); + }); + + it('should pass the default interval value', () => { + const interval = replaceMock.mock.calls[0][1].__interval; + const intervalMs = replaceMock.mock.calls[0][1].__interval_ms; + expect(interval).toEqual({ text: '15s', value: '15s' }); + expect(intervalMs).toEqual({ text: 15000, value: 15000 }); + }); + }); +}); + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; + +const time = ({ hours = 0, seconds = 0, minutes = 0 }) => dateTime(hours * HOUR + minutes * MINUTE + seconds * SECOND); + +describe('PrometheusDatasource', () => { + const instanceSettings = ({ + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, + } as unknown) as DataSourceInstanceSettings; + + let ds: PrometheusDatasource; + beforeEach(() => { + ds = new PrometheusDatasource(instanceSettings); + }); + + describe('When querying prometheus with one target using query editor target spec', () => { + describe('and query syntax is valid', () => { + let results: any; + const query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + + // 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']], + }, + ], + }, + }, + }; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any).subscribe((data: any) => { + results = data; + }); + }); + + it('should generate the correct query', () => { + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('should return series list', async () => { + expect(results.data.length).toBe(1); + expect(results.data[0].target).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', + }; + + 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', () => { + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.reject(response)); + ds.query(query as any).subscribe((e: any) => { + results = e.message; + expect(results).toBe(`"${errMessage}"`); + }); + }); + }); + }); + + describe('When querying prometheus with one target which returns multiple series', () => { + let results: any; + 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', + }; + + 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']], + }, + ], + }, + }, + }; + + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + + ds.query(query as any).subscribe((data: any) => { + results = data; + }); + }); + + it('should be same length', () => { + expect(results.data.length).toBe(2); + expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); + expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); + }); + + it('should fill null until first datapoint in response', () => { + expect(results.data[0].datapoints[0][1]).toBe(start * 1000); + expect(results.data[0].datapoints[0][0]).toBe(null); + expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[0].datapoints[1][0]).toBe(3846); + }); + + it('should fill null after last datapoint in response', () => { + const length = (end - start) / step + 1; + expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); + expect(results.data[0].datapoints[length - 2][0]).toBe(3848); + expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); + expect(results.data[0].datapoints[length - 1][0]).toBe(null); + }); + + it('should fill null at gap between series', () => { + expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); + expect(results.data[0].datapoints[2][0]).toBe(null); + expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[1].datapoints[1][0]).toBe(null); + expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000); + expect(results.data[1].datapoints[3][0]).toBe(null); + }); + }); + + describe('When querying prometheus with one target and instant = true', () => { + let results: any; + const urlExpected = `proxied/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', + }; + + beforeEach(async () => { + const response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any).subscribe((data: any) => { + results = data; + }); + }); + + it('should generate the correct query', () => { + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('should return series list', () => { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + + describe('When performing annotationQuery', () => { + let results: any; + const options: any = { + annotation: { + expr: 'ALERTS{alertstate="firing"}', + tagKeys: 'job', + titleFormat: '{{alertname}}', + textFormat: '{{instance}}', + }, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + + const response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', + }, + values: [[123, '1']], + }, + ], + }, + }, + }; + + describe('when time series query is cancelled', () => { + it('should return empty results', async () => { + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve({ cancelled: true })); + + await ds.annotationQuery(options).then((data: any) => { + results = data; + }); + + expect(results).toEqual([]); + }); + }); + + describe('not use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = false; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + + await ds.annotationQuery(options).then((data: any) => { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results.length).toBe(1); + expect(results[0].tags).toContain('testjob'); + expect(results[0].title).toBe('InstanceDown'); + expect(results[0].text).toBe('testinstance'); + expect(results[0].time).toBe(123 * 1000); + }); + }); + + describe('use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = true; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + + await ds.annotationQuery(options).then((data: any) => { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results[0].time).toEqual(1); + }); + }); + + describe('step parameter', () => { + beforeEach(() => { + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + }); + + it('should use default step for short range if no interval is given', () => { + const query = { + ...options, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ds.annotationQuery(query); + const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(req.url).toContain('step=60'); + }); + + it('should use custom step for short range', () => { + const annotation = { + ...options.annotation, + step: '10s', + }; + const query = { + ...options, + annotation, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ds.annotationQuery(query); + const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(req.url).toContain('step=10'); + }); + + it('should use custom step for short range', () => { + const annotation = { + ...options.annotation, + step: '10s', + }; + const query = { + ...options, + annotation, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + ds.annotationQuery(query); + const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(req.url).toContain('step=10'); + }); + + it('should use dynamic step on long ranges if no option was given', () => { + const query = { + ...options, + range: { + from: time({ seconds: 63 }), + to: time({ hours: 24 * 30, seconds: 63 }), + }, + }; + ds.annotationQuery(query); + const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + // Range in seconds: (to - from) / 1000 + // Max_datapoints: 11000 + // Step: range / max_datapoints + const step = 236; + expect(req.url).toContain(`step=${step}`); + }); + }); + }); + + describe('When resultFormat is table and instant = true', () => { + let results: any; + const query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], + interval: '60s', + }; + + beforeEach(async () => { + const response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any).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', + }; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('step should never go below 1', async () => { + const query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [{ expr: 'test' }], + interval: '100ms', + }; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + const end = 7 * 60 * 60; + const start = 60 * 60; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + // times get rounded up to interval + const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + // times get aligned to interval + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', + }; + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'test', + intervalFactor: 10, + }, + ], + interval: '5s', + }; + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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'; + + templateSrv.replace = jest.fn(str => str); + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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'; + + templateSrv.replace = jest.fn(str => str); + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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'; + + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + intervalFactor: 10, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=' + + start + + '&end=' + + end + + '&step=60'; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.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 = '1h'; + const query = { + range: { + from: time({}), + to: time({ hours: 1 }), + }, + targets: [ + { + expr: 'test[${__range_s}s]', + }, + ], + interval: '60s', + }; + const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent( + query.targets[0].expr + )}&start=0&end=3600&step=60`; + + templateSrv.replace = jest.fn(str => str); + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any); + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.url).toBe(urlExpected); + + // @ts-ignore + expect(templateSrv.replace.mock.calls[1][1]).toEqual({ + __range_s: { + text: expectedRangeSecond, + value: expectedRangeSecond, + }, + __range: { + text: expectedRangeString, + value: expectedRangeString, + }, + __range_ms: { + text: expectedRangeSecond * 1000, + value: expectedRangeSecond * 1000, + }, + }); + }); + }); +}); + +describe('PrometheusDatasource for POST', () => { + // const ctx = new helpers.ServiceTestContext(); + const instanceSettings = ({ + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + } as unknown) as DataSourceInstanceSettings; + + let ds: PrometheusDatasource; + beforeEach(() => { + ds = new PrometheusDatasource(instanceSettings); + }); + + describe('When querying prometheus with one target using query editor target spec', () => { + let results: any; + 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', + }; + + beforeEach(async () => { + const response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[2 * 60, '3846']], + }, + ], + }, + }, + }; + getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + ds.query(query as any).subscribe((data: any) => { + results = data; + }); + }); + + it('should generate the correct query', () => { + const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + expect(res.method).toBe('POST'); + expect(res.url).toBe(urlExpected); + expect(res.data).toEqual(dataExpected); + }); + + it('should return series list', () => { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + + describe('When querying prometheus via check headers X-Dashboard-Id and X-Panel-Id', () => { + const options = { dashboardId: 1, panelId: 2 }; + const httpOptions = { + headers: {} as { [key: string]: number | undefined }, + }; + + it('with proxy access tracing headers should be added', () => { + ds._addTracingHeaders(httpOptions as any, options as any); + expect(httpOptions.headers['X-Dashboard-Id']).toBe(1); + expect(httpOptions.headers['X-Panel-Id']).toBe(2); + }); + + it('with direct access tracing headers should not be added', () => { + const mockDs = new PrometheusDatasource({ ...instanceSettings, url: 'http://127.0.0.1:8000' }); + mockDs._addTracingHeaders(httpOptions as any, options as any); + expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); + expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); + }); + }); +}); + +const getPrepareTargetsContext = (target: PromQuery) => { + const instanceSettings = ({ + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + } as unknown) as DataSourceInstanceSettings; + const start = 0; + const end = 1; + const panelId = '2'; + const options = ({ targets: [target], interval: '1s', panelId } as any) as DataQueryRequest; + + const ds = new PrometheusDatasource(instanceSettings); + 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', + context: PromContext.Panel, + }; + + const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); + + expect(queries.length).toBe(1); + expect(activeTargets.length).toBe(1); + expect(queries[0]).toEqual({ + end, + expr: 'up', + headers: { + 'X-Dashboard-Id': undefined, + 'X-Panel-Id': panelId, + }, + hinting: undefined, + instant: undefined, + refId: target.refId, + requestId: panelId + target.refId, + start, + step: 1, + }); + expect(activeTargets[0]).toEqual(target); + }); + }); + + describe('when run from Explore', () => { + describe('and both Graph and Table are shown', () => { + it('then it should return both instant and time series related objects', () => { + const target: PromQuery = { + refId: 'A', + expr: 'up', + context: PromContext.Explore, + showingGraph: true, + showingTable: true, + }; + + const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); + + expect(queries.length).toBe(2); + expect(activeTargets.length).toBe(2); + expect(queries[0]).toEqual({ + end, + expr: 'up', + headers: { + 'X-Dashboard-Id': undefined, + 'X-Panel-Id': panelId, + }, + hinting: undefined, + instant: true, + refId: target.refId, + requestId: panelId + target.refId + '_instant', + start, + step: 1, + }); + expect(activeTargets[0]).toEqual({ + ...target, + format: 'table', + instant: true, + requestId: panelId + target.refId + '_instant', + valueWithRefId: true, + }); + expect(queries[1]).toEqual({ + end, + expr: 'up', + headers: { + 'X-Dashboard-Id': undefined, + 'X-Panel-Id': panelId, + }, + hinting: undefined, + instant: false, + refId: target.refId, + requestId: panelId + target.refId, + start, + step: 1, + }); + expect(activeTargets[1]).toEqual({ + ...target, + format: 'time_series', + instant: false, + requestId: panelId + target.refId, + }); + }); + }); + + describe('and both Graph and Table are hidden', () => { + it('then it should return empty arrays', () => { + const target: PromQuery = { + refId: 'A', + expr: 'up', + context: PromContext.Explore, + showingGraph: false, + showingTable: false, + }; + + const { queries, activeTargets } = getPrepareTargetsContext(target); + + expect(queries.length).toBe(0); + expect(activeTargets.length).toBe(0); + }); + }); + + describe('and Graph is hidden', () => { + it('then it should return only intant related objects', () => { + const target: PromQuery = { + refId: 'A', + expr: 'up', + context: PromContext.Explore, + showingGraph: false, + showingTable: true, + }; + + const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); + + expect(queries.length).toBe(1); + expect(activeTargets.length).toBe(1); + expect(queries[0]).toEqual({ + end, + expr: 'up', + headers: { + 'X-Dashboard-Id': undefined, + 'X-Panel-Id': panelId, + }, + hinting: undefined, + instant: true, + refId: target.refId, + requestId: panelId + target.refId + '_instant', + start, + step: 1, + }); + expect(activeTargets[0]).toEqual({ + ...target, + format: 'table', + instant: true, + requestId: panelId + target.refId + '_instant', + valueWithRefId: true, + }); + }); + }); + + describe('and Table is hidden', () => { + it('then it should return only time series related objects', () => { + const target: PromQuery = { + refId: 'A', + expr: 'up', + context: PromContext.Explore, + showingGraph: true, + showingTable: false, + }; + + const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); + + expect(queries.length).toBe(1); + expect(activeTargets.length).toBe(1); + expect(queries[0]).toEqual({ + end, + expr: 'up', + headers: { + 'X-Dashboard-Id': undefined, + 'X-Panel-Id': panelId, + }, + hinting: undefined, + instant: false, + refId: target.refId, + requestId: panelId + target.refId, + start, + step: 1, + }); + expect(activeTargets[0]).toEqual({ + ...target, + format: 'time_series', + instant: false, + requestId: panelId + target.refId, + }); + }); + }); + }); }); function makeQuery(targets: any[]): any { diff --git a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts b/public/app/plugins/datasource/prometheus/language_utils.test.ts similarity index 97% rename from public/app/plugins/datasource/prometheus/specs/language_utils.test.ts rename to public/app/plugins/datasource/prometheus/language_utils.test.ts index b33c0094700..11d83f61efd 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.test.ts @@ -1,4 +1,4 @@ -import { expandRecordingRules, parseSelector } from '../language_utils'; +import { expandRecordingRules, parseSelector } from './language_utils'; describe('parseSelector()', () => { let parsed; diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts similarity index 98% rename from public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts rename to public/app/plugins/datasource/prometheus/metric_find_query.test.ts index 0324a9cd00d..9e53eada694 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts @@ -1,7 +1,7 @@ -import { PrometheusDatasource } from '../datasource'; -import PrometheusMetricFindQuery from '../metric_find_query'; +import { PrometheusDatasource } from './datasource'; +import PrometheusMetricFindQuery from './metric_find_query'; import { toUtc, DataSourceInstanceSettings } from '@grafana/data'; -import { PromOptions } from '../types'; +import { PromOptions } from './types'; import templateSrv from 'app/features/templating/template_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; diff --git a/public/app/plugins/datasource/prometheus/specs/module.test.ts b/public/app/plugins/datasource/prometheus/module.test.ts similarity index 80% rename from public/app/plugins/datasource/prometheus/specs/module.test.ts rename to public/app/plugins/datasource/prometheus/module.test.ts index 61cad3090b3..5ad08259639 100644 --- a/public/app/plugins/datasource/prometheus/specs/module.test.ts +++ b/public/app/plugins/datasource/prometheus/module.test.ts @@ -1,4 +1,4 @@ -import { plugin as PrometheusDatasourcePlugin } from '../module'; +import { plugin as PrometheusDatasourcePlugin } from './module'; describe('module', () => { it('should have metrics query field in panels and Explore', () => { diff --git a/public/app/plugins/datasource/prometheus/query_hints.test.ts b/public/app/plugins/datasource/prometheus/query_hints.test.ts index 2c1a22914a4..a59e252522f 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.test.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.test.ts @@ -1,6 +1,155 @@ -import { getQueryHints } from './query_hints'; +import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from './query_hints'; +////// +describe('getQueryHints()', () => { + it('returns no hints for no series', () => { + expect(getQueryHints('', [])).toEqual(null); + }); + + it('returns no hints for empty series', () => { + expect(getQueryHints('', [{ datapoints: [] }])).toEqual(null); + }); + + it('returns no hint for a monotonically decreasing series', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [22, 1001], + ], + }, + ]; + const hints = getQueryHints('metric', series); + expect(hints).toEqual(null); + }); + + it('returns no hint for a flat series', () => { + const series = [ + { + datapoints: [ + [null, 1000], + [23, 1001], + [null, 1002], + [23, 1003], + ], + }, + ]; + const hints = getQueryHints('metric', series); + expect(hints).toEqual(null); + }); + + it('returns a rate hint for a monotonically increasing series', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [24, 1001], + ], + }, + ]; + const hints = getQueryHints('metric', series); + + expect(hints!.length).toBe(1); + expect(hints![0]).toMatchObject({ + label: 'Time series is monotonically increasing.', + fix: { + action: { + type: 'ADD_RATE', + query: 'metric', + }, + }, + }); + }); + + it('returns no rate hint for a monotonically increasing series that already has a rate', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [24, 1001], + ], + }, + ]; + const hints = getQueryHints('rate(metric[1m])', series); + expect(hints).toEqual(null); + }); + + it('returns a rate hint w/o action for a complex monotonically increasing series', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [24, 1001], + ], + }, + ]; + const hints = getQueryHints('sum(metric)', series); + expect(hints!.length).toBe(1); + expect(hints![0].label).toContain('rate()'); + expect(hints![0].fix).toBeUndefined(); + }); + + it('returns a rate hint for a monotonically increasing series with missing data', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [null, 1001], + [24, 1002], + ], + }, + ]; + const hints = getQueryHints('metric', series); + expect(hints!.length).toBe(1); + expect(hints![0]).toMatchObject({ + label: 'Time series is monotonically increasing.', + fix: { + action: { + type: 'ADD_RATE', + query: 'metric', + }, + }, + }); + }); + + it('returns a histogram hint for a bucket series', () => { + const series = [{ datapoints: [[23, 1000]] }]; + const hints = getQueryHints('metric_bucket', series); + expect(hints!.length).toBe(1); + expect(hints![0]).toMatchObject({ + label: 'Time series has buckets, you probably wanted a histogram.', + fix: { + action: { + type: 'ADD_HISTOGRAM_QUANTILE', + query: 'metric_bucket', + }, + }, + }); + }); + + it('returns a sum hint when many time series results are returned for a simple metric', () => { + const seriesCount = SUM_HINT_THRESHOLD_COUNT; + const series = Array.from({ length: seriesCount }, _ => ({ + datapoints: [ + [0, 0], + [0, 0], + ], + })); + const hints = getQueryHints('metric', series); + expect(hints!.length).toBe(1); + expect(hints![0]).toMatchObject({ + type: 'ADD_SUM', + label: 'Many time series results returned.', + fix: { + label: 'Consider aggregating with sum().', + action: { + type: 'ADD_SUM', + query: 'metric', + preventSubmit: true, + }, + }, + }); + }); -describe('getQueryHints', () => { describe('when called without datapoints in series', () => { it('then it should use rows instead and return correct hint', () => { const series = [ diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts similarity index 99% rename from public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts rename to public/app/plugins/datasource/prometheus/result_transformer.test.ts index f92375ecfbe..35ad7c69966 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -1,4 +1,4 @@ -import { ResultTransformer } from '../result_transformer'; +import { ResultTransformer } from './result_transformer'; import { DataQueryResponseData } from '@grafana/data'; describe('Prometheus Result Transformer', () => { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts deleted file mode 100644 index 5df9e644a7a..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ /dev/null @@ -1,1696 +0,0 @@ -import _ from 'lodash'; -import { - alignRange, - extractRuleMappingFromGroups, - PrometheusDatasource, - prometheusRegularEscape, - prometheusSpecialRegexEscape, -} from '../datasource'; -import { DataSourceInstanceSettings, DataQueryResponseData, DataQueryRequest, dateTime } from '@grafana/data'; -import { PromOptions, PromQuery, PromContext } from '../types'; -import templateSrv from 'app/features/templating/template_srv'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { CustomVariable } from 'app/features/templating/custom_variable'; -import * as Backend from 'app/core/services/backend_srv'; -import { BackendSrv } from '@grafana/runtime/src/services/backendSrv'; - -jest.mock('../metric_find_query'); -jest.mock('app/core/services/backend_srv'); -jest.mock('app/features/templating/template_srv', () => { - return { - getAdhocFilters: jest.fn(() => [] as any[]), - replace: jest.fn((a: string) => a), - }; -}); -jest.mock('app/features/dashboard/services/TimeSrv', () => ({ - __esModule: true, - getTimeSrv: jest.fn().mockReturnValue({ - timeRange(): any { - return { - from: dateTime(1531468681), - to: dateTime(1531489712), - }; - }, - }), -})); - -const getBackendSrvMock = (Backend.getBackendSrv as any) as jest.Mock; -const getAdhocFiltersMock = (templateSrv.getAdhocFilters as any) as jest.Mock; -const replaceMock = (templateSrv.replace as any) as jest.Mock; -const getTimeSrvMock = (getTimeSrv as any) as jest.Mock; - -beforeEach(() => { - getBackendSrvMock.mockClear(); - getAdhocFiltersMock.mockClear(); - replaceMock.mockClear(); - getTimeSrvMock.mockClear(); -}); - -describe('PrometheusDatasource', () => { - let ds: PrometheusDatasource; - const instanceSettings = ({ - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: {} as any, - } as unknown) as DataSourceInstanceSettings; - - beforeEach(() => { - ds = new PrometheusDatasource(instanceSettings); - }); - - describe('Datasource metadata requests', () => { - const originalBackendMock = getBackendSrvMock(); - - afterAll(() => { - getBackendSrvMock.mockImplementation(() => originalBackendMock); - }); - - it('should perform a GET request with the default config', () => { - const datasourceRequestMock = jest.fn(); - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: datasourceRequestMock, - } as any) - ); - - ds.metadataRequest('/foo'); - expect(datasourceRequestMock.mock.calls.length).toBe(1); - expect(datasourceRequestMock.mock.calls[0][0].method).toBe('GET'); - }); - - it('should still perform a GET request with the DS HTTP method set to POST', () => { - const datasourceRequestMock = jest.fn(); - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: datasourceRequestMock, - } as any) - ); - const postSettings = _.cloneDeep(instanceSettings); - postSettings.jsonData.httpMethod = 'POST'; - const promDs = new PrometheusDatasource(postSettings); - promDs.metadataRequest('/foo'); - expect(datasourceRequestMock.mock.calls.length).toBe(1); - expect(datasourceRequestMock.mock.calls[0][0].method).toBe('GET'); - }); - }); - - describe('When using adhoc filters', () => { - const DEFAULT_QUERY_EXPRESSION = 'metric{job="foo"} - metric'; - const target = { expr: DEFAULT_QUERY_EXPRESSION }; - const originalAdhocFiltersMock = getAdhocFiltersMock(); - - afterAll(() => { - getAdhocFiltersMock.mockReturnValue(originalAdhocFiltersMock); - }); - - it('should not modify expression with no filters', () => { - const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); - expect(result).toMatchObject({ expr: DEFAULT_QUERY_EXPRESSION }); - }); - - it('should add filters to expression', () => { - getAdhocFiltersMock.mockReturnValue([ - { - key: 'k1', - operator: '=', - value: 'v1', - }, - { - key: 'k2', - operator: '!=', - value: 'v2', - }, - ]); - const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); - expect(result).toMatchObject({ expr: 'metric{job="foo",k1="v1",k2!="v2"} - metric{k1="v1",k2!="v2"}' }); - }); - - it('should add escaping if needed to regex filter expressions', () => { - getAdhocFiltersMock.mockReturnValue([ - { - key: 'k1', - operator: '=~', - value: 'v.*', - }, - { - key: 'k2', - operator: '=~', - value: `v'.*`, - }, - ]); - const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); - expect(result).toMatchObject({ - expr: `metric{job="foo",k1=~"v.*",k2=~"v\\\\'.*"} - metric{k1=~"v.*",k2=~"v\\\\'.*"}`, - }); - }); - }); - - describe('When performing performSuggestQuery', () => { - const originalBackendMock = getBackendSrvMock(); - - it('should cache response', async () => { - const datasourceRequestMock = Promise.resolve({ - status: 'success', - data: { data: ['value1', 'value2', 'value3'] }, - }); - - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: () => datasourceRequestMock, - } as any) - ); - - let results = await ds.performSuggestQuery('value', true); - - expect(results).toHaveLength(3); - - getBackendSrvMock.mockImplementation(() => originalBackendMock); - results = await ds.performSuggestQuery('value', true); - - expect(results).toHaveLength(3); - }); - }); - - describe('When converting prometheus histogram to heatmap format', () => { - let query: any; - beforeEach(() => { - query = { - range: { from: dateTime(1443454528000), to: dateTime(1443454528000) }, - targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }], - interval: '1s', - }; - }); - - it('should convert cumullative histogram to ordinary', () => { - 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 } } }; - - const expected = [ - { - target: '10', - datapoints: [ - [10, 1443454528000], - [10, 1443454528000], - ], - }, - { - target: '20', - datapoints: [ - [10, 1443454528000], - [0, 1443454528000], - ], - }, - { - target: '30', - datapoints: [ - [5, 1443454528000], - [0, 1443454528000], - ], - }, - ]; - - ds.performTimeSeriesQuery = jest.fn().mockReturnValue([responseMock]); - ds.query(query).subscribe((result: any) => { - const results = result.data; - return expect(results).toMatchObject(expected); - }); - }); - - it('should sort series by label value', () => { - 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([responseMock]); - ds.query(query).subscribe((result: any) => { - const seriesLabels = _.map(result.data, 'target'); - return expect(seriesLabels).toEqual(expected); - }); - }); - }); - - describe('alignRange', () => { - it('does not modify already aligned intervals with perfect step', () => { - const range = alignRange(0, 3, 3, 0); - expect(range.start).toEqual(0); - expect(range.end).toEqual(3); - }); - - it('does modify end-aligned intervals to reflect number of steps possible', () => { - const range = alignRange(1, 6, 3, 0); - expect(range.start).toEqual(0); - expect(range.end).toEqual(6); - }); - - it('does align intervals that are a multiple of steps', () => { - const range = alignRange(1, 4, 3, 0); - expect(range.start).toEqual(0); - expect(range.end).toEqual(3); - }); - - it('does align intervals that are not a multiple of steps', () => { - const range = alignRange(1, 5, 3, 0); - expect(range.start).toEqual(0); - expect(range.end).toEqual(3); - }); - - it('does align intervals with local midnight -UTC offset', () => { - //week range, location 4+ hours UTC offset, 24h step time - const range = alignRange(4 * 60 * 60, (7 * 24 + 4) * 60 * 60, 24 * 60 * 60, -4 * 60 * 60); //04:00 UTC, 7 day range - expect(range.start).toEqual(4 * 60 * 60); - expect(range.end).toEqual((7 * 24 + 4) * 60 * 60); - }); - - it('does align intervals with local midnight +UTC offset', () => { - //week range, location 4- hours UTC offset, 24h step time - const range = alignRange(20 * 60 * 60, (8 * 24 - 4) * 60 * 60, 24 * 60 * 60, 4 * 60 * 60); //20:00 UTC on day1, 7 days later is 20:00 on day8 - expect(range.start).toEqual(20 * 60 * 60); - expect(range.end).toEqual((8 * 24 - 4) * 60 * 60); - }); - }); - - describe('extractRuleMappingFromGroups()', () => { - it('returns empty mapping for no rule groups', () => { - expect(extractRuleMappingFromGroups([])).toEqual({}); - }); - - it('returns a mapping for recording rules only', () => { - const groups = [ - { - rules: [ - { - name: 'HighRequestLatency', - query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5', - type: 'alerting', - }, - { - name: 'job:http_inprogress_requests:sum', - query: 'sum(http_inprogress_requests) by (job)', - type: 'recording', - }, - ], - file: '/rules.yaml', - interval: 60, - name: 'example', - }, - ]; - const mapping = extractRuleMappingFromGroups(groups); - expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' }); - }); - }); - - describe('Prometheus regular escaping', () => { - it('should not escape non-string', () => { - expect(prometheusRegularEscape(12)).toEqual(12); - }); - - it('should not escape simple string', () => { - expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression'); - }); - - it("should escape '", () => { - expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass"); - }); - - it('should escape multiple characters', () => { - expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'"); - }); - }); - - describe('Prometheus regexes escaping', () => { - it('should not escape simple string', () => { - expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression'); - }); - - it('should escape $^*+?.()|\\', () => { - expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass"); - expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass'); - expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass'); - expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass'); - expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass'); - expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass'); - expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass'); - expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass'); - expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass'); - expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass'); - expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass'); - expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass'); - expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass'); - expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass'); - expect(prometheusSpecialRegexEscape('looking|glass')).toEqual('looking\\\\|glass'); - }); - - it('should escape multiple special characters', () => { - expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?'); - }); - }); - - describe('When interpolating variables', () => { - let customVariable: CustomVariable; - beforeEach(() => { - customVariable = new CustomVariable({}, {} as any); - }); - - describe('and value is a string', () => { - it('should only escape single quotes', () => { - expect(ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", customVariable)).toEqual("abc\\\\'$^*{}[]+?.()|"); - }); - }); - - describe('and value is a number', () => { - it('should return a number', () => { - expect(ds.interpolateQueryExpr(1000 as any, customVariable)).toEqual(1000); - }); - }); - - describe('and variable allows multi-value', () => { - beforeEach(() => { - customVariable.multi = true; - }); - - it('should regex escape values if the value is a string', () => { - expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass'); - }); - - it('should return pipe separated values if the value is an array of strings', () => { - expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('a\\\\|bc|de\\\\|f'); - }); - }); - - describe('and variable allows all', () => { - beforeEach(() => { - customVariable.includeAll = true; - }); - - it('should regex escape values if the array is a string', () => { - expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass'); - }); - - it('should return pipe separated values if the value is an array of strings', () => { - expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('a\\\\|bc|de\\\\|f'); - }); - }); - }); - - describe('metricFindQuery', () => { - beforeEach(() => { - const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; - replaceMock.mockImplementation(jest.fn); - ds.metricFindQuery(query); - }); - - afterAll(() => { - replaceMock.mockImplementation((a: string) => a); - }); - - it('should call templateSrv.replace with scopedVars', () => { - expect(replaceMock.mock.calls[0][1]).toBeDefined(); - }); - - it('should have the correct range and range_ms', () => { - const range = replaceMock.mock.calls[0][1].__range; - const rangeMs = replaceMock.mock.calls[0][1].__range_ms; - const rangeS = replaceMock.mock.calls[0][1].__range_s; - expect(range).toEqual({ text: '21s', value: '21s' }); - expect(rangeMs).toEqual({ text: 21031, value: 21031 }); - expect(rangeS).toEqual({ text: 21, value: 21 }); - }); - - it('should pass the default interval value', () => { - const interval = replaceMock.mock.calls[0][1].__interval; - const intervalMs = replaceMock.mock.calls[0][1].__interval_ms; - expect(interval).toEqual({ text: '15s', value: '15s' }); - expect(intervalMs).toEqual({ text: 15000, value: 15000 }); - }); - }); -}); - -const SECOND = 1000; -const MINUTE = 60 * SECOND; -const HOUR = 60 * MINUTE; - -const time = ({ hours = 0, seconds = 0, minutes = 0 }) => dateTime(hours * HOUR + minutes * MINUTE + seconds * SECOND); - -describe('PrometheusDatasource', () => { - const instanceSettings = ({ - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, - } as unknown) as DataSourceInstanceSettings; - - let ds: PrometheusDatasource; - beforeEach(() => { - ds = new PrometheusDatasource(instanceSettings); - }); - - describe('When querying prometheus with one target using query editor target spec', () => { - describe('and query syntax is valid', () => { - let results: any; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - - // 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']], - }, - ], - }, - }, - }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any).subscribe((data: any) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should return series list', async () => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).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', - }; - - 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', () => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.reject(response)); - ds.query(query as any).subscribe((e: any) => { - results = e.message; - expect(results).toBe(`"${errMessage}"`); - }); - }); - }); - }); - - describe('When querying prometheus with one target which returns multiple series', () => { - let results: any; - 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', - }; - - 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']], - }, - ], - }, - }, - }; - - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - - ds.query(query as any).subscribe((data: any) => { - results = data; - }); - }); - - it('should be same length', () => { - expect(results.data.length).toBe(2); - expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); - expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); - }); - - it('should fill null until first datapoint in response', () => { - expect(results.data[0].datapoints[0][1]).toBe(start * 1000); - expect(results.data[0].datapoints[0][0]).toBe(null); - expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); - expect(results.data[0].datapoints[1][0]).toBe(3846); - }); - - it('should fill null after last datapoint in response', () => { - const length = (end - start) / step + 1; - expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); - expect(results.data[0].datapoints[length - 2][0]).toBe(3848); - expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); - expect(results.data[0].datapoints[length - 1][0]).toBe(null); - }); - - it('should fill null at gap between series', () => { - expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); - expect(results.data[0].datapoints[2][0]).toBe(null); - expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); - expect(results.data[1].datapoints[1][0]).toBe(null); - expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000); - expect(results.data[1].datapoints[3][0]).toBe(null); - }); - }); - - describe('When querying prometheus with one target and instant = true', () => { - let results: any; - const urlExpected = `proxied/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', - }; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any).subscribe((data: any) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should return series list', () => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); - }); - }); - - describe('When performing annotationQuery', () => { - let results: any; - const options: any = { - annotation: { - expr: 'ALERTS{alertstate="firing"}', - tagKeys: 'job', - titleFormat: '{{alertname}}', - textFormat: '{{instance}}', - }, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], - }, - ], - }, - }, - }; - - describe('when time series query is cancelled', () => { - it('should return empty results', async () => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve({ cancelled: true })); - - await ds.annotationQuery(options).then((data: any) => { - results = data; - }); - - expect(results).toEqual([]); - }); - }); - - describe('not use useValueForTime', () => { - beforeEach(async () => { - options.annotation.useValueForTime = false; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - - await ds.annotationQuery(options).then((data: any) => { - results = data; - }); - }); - - it('should return annotation list', () => { - expect(results.length).toBe(1); - expect(results[0].tags).toContain('testjob'); - expect(results[0].title).toBe('InstanceDown'); - expect(results[0].text).toBe('testinstance'); - expect(results[0].time).toBe(123 * 1000); - }); - }); - - describe('use useValueForTime', () => { - beforeEach(async () => { - options.annotation.useValueForTime = true; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - - await ds.annotationQuery(options).then((data: any) => { - results = data; - }); - }); - - it('should return annotation list', () => { - expect(results[0].time).toEqual(1); - }); - }); - - describe('step parameter', () => { - beforeEach(() => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - }); - - it('should use default step for short range if no interval is given', () => { - const query = { - ...options, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(req.url).toContain('step=60'); - }); - - it('should use custom step for short range', () => { - const annotation = { - ...options.annotation, - step: '10s', - }; - const query = { - ...options, - annotation, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(req.url).toContain('step=10'); - }); - - it('should use custom step for short range', () => { - const annotation = { - ...options.annotation, - step: '10s', - }; - const query = { - ...options, - annotation, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(req.url).toContain('step=10'); - }); - - it('should use dynamic step on long ranges if no option was given', () => { - const query = { - ...options, - range: { - from: time({ seconds: 63 }), - to: time({ hours: 24 * 30, seconds: 63 }), - }, - }; - ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - // Range in seconds: (to - from) / 1000 - // Max_datapoints: 11000 - // Step: range / max_datapoints - const step = 236; - expect(req.url).toContain(`step=${step}`); - }); - }); - }); - - describe('When resultFormat is table and instant = true', () => { - let results: any; - const query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - }; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any).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', - }; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('step should never go below 1', async () => { - const query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [{ expr: 'test' }], - interval: '100ms', - }; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - const end = 7 * 60 * 60; - const start = 60 * 60; - const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - // times get rounded up to interval - const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - // times get aligned to interval - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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', - }; - const end = 7 * 24 * 60 * 60; - const start = 0; - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '5s', - }; - const end = 7 * 24 * 60 * 60; - const start = 0; - const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).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'; - - templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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'; - - templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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'; - - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - const end = 7 * 24 * 60 * 60; - const start = 0; - const urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=' + - start + - '&end=' + - end + - '&step=60'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.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 = '1h'; - const query = { - range: { - from: time({}), - to: time({ hours: 1 }), - }, - targets: [ - { - expr: 'test[${__range_s}s]', - }, - ], - interval: '60s', - }; - const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent( - query.targets[0].expr - )}&start=0&end=3600&step=60`; - - templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.url).toBe(urlExpected); - - // @ts-ignore - expect(templateSrv.replace.mock.calls[1][1]).toEqual({ - __range_s: { - text: expectedRangeSecond, - value: expectedRangeSecond, - }, - __range: { - text: expectedRangeString, - value: expectedRangeString, - }, - __range_ms: { - text: expectedRangeSecond * 1000, - value: expectedRangeSecond * 1000, - }, - }); - }); - }); -}); - -describe('PrometheusDatasource for POST', () => { - // const ctx = new helpers.ServiceTestContext(); - const instanceSettings = ({ - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - } as unknown) as DataSourceInstanceSettings; - - let ds: PrometheusDatasource; - beforeEach(() => { - ds = new PrometheusDatasource(instanceSettings); - }); - - describe('When querying prometheus with one target using query editor target spec', () => { - let results: any; - 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', - }; - - beforeEach(async () => { - const response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[2 * 60, '3846']], - }, - ], - }, - }, - }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); - ds.query(query as any).subscribe((data: any) => { - results = data; - }); - }); - - it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; - expect(res.method).toBe('POST'); - expect(res.url).toBe(urlExpected); - expect(res.data).toEqual(dataExpected); - }); - - it('should return series list', () => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); - }); - }); - - describe('When querying prometheus via check headers X-Dashboard-Id and X-Panel-Id', () => { - const options = { dashboardId: 1, panelId: 2 }; - const httpOptions = { - headers: {} as { [key: string]: number | undefined }, - }; - - it('with proxy access tracing headers should be added', () => { - ds._addTracingHeaders(httpOptions as any, options as any); - expect(httpOptions.headers['X-Dashboard-Id']).toBe(1); - expect(httpOptions.headers['X-Panel-Id']).toBe(2); - }); - - it('with direct access tracing headers should not be added', () => { - const mockDs = new PrometheusDatasource({ ...instanceSettings, url: 'http://127.0.0.1:8000' }); - mockDs._addTracingHeaders(httpOptions as any, options as any); - expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); - expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); - }); - }); -}); - -const getPrepareTargetsContext = (target: PromQuery) => { - const instanceSettings = ({ - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - } as unknown) as DataSourceInstanceSettings; - const start = 0; - const end = 1; - const panelId = '2'; - const options = ({ targets: [target], interval: '1s', panelId } as any) as DataQueryRequest; - - const ds = new PrometheusDatasource(instanceSettings); - 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', - context: PromContext.Panel, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: undefined, - refId: target.refId, - requestId: panelId + target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual(target); - }); - }); - - describe('when run from Explore', () => { - describe('and both Graph and Table are shown', () => { - it('then it should return both instant and time series related objects', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - context: PromContext.Explore, - showingGraph: true, - showingTable: true, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); - - expect(queries.length).toBe(2); - expect(activeTargets.length).toBe(2); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: true, - refId: target.refId, - requestId: panelId + target.refId + '_instant', - start, - step: 1, - }); - expect(activeTargets[0]).toEqual({ - ...target, - format: 'table', - instant: true, - requestId: panelId + target.refId + '_instant', - valueWithRefId: true, - }); - expect(queries[1]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: false, - refId: target.refId, - requestId: panelId + target.refId, - start, - step: 1, - }); - expect(activeTargets[1]).toEqual({ - ...target, - format: 'time_series', - instant: false, - requestId: panelId + target.refId, - }); - }); - }); - - describe('and both Graph and Table are hidden', () => { - it('then it should return empty arrays', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - context: PromContext.Explore, - showingGraph: false, - showingTable: false, - }; - - const { queries, activeTargets } = getPrepareTargetsContext(target); - - expect(queries.length).toBe(0); - expect(activeTargets.length).toBe(0); - }); - }); - - describe('and Graph is hidden', () => { - it('then it should return only intant related objects', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - context: PromContext.Explore, - showingGraph: false, - showingTable: true, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: true, - refId: target.refId, - requestId: panelId + target.refId + '_instant', - start, - step: 1, - }); - expect(activeTargets[0]).toEqual({ - ...target, - format: 'table', - instant: true, - requestId: panelId + target.refId + '_instant', - valueWithRefId: true, - }); - }); - }); - - describe('and Table is hidden', () => { - it('then it should return only time series related objects', () => { - const target: PromQuery = { - refId: 'A', - expr: 'up', - context: PromContext.Explore, - showingGraph: true, - showingTable: false, - }; - - const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); - - expect(queries.length).toBe(1); - expect(activeTargets.length).toBe(1); - expect(queries[0]).toEqual({ - end, - expr: 'up', - headers: { - 'X-Dashboard-Id': undefined, - 'X-Panel-Id': panelId, - }, - hinting: undefined, - instant: false, - refId: target.refId, - requestId: panelId + target.refId, - start, - step: 1, - }); - expect(activeTargets[0]).toEqual({ - ...target, - format: 'time_series', - instant: false, - requestId: panelId + target.refId, - }); - }); - }); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts b/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts deleted file mode 100644 index ff1bbe070c6..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from '../query_hints'; - -describe('getQueryHints()', () => { - it('returns no hints for no series', () => { - expect(getQueryHints('', [])).toEqual(null); - }); - - it('returns no hints for empty series', () => { - expect(getQueryHints('', [{ datapoints: [] }])).toEqual(null); - }); - - it('returns no hint for a monotonically decreasing series', () => { - const series = [ - { - datapoints: [ - [23, 1000], - [22, 1001], - ], - }, - ]; - const hints = getQueryHints('metric', series); - expect(hints).toEqual(null); - }); - - it('returns no hint for a flat series', () => { - const series = [ - { - datapoints: [ - [null, 1000], - [23, 1001], - [null, 1002], - [23, 1003], - ], - }, - ]; - const hints = getQueryHints('metric', series); - expect(hints).toEqual(null); - }); - - it('returns a rate hint for a monotonically increasing series', () => { - const series = [ - { - datapoints: [ - [23, 1000], - [24, 1001], - ], - }, - ]; - const hints = getQueryHints('metric', series); - - expect(hints!.length).toBe(1); - expect(hints![0]).toMatchObject({ - label: 'Time series is monotonically increasing.', - fix: { - action: { - type: 'ADD_RATE', - query: 'metric', - }, - }, - }); - }); - - it('returns no rate hint for a monotonically increasing series that already has a rate', () => { - const series = [ - { - datapoints: [ - [23, 1000], - [24, 1001], - ], - }, - ]; - const hints = getQueryHints('rate(metric[1m])', series); - expect(hints).toEqual(null); - }); - - it('returns a rate hint w/o action for a complex monotonically increasing series', () => { - const series = [ - { - datapoints: [ - [23, 1000], - [24, 1001], - ], - }, - ]; - const hints = getQueryHints('sum(metric)', series); - expect(hints!.length).toBe(1); - expect(hints![0].label).toContain('rate()'); - expect(hints![0].fix).toBeUndefined(); - }); - - it('returns a rate hint for a monotonically increasing series with missing data', () => { - const series = [ - { - datapoints: [ - [23, 1000], - [null, 1001], - [24, 1002], - ], - }, - ]; - const hints = getQueryHints('metric', series); - expect(hints!.length).toBe(1); - expect(hints![0]).toMatchObject({ - label: 'Time series is monotonically increasing.', - fix: { - action: { - type: 'ADD_RATE', - query: 'metric', - }, - }, - }); - }); - - it('returns a histogram hint for a bucket series', () => { - const series = [{ datapoints: [[23, 1000]] }]; - const hints = getQueryHints('metric_bucket', series); - expect(hints!.length).toBe(1); - expect(hints![0]).toMatchObject({ - label: 'Time series has buckets, you probably wanted a histogram.', - fix: { - action: { - type: 'ADD_HISTOGRAM_QUANTILE', - query: 'metric_bucket', - }, - }, - }); - }); - - it('returns a sum hint when many time series results are returned for a simple metric', () => { - const seriesCount = SUM_HINT_THRESHOLD_COUNT; - const series = Array.from({ length: seriesCount }, _ => ({ - datapoints: [ - [0, 0], - [0, 0], - ], - })); - const hints = getQueryHints('metric', series); - expect(hints!.length).toBe(1); - expect(hints![0]).toMatchObject({ - type: 'ADD_SUM', - label: 'Many time series results returned.', - fix: { - label: 'Consider aggregating with sum().', - action: { - type: 'ADD_SUM', - query: 'metric', - preventSubmit: true, - }, - }, - }); - }); -});