import { map } from 'lodash'; import { Observable, of, throwError } from 'rxjs'; import { ArrayVector, CoreApp, DataLink, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, dateMath, DateTime, dateTime, Field, FieldType, MutableDataFrame, RawTimeRange, SupplementaryQueryType, TimeRange, toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, reportInteraction } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { createFetchResponse } from '../../../../test/helpers/createFetchResponse'; import { ElasticDatasource, enhanceDataFrame } from './datasource'; import { createElasticDatasource } from './mocks'; import { Filters, ElasticsearchOptions, ElasticsearchQuery } from './types'; const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, reportInteraction: jest.fn(), getDataSourceSrv: () => { return { getInstanceSettings: () => { return { name: 'elastic25' }; }, }; }, })); const TIMESRV_START = [2022, 8, 21, 6, 10, 10]; const TIMESRV_END = [2022, 8, 24, 6, 10, 21]; const DATAQUERY_BASE = { requestId: '1', interval: '', intervalMs: 0, scopedVars: { test: { text: '', value: '' }, }, timezone: '', app: 'test', startTime: 0, }; jest.mock('app/features/dashboard/services/TimeSrv', () => ({ ...jest.requireActual('app/features/dashboard/services/TimeSrv'), getTimeSrv: () => ({ timeRange: () => createTimeRange(toUtc(TIMESRV_START), toUtc(TIMESRV_END)), }), })); const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ from, to, raw: { from, to, }, }); interface TestContext { data?: Data; from?: string; jsonData?: Partial; database?: string; fetchMockImplementation?: (options: BackendSrvRequest) => Observable; templateSrvMock?: TemplateSrv; } interface Data { [key: string]: undefined | string | string[] | number | Data | Data[]; } function getTestContext({ data = { responses: [] }, from = 'now-5m', jsonData, database = '[test-]YYYY.MM.DD', fetchMockImplementation, templateSrvMock, }: TestContext = {}) { const defaultMock = (options: BackendSrvRequest) => of(createFetchResponse(data)); const fetchMock = jest.spyOn(backendSrv, 'fetch'); fetchMock.mockImplementation(fetchMockImplementation ?? defaultMock); const timeSrv = { time: { from, to: 'now' }, timeRange: () => ({ from: dateMath.parse(timeSrv.time.from, false), to: dateMath.parse(timeSrv.time.to, true), }), setTime: (time: RawTimeRange) => { timeSrv.time = time; }, } as TimeSrv; const settings: Partial> = { url: ELASTICSEARCH_MOCK_URL }; settings.jsonData = jsonData as ElasticsearchOptions; const templateSrv = templateSrvMock ?? ({ replace: (text?: string) => { if (text?.startsWith('$')) { return `resolvedVariable`; } else { return text; } }, getAdhocFilters: () => [], } as unknown as TemplateSrv); const ds = createElasticDatasource(settings, templateSrv); return { timeSrv, ds, fetchMock }; } describe('ElasticDatasource', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('When calling getTagValues', () => { it('should respect the currently selected time range', () => { const data = { responses: [ { aggregations: { '1': { buckets: [ { doc_count: 10, key: 'val1', }, { doc_count: 20, key: 'val2', }, { doc_count: 30, key: 'val3', }, ], }, }, }, ], }; const { ds, fetchMock } = getTestContext({ data, jsonData: { interval: 'Daily', timeField: '@timestamp' }, }); ds.getTagValues({ key: 'test' }); expect(fetchMock).toHaveBeenCalledTimes(1); const obj = JSON.parse(fetchMock.mock.calls[0][0].data.split('\n')[1]); const { lte, gte } = obj.query.bool.filter[0].range['@timestamp']; expect(gte).toBe('1663740610000'); // 2022-09-21T06:10:10Z expect(lte).toBe('1663999821000'); // 2022-09-24T06:10:21Z }); }); describe('When testing datasource with index pattern', () => { it('should translate index pattern to current day', async () => { const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily' } }); await ds.testDatasource(); const today = toUtc().format('YYYY.MM.DD'); const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]; expect(lastCall[0].url).toBe(`${ELASTICSEARCH_MOCK_URL}/test-${today}/_mapping`); }); }); describe('When issuing metric query with interval pattern', () => { async function runScenario() { const range = { from: toUtc([2015, 4, 30, 10]), to: toUtc([2015, 5, 1, 10]), raw: { from: '', to: '' } }; const targets: ElasticsearchQuery[] = [ { refId: 'test', alias: '$varAlias', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], metrics: [{ type: 'count', id: '1' }], query: 'escape\\:test', }, ]; const query = { ...DATAQUERY_BASE, range, targets }; const data = { responses: [ { aggregations: { '1': { buckets: [ { doc_count: 10, key: 1000, }, ], }, }, }, ], }; const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily' }, data }); let result: DataQueryResponse = { data: [] }; await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual({ data: [ { name: 'resolvedVariable', refId: 'test', fields: [ { name: 'Time', type: FieldType.time, config: {}, values: new ArrayVector([1000]), }, { name: 'Value', type: FieldType.number, config: {}, values: new ArrayVector([10]), }, ], length: 1, }, ], }); result = received[0]; }); expect(fetchMock).toHaveBeenCalledTimes(1); const requestOptions = fetchMock.mock.calls[0][0]; const parts = requestOptions.data.split('\n'); const header = JSON.parse(parts[0]); const body = JSON.parse(parts[1]); return { result, body, header, query }; } it('should translate index pattern to current day', async () => { const { header } = await runScenario(); expect(header.index).toEqual(['test-2015.05.30', 'test-2015.05.31', 'test-2015.06.01']); }); it('should not resolve the variable in the original alias field in the query', async () => { const { query } = await runScenario(); expect(query.targets[0].alias).toEqual('$varAlias'); }); it('should resolve the alias variable for the alias/target in the result', async () => { const { result } = await runScenario(); expect(result.data[0].name).toEqual('resolvedVariable'); }); it('should json escape lucene query', async () => { const { body } = await runScenario(); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); }); it('should report query interaction', async () => { await runScenario(); expect(reportInteraction).toHaveBeenCalledWith( 'grafana_elasticsearch_query_executed', expect.objectContaining({ alias: '$varAlias', app: 'test', has_data: true, has_error: false, line_limit: undefined, query_type: 'metric', simultaneously_sent_query_count: 1, with_lucene_query: true, }) ); }); }); describe('When issuing logs query with interval pattern', () => { async function setupDataSource(jsonData?: Partial) { jsonData = { interval: 'Daily', timeField: '@timestamp', ...(jsonData || {}), }; const { ds } = getTestContext({ jsonData, data: logsResponse.data, database: 'mock-index', }); const query = { range: createTimeRange(toUtc([2015, 4, 30, 10]), toUtc([2019, 7, 1, 10])), targets: [ { alias: '$varAlias', refId: 'A', bucketAggs: [ { type: 'date_histogram', settings: { interval: 'auto' }, id: '2', }, ], metrics: [{ type: 'logs', id: '1' }], query: 'escape\\:test', timeField: '@timestamp', }, ], } as unknown as DataQueryRequest; const queryBuilderSpy = jest.spyOn(ds.queryBuilder, 'getLogsQuery'); let response: DataQueryResponse = { data: [] }; await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); response = received[0]; }); return { queryBuilderSpy, response }; } it('should call getLogsQuery()', async () => { const { queryBuilderSpy } = await setupDataSource(); expect(queryBuilderSpy).toHaveBeenCalled(); }); it('should enhance fields with links', async () => { const { response } = await setupDataSource({ dataLinks: [ { field: 'host', url: 'http://localhost:3000/${__value.raw}', urlDisplayLabel: 'Custom Label', }, ], }); expect(response.data.length).toBe(1); const links: DataLink[] = response.data[0].fields.find((field: Field) => field.name === 'host').config.links; expect(links.length).toBe(1); expect(links[0].url).toBe('http://localhost:3000/${__value.raw}'); expect(links[0].title).toBe('Custom Label'); }); it('should report query interaction', async () => { await setupDataSource(); expect(reportInteraction).toHaveBeenCalledWith( 'grafana_elasticsearch_query_executed', expect.objectContaining({ alias: '$varAlias', app: undefined, has_data: true, has_error: false, line_limit: undefined, query_type: 'logs', simultaneously_sent_query_count: 1, with_lucene_query: true, }) ); }); }); describe('When issuing document query', () => { async function runScenario() { const range = createTimeRange(dateTime([2015, 4, 30, 10]), dateTime([2015, 5, 1, 10])); const targets: ElasticsearchQuery[] = [ { refId: 'A', metrics: [{ type: 'raw_document', id: '1' }], query: 'test' }, ]; const query = { ...DATAQUERY_BASE, range, targets }; const data = { responses: [] }; const { ds, fetchMock } = getTestContext({ data, database: 'test' }); await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual({ data: [] }); }); expect(fetchMock).toHaveBeenCalledTimes(1); const requestOptions = fetchMock.mock.calls[0][0]; const parts = requestOptions.data.split('\n'); const header = JSON.parse(parts[0]); const body = JSON.parse(parts[1]); return { body, header }; } it('should set search type to query_then_fetch', async () => { const { header } = await runScenario(); expect(header.search_type).toEqual('query_then_fetch'); }); it('should set size', async () => { const { body } = await runScenario(); expect(body.size).toBe(500); }); it('should report query interaction', async () => { await runScenario(); expect(reportInteraction).toHaveBeenCalledWith( 'grafana_elasticsearch_query_executed', expect.objectContaining({ alias: undefined, app: 'test', has_data: false, has_error: false, line_limit: undefined, query_type: 'raw_document', simultaneously_sent_query_count: 1, with_lucene_query: true, }) ); }); }); describe('When getting an error on response', () => { const query: DataQueryRequest = { range: createTimeRange(toUtc([2020, 1, 1, 10]), toUtc([2020, 2, 1, 10])), targets: [ { refId: 'A', alias: '$varAlias', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], metrics: [{ type: 'count', id: '1' }], query: 'escape\\:test', }, ], } as DataQueryRequest; it('should process it properly', async () => { const { ds } = getTestContext({ jsonData: { interval: 'Daily' }, data: { took: 1, responses: [ { error: { reason: 'all shards failed', }, status: 400, }, ], }, }); const errObject = { data: '{\n "reason": "all shards failed"\n}', message: 'all shards failed', config: { url: 'http://localhost:3000/api/ds/query', }, }; await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual(errObject); }); }); it('should properly throw an error with just a message', async () => { const response: FetchResponse = { data: { error: 'Bad Request', message: 'Authentication to data source failed', }, status: 400, url: 'http://localhost:3000/api/ds/query', config: { url: 'http://localhost:3000/api/ds/query' }, type: 'basic', statusText: 'Bad Request', redirected: false, headers: {} as unknown as Headers, ok: false, }; const { ds } = getTestContext({ fetchMockImplementation: () => throwError(response), from: undefined, }); const errObject = { error: 'Bad Request', message: 'Elasticsearch error: Authentication to data source failed', }; await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual(errObject); }); }); it('should properly throw an unknown error', async () => { const { ds } = getTestContext({ jsonData: { interval: 'Daily' }, data: { took: 1, responses: [ { error: {}, status: 400, }, ], }, }); const errObject = { data: '{}', message: 'Unknown elastic error response', config: { url: 'http://localhost:3000/api/ds/query', }, }; await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual(errObject); }); }); }); describe('When getting field mappings on indices with gaps', () => { const basicResponse = { metricbeat: { mappings: { metricsets: { _all: {}, properties: { '@timestamp': { type: 'date' }, beat: { properties: { hostname: { type: 'string' }, }, }, }, }, }, }, }; it('should not retry when ES is down', async () => { const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); const { ds, timeSrv, fetchMock } = getTestContext({ from: 'now-2w', jsonData: { interval: 'Daily' }, fetchMockImplementation: (options) => { if (options.url === `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`) { return of(createFetchResponse(basicResponse)); } return throwError({ status: 500 }); }, }); const range = timeSrv.timeRange(); await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual({ status: 500 }); expect(fetchMock).toBeCalledTimes(1); }); }); it('should not retry more than 7 indices', async () => { const { ds, timeSrv, fetchMock } = getTestContext({ from: 'now-2w', jsonData: { interval: 'Daily' }, fetchMockImplementation: (options) => { return throwError({ status: 404 }); }, }); const range = timeSrv.timeRange(); await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toStrictEqual('Could not find an available index for this time range.'); expect(fetchMock).toBeCalledTimes(7); }); }); }); describe('When getting fields from ES 7.0', () => { const data = { 'genuine.es7._mapping.response': { mappings: { properties: { '@timestamp_millis': { type: 'date', format: 'epoch_millis', }, classification_terms: { type: 'keyword', }, domains: { type: 'keyword', }, ip_address: { type: 'ip', }, justification_blob: { properties: { criterion: { type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256, }, }, }, overall_vote_score: { type: 'float', }, shallow: { properties: { jsi: { properties: { sdb: { properties: { dsel2: { properties: { 'bootlegged-gille': { properties: { botness: { type: 'float', }, general_algorithm_score: { type: 'float', }, }, }, 'uncombed-boris': { properties: { botness: { type: 'float', }, general_algorithm_score: { type: 'float', }, }, }, }, }, }, }, }, }, }, }, }, }, overall_vote_score: { type: 'float', }, ua_terms_long: { type: 'keyword', }, ua_terms_short: { type: 'keyword', }, }, }, }, }; const dateFields = ['@timestamp_millis']; const numberFields = [ 'justification_blob.overall_vote_score', 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness', 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score', 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness', 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score', 'overall_vote_score', ]; it('should return nested fields', async () => { const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', }); await expect(ds.getFields()).toEmitValuesWith((received) => { expect(received.length).toBe(1); const fieldObjects = received[0]; const fields = map(fieldObjects, 'text'); expect(fields).toEqual([ '@timestamp_millis', 'classification_terms', 'domains', 'ip_address', 'justification_blob.criterion.keyword', 'justification_blob.criterion', 'justification_blob.overall_vote_score', 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness', 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score', 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness', 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score', 'overall_vote_score', 'ua_terms_long', 'ua_terms_short', ]); }); }); it('should return number fields', async () => { const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', }); await expect(ds.getFields(['number'])).toEmitValuesWith((received) => { expect(received.length).toBe(1); const fieldObjects = received[0]; const fields = map(fieldObjects, 'text'); expect(fields).toEqual(numberFields); }); }); it('should return date fields', async () => { const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', }); await expect(ds.getFields(['date'])).toEmitValuesWith((received) => { expect(received.length).toBe(1); const fieldObjects = received[0]; const fields = map(fieldObjects, 'text'); expect(fields).toEqual(dateFields); }); }); }); describe('When issuing aggregation query on es5.x', () => { async function runScenario() { const range = createTimeRange(dateTime([2015, 4, 30, 10]), dateTime([2015, 5, 1, 10])); const targets: ElasticsearchQuery[] = [ { refId: 'A', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }], metrics: [{ type: 'count', id: '1' }], query: 'test', }, ]; const query = { ...DATAQUERY_BASE, range, targets }; const data = { responses: [] }; const { ds, fetchMock } = getTestContext({ data, database: 'test' }); await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); expect(received[0]).toEqual({ data: [] }); }); expect(fetchMock).toHaveBeenCalledTimes(1); const requestOptions = fetchMock.mock.calls[0][0]; const parts = requestOptions.data.split('\n'); const header = JSON.parse(parts[0]); const body = JSON.parse(parts[1]); return { body, header }; } it('should not set search type to count', async () => { const { header } = await runScenario(); expect(header.search_type).not.toEqual('count'); }); it('should set size to 0', async () => { const { body } = await runScenario(); expect(body.size).toBe(0); }); }); describe('When issuing metricFind query on es5.x', () => { async function runScenario() { const data = { responses: [ { aggregations: { '1': { buckets: [ { doc_count: 1, key: 'test' }, { doc_count: 2, key: 'test2', key_as_string: 'test2_as_string', }, ], }, }, }, ], }; const { ds, fetchMock } = getTestContext({ data, database: 'test' }); const results = await ds.metricFindQuery('{"find": "terms", "field": "test"}'); expect(fetchMock).toHaveBeenCalledTimes(1); const requestOptions = fetchMock.mock.calls[0][0]; const parts = requestOptions.data.split('\n'); const header = JSON.parse(parts[0]); const body = JSON.parse(parts[1]); return { results, body, header }; } it('should get results', async () => { const { results } = await runScenario(); expect(results.length).toEqual(2); }); it('should use key or key_as_string', async () => { const { results } = await runScenario(); expect(results[0].text).toEqual('test'); expect(results[1].text).toEqual('test2_as_string'); }); it('should not set search type to count', async () => { const { header } = await runScenario(); expect(header.search_type).not.toEqual('count'); }); it('should set size to 0', async () => { const { body } = await runScenario(); expect(body.size).toBe(0); }); it('should not set terms aggregation size to 0', async () => { const { body } = await runScenario(); expect(body['aggs']['1']['terms'].size).not.toBe(0); }); }); describe('query', () => { it('should replace range as integer not string', async () => { const { ds } = getTestContext({ jsonData: { interval: 'Daily', timeField: '@time' } }); const postMock = jest.fn((url: string, data) => of(createFetchResponse({ responses: [] }))); ds['post'] = postMock; await expect(ds.query(createElasticQuery())).toEmitValuesWith((received) => { expect(postMock).toHaveBeenCalledTimes(1); const query = postMock.mock.calls[0][1]; expect(typeof JSON.parse(query.split('\n')[1]).query.bool.filter[0].range['@time'].gte).toBe('number'); }); }); }); it('should correctly interpolate variables in query', () => { const { ds } = getTestContext(); const query: ElasticsearchQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], query: '$var', }; const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0]; expect(interpolatedQuery.query).toBe('resolvedVariable'); expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('resolvedVariable'); }); it('should correctly add ad hoc filters when interpolating variables in query', () => { const templateSrvMock = { replace: (text?: string) => text, getAdhocFilters: () => [{ key: 'bar', operator: '=', value: 'test' }], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); const query: ElasticsearchQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], query: 'foo:"bar"', }; const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0]; expect(interpolatedQuery.query).toBe('foo:"bar" AND bar:"test"'); }); it('should correctly handle empty query strings in filters bucket aggregation', () => { const { ds } = getTestContext(); const query: ElasticsearchQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], query: '', }; const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0]; expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('*'); }); describe('getSupplementaryQuery', () => { let ds: ElasticDatasource; beforeEach(() => { ds = getTestContext().ds; }); it('does not return logs volume query for metric query', () => { expect( ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { refId: 'A', metrics: [{ type: 'count', id: '1' }], bucketAggs: [{ type: 'filters', settings: { filters: [{ query: 'foo', label: '' }] }, id: '1' }], query: 'foo="bar"', }) ).toEqual(undefined); }); it('returns logs volume query for log query', () => { expect( ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { refId: 'A', metrics: [{ type: 'logs', id: '1' }], query: 'foo="bar"', }) ).toEqual({ bucketAggs: [ { field: '', id: '3', settings: { interval: 'auto', min_doc_count: '0', trimEdges: '0', }, type: 'date_histogram', }, ], metrics: [ { id: '1', type: 'count', }, ], query: 'foo="bar"', refId: 'log-volume-A', timeField: '', }); }); }); }); describe('getMultiSearchUrl', () => { it('Should add correct params to URL if "includeFrozen" is enabled', () => { const { ds } = getTestContext({ jsonData: { includeFrozen: true, xpack: true } }); expect(ds.getMultiSearchUrl()).toMatch(/ignore_throttled=false/); }); it('Should NOT add ignore_throttled if "includeFrozen" is disabled', () => { const { ds } = getTestContext({ jsonData: { includeFrozen: false, xpack: true } }); expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); }); it('Should NOT add ignore_throttled if "xpack" is disabled', () => { const { ds } = getTestContext({ jsonData: { includeFrozen: true, xpack: false } }); expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); }); }); describe('enhanceDataFrame', () => { it('adds links to dataframe', () => { const df = new MutableDataFrame({ fields: [ { name: 'urlField', values: new ArrayVector([]), }, { name: 'traceField', values: new ArrayVector([]), }, ], }); enhanceDataFrame(df, [ { field: 'urlField', url: 'someUrl', }, { field: 'urlField', url: 'someOtherUrl', }, { field: 'traceField', url: 'query', datasourceUid: 'ds1', }, { field: 'traceField', url: 'otherQuery', datasourceUid: 'ds2', }, ]); expect(df.fields[0].config.links).toHaveLength(2); expect(df.fields[0].config.links).toContainEqual({ title: '', url: 'someUrl', }); expect(df.fields[0].config.links).toContainEqual({ title: '', url: 'someOtherUrl', }); expect(df.fields[1].config.links).toHaveLength(2); expect(df.fields[1].config.links).toContainEqual( expect.objectContaining({ title: '', url: '', internal: expect.objectContaining({ query: { query: 'query' }, datasourceUid: 'ds1', }), }) ); expect(df.fields[1].config.links).toContainEqual( expect.objectContaining({ title: '', url: '', internal: expect.objectContaining({ query: { query: 'otherQuery' }, datasourceUid: 'ds2', }), }) ); }); it('adds limit to dataframe', () => { const df = new MutableDataFrame({ fields: [ { name: 'someField', values: new ArrayVector([]), }, ], }); enhanceDataFrame(df, [], 10); expect(df.meta?.limit).toBe(10); }); }); describe('modifyQuery', () => { let ds: ElasticDatasource; beforeEach(() => { ds = getTestContext().ds; }); describe('with empty query', () => { let query: ElasticsearchQuery; beforeEach(() => { query = { query: '', refId: 'A' }; }); it('should add the filter', () => { expect(ds.modifyQuery(query, { type: 'ADD_FILTER', options: { key: 'foo', value: 'bar' } }).query).toBe( 'foo:"bar"' ); }); it('should add the negative filter', () => { expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( '-foo:"bar"' ); }); it('should do nothing on unknown type', () => { expect(ds.modifyQuery(query, { type: 'unknown', options: { key: 'foo', value: 'bar' } }).query).toBe(query.query); }); }); describe('with non-empty query', () => { let query: ElasticsearchQuery; beforeEach(() => { query = { query: 'test:"value"', refId: 'A' }; }); it('should add the filter', () => { expect(ds.modifyQuery(query, { type: 'ADD_FILTER', options: { key: 'foo', value: 'bar' } }).query).toBe( 'test:"value" AND foo:"bar"' ); }); it('should add the negative filter', () => { expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', options: { key: 'foo', value: 'bar' } }).query).toBe( 'test:"value" AND -foo:"bar"' ); }); it('should do nothing on unknown type', () => { expect(ds.modifyQuery(query, { type: 'unknown', options: { key: 'foo', value: 'bar' } }).query).toBe(query.query); }); }); }); describe('addAdhocFilters', () => { describe('with invalid filters', () => { it('should filter out ad hoc filter without key', () => { const templateSrvMock = { getAdhocFilters: () => [{ key: '', operator: '=', value: 'a' }], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); const query = ds.addAdHocFilters('foo:"bar"'); expect(query).toBe('foo:"bar"'); }); it('should filter out ad hoc filter without value', () => { const templateSrvMock = { getAdhocFilters: () => [{ key: 'a', operator: '=', value: '' }], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); const query = ds.addAdHocFilters('foo:"bar"'); expect(query).toBe('foo:"bar"'); }); it('should filter out filter ad hoc filter with invalid operator', () => { const templateSrvMock = { getAdhocFilters: () => [{ key: 'a', operator: 'A', value: '' }], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); const query = ds.addAdHocFilters('foo:"bar"'); expect(query).toBe('foo:"bar"'); }); }); describe('with 1 ad hoc filter', () => { const templateSrvMock = { getAdhocFilters: () => [{ key: 'test', operator: '=', value: 'test1' }], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); it('should correctly add 1 ad hoc filter when query is not empty', () => { const query = ds.addAdHocFilters('foo:"bar"'); expect(query).toBe('foo:"bar" AND test:"test1"'); }); it('should correctly add 1 ad hoc filter when query is empty', () => { const query = ds.addAdHocFilters(''); expect(query).toBe('test:"test1"'); }); }); describe('with multiple ad hoc filters', () => { const templateSrvMock = { getAdhocFilters: () => [ { key: 'bar', operator: '=', value: 'baz' }, { key: 'job', operator: '!=', value: 'grafana' }, { key: 'service', operator: '=~', value: 'service' }, { key: 'count', operator: '>', value: '1' }, ], } as unknown as TemplateSrv; const { ds } = getTestContext({ templateSrvMock }); it('should correctly add ad hoc filters when query is not empty', () => { const query = ds.addAdHocFilters('foo:"bar" AND test:"test1"'); expect(query).toBe( 'foo:"bar" AND test:"test1" AND bar:"baz" AND -job:"grafana" AND service:/service/ AND count:>1' ); }); it('should correctly add ad hoc filters when query is empty', () => { const query = ds.addAdHocFilters(''); expect(query).toBe('bar:"baz" AND -job:"grafana" AND service:/service/ AND count:>1'); }); }); }); const createElasticQuery = (): DataQueryRequest => { return { requestId: '', dashboardId: 0, interval: '', panelId: 0, intervalMs: 1, scopedVars: {}, timezone: '', app: CoreApp.Dashboard, startTime: 0, range: { from: dateTime([2015, 4, 30, 10]), to: dateTime([2015, 5, 1, 10]), raw: { from: '', to: '', }, }, targets: [ { refId: '', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }], metrics: [{ type: 'count', id: '' }], query: 'test', }, ], }; }; const logsResponse = { data: { responses: [ { aggregations: { '2': { buckets: [ { doc_count: 10, key: 1000, }, { doc_count: 15, key: 2000, }, ], }, }, hits: { hits: [ { '@timestamp': ['2019-06-24T09:51:19.765Z'], _id: 'fdsfs', _type: '_doc', _index: 'mock-index', _source: { '@timestamp': '2019-06-24T09:51:19.765Z', host: 'djisaodjsoad', message: 'hello, i am a message', }, fields: { '@timestamp': ['2019-06-24T09:51:19.765Z'], }, }, { '@timestamp': ['2019-06-24T09:52:19.765Z'], _id: 'kdospaidopa', _type: '_doc', _index: 'mock-index', _source: { '@timestamp': '2019-06-24T09:52:19.765Z', host: 'dsalkdakdop', message: 'hello, i am also message', }, fields: { '@timestamp': ['2019-06-24T09:52:19.765Z'], }, }, ], }, }, ], }, };