diff --git a/.betterer.results b/.betterer.results index 01c46062c5a..5e7202416bd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4162,14 +4162,14 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"], + [0, 0, 0, "Do not use any type assertions.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], [0, 0, 0, "Unexpected any. Specify a different type.", "15"], @@ -4184,7 +4184,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "24"], [0, 0, 0, "Unexpected any. Specify a different type.", "25"], [0, 0, 0, "Unexpected any. Specify a different type.", "26"], - [0, 0, 0, "Do not use any type assertions.", "27"] + [0, 0, 0, "Unexpected any. Specify a different type.", "27"], + [0, 0, 0, "Do not use any type assertions.", "28"] ], "public/app/plugins/datasource/influxdb/influx_query_model.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -4229,14 +4230,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"] ], - "public/app/plugins/datasource/influxdb/influxql_query_builder.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"] - ], "public/app/plugins/datasource/influxdb/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4294,6 +4287,12 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "15"], [0, 0, 0, "Unexpected any. Specify a different type.", "16"] ], + "public/app/plugins/datasource/influxdb/specs/mocks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] + ], "public/app/plugins/datasource/influxdb/specs/response_parser.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx index e3c0c590b5a..7a2ad49cd3f 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx @@ -139,7 +139,7 @@ describe('InfluxDB InfluxQL Visual Editor field-filtering', () => { // verify `getTagValues` was called once, and in the tags-param we did not receive `field1` expect(mockedMeta.getTagValues).toHaveBeenCalledTimes(1); - expect((mockedMeta.getTagValues as jest.Mock).mock.calls[0][3]).toStrictEqual(ONLY_TAGS); + expect((mockedMeta.getTagValues as jest.Mock).mock.calls[0][1]).toStrictEqual(ONLY_TAGS); // now we click on the FROM/cpudata button await userEvent.click(screen.getByRole('button', { name: 'cpudata' })); diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx index c763668811d..d3cc6f80ab4 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx @@ -2,6 +2,7 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import InfluxDatasource from '../../datasource'; +import { getMockDS, getMockDSInstanceSettings } from '../../specs/mocks'; import { InfluxQuery } from '../../types'; import { Editor } from './Editor'; @@ -38,10 +39,8 @@ jest.mock('./Seg', () => { async function assertEditor(query: InfluxQuery, textContent: string) { const onChange = jest.fn(); const onRunQuery = jest.fn(); - const datasource: InfluxDatasource = { - retentionPolicies: [], - metricFindQuery: () => Promise.resolve([]), - } as unknown as InfluxDatasource; + const datasource: InfluxDatasource = getMockDS(getMockDSInstanceSettings()); + datasource.metricFindQuery = () => Promise.resolve([]); const { container } = render( ); diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx index e085e4d088a..da71ca6ff9b 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx @@ -92,11 +92,11 @@ export const Editor = (props: Props): JSX.Element => { const retentionPolicies = !!policyData.error ? [] : policyData.value ?? []; const allTagKeys = useMemo(async () => { - const tagKeys = (await getTagKeysForMeasurementAndTags(measurement, policy, [], datasource)).map( + const tagKeys = (await getTagKeysForMeasurementAndTags(datasource, [], measurement, policy)).map( (tag) => `${tag}::tag` ); - const fieldKeys = (await getFieldKeysForMeasurement(measurement || '', policy, datasource)).map( + const fieldKeys = (await getFieldKeysForMeasurement(datasource, measurement || '', policy)).map( (field) => `${field}::field` ); @@ -109,7 +109,7 @@ export const Editor = (props: Props): JSX.Element => { 'field_0', () => { return measurement !== undefined - ? getFieldKeysForMeasurement(measurement, policy, datasource) + ? getFieldKeysForMeasurement(datasource, measurement, policy) : Promise.resolve([]); }, ], @@ -162,7 +162,7 @@ export const Editor = (props: Props): JSX.Element => { measurement={measurement} getPolicyOptions={() => withTemplateVariableOptions( - allTagKeys.then((keys) => getAllPolicies(datasource)), + allTagKeys.then(() => getAllPolicies(datasource)), wrapPure ) } @@ -170,9 +170,9 @@ export const Editor = (props: Props): JSX.Element => { withTemplateVariableOptions( allTagKeys.then((keys) => getAllMeasurementsForTags( - filter === '' ? undefined : filter, + datasource, filterTags(query.tags ?? [], keys), - datasource + filter === '' ? undefined : filter ) ), wrapRegex, @@ -188,11 +188,9 @@ export const Editor = (props: Props): JSX.Element => { tags={query.tags ?? []} onChange={handleTagsSectionChange} getTagKeyOptions={getTagKeys} - getTagValueOptions={(key: string) => + getTagValueOptions={(key) => withTemplateVariableOptions( - allTagKeys.then((keys) => - getTagValues(key, measurement, policy, filterTags(query.tags ?? [], keys), datasource) - ), + allTagKeys.then((keys) => getTagValues(datasource, filterTags(query.tags ?? [], keys), key)), wrapRegex ) } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 349e1e01d41..b8deb8b2639 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -35,7 +35,7 @@ import { BROWSER_MODE_DISABLED_MESSAGE } from './constants'; import InfluxQueryModel from './influx_query_model'; import InfluxSeries from './influx_series'; import { getAllPolicies } from './influxql_metadata_query'; -import { InfluxQueryBuilder } from './influxql_query_builder'; +import { buildMetadataQuery } from './influxql_query_builder'; import { prepareAnnotation } from './migrations'; import { buildRawQuery, replaceHardCodedRetentionPolicy } from './queryUtils'; import ResponseParser from './response_parser'; @@ -59,7 +59,7 @@ export default class InfluxDatasource extends DataSourceWithBackend, - private readonly templateSrv: TemplateSrv = getTemplateSrv() + readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -310,6 +310,19 @@ export default class InfluxDatasource extends DataSourceWithBackend { + return lastValueFrom( + super.query({ + targets: [target], + } as DataQueryRequest) + ).then((rsp) => { + if (rsp.data?.length) { + return frameToMetricFindValue(rsp.data[0]); + } + return []; + }); + } + async metricFindQuery(query: string, options?: any): Promise { if (this.isFlux || this.isMigrationToggleOnAndIsAccessProxy()) { const target: InfluxQuery = { @@ -348,14 +361,25 @@ export default class InfluxDatasource extends DataSourceWithBackend> => { - const builder = new InfluxQueryBuilder(target, datasource.database); - const q = builder.buildExploreQuery(type, withKey, withMeasurementFilter); - const options = { policy: replaceHardCodedRetentionPolicy(target.policy, datasource.retentionPolicies) }; - return datasource.metricFindQuery(q, options); +import InfluxDatasource from './datasource'; +import { buildMetadataQuery } from './influxql_query_builder'; +import { replaceHardCodedRetentionPolicy } from './queryUtils'; +import { InfluxQuery, InfluxQueryTag, MetadataQueryType } from './types'; + +type MetadataQueryOptions = { + type: MetadataQueryType; + datasource: InfluxDatasource; + scopedVars?: ScopedVars; + measurement?: string; + retentionPolicy?: string; + tags?: InfluxQueryTag[]; + withKey?: string; + withMeasurementFilter?: string; +}; + +const runExploreQuery = async (options: MetadataQueryOptions): Promise> => { + const { type, datasource, scopedVars, measurement, retentionPolicy, tags, withKey, withMeasurementFilter } = options; + const query = buildMetadataQuery({ + type, + scopedVars, + measurement, + retentionPolicy, + tags, + withKey, + withMeasurementFilter, + templateService: datasource.templateSrv, + database: datasource.database, + }); + const policy = retentionPolicy ? datasource.templateSrv.replace(retentionPolicy, {}, 'regex') : ''; + const target: InfluxQuery = { + query, + rawQuery: true, + refId: 'metadataQuery', + policy: replaceHardCodedRetentionPolicy(policy, datasource.retentionPolicies), + }; + if (config.featureToggles.influxdbBackendMigration) { + return datasource.runMetadataQuery(target); + } else { + const options = { policy: target.policy }; + return datasource.metricFindQuery(query, options); + } }; export async function getAllPolicies(datasource: InfluxDatasource): Promise { - const target = { tags: [], measurement: undefined, policy: undefined }; - const data = await runExploreQuery('RETENTION POLICIES', undefined, undefined, target, datasource); + const data = await runExploreQuery({ type: 'RETENTION_POLICIES', datasource }); return data.map((item) => item.text); } export async function getAllMeasurementsForTags( - measurementFilter: string | undefined, + datasource: InfluxDatasource, tags: InfluxQueryTag[], - datasource: InfluxDatasource + withMeasurementFilter?: string ): Promise { - const target = { tags, measurement: undefined, policy: undefined }; - const data = await runExploreQuery('MEASUREMENTS', undefined, measurementFilter, target, datasource); + const data = await runExploreQuery({ type: 'MEASUREMENTS', datasource, tags, withMeasurementFilter }); return data.map((item) => item.text); } export async function getTagKeysForMeasurementAndTags( - measurement: string | undefined, - policy: string | undefined, + datasource: InfluxDatasource, tags: InfluxQueryTag[], - datasource: InfluxDatasource + measurement?: string, + retentionPolicy?: string ): Promise { - const target = { tags, measurement, policy }; - const data = await runExploreQuery('TAG_KEYS', undefined, undefined, target, datasource); + const data = await runExploreQuery({ type: 'TAG_KEYS', datasource, measurement, retentionPolicy }); return data.map((item) => item.text); } export async function getTagValues( - tagKey: string, - measurement: string | undefined, - policy: string | undefined, + datasource: InfluxDatasource, tags: InfluxQueryTag[], - datasource: InfluxDatasource + tagKey: string, + measurement?: string, + retentionPolicy?: string ): Promise { - const target = { tags, measurement, policy }; - if (tagKey.endsWith('::field')) { return []; } - - const data = await runExploreQuery('TAG_VALUES', tagKey, undefined, target, datasource); + const data = await runExploreQuery({ + type: 'TAG_VALUES', + withKey: tagKey, + datasource, + measurement, + retentionPolicy, + }); return data.map((item) => item.text); } export async function getFieldKeysForMeasurement( + datasource: InfluxDatasource, measurement: string, - policy: string | undefined, - datasource: InfluxDatasource + retentionPolicy?: string ): Promise { - const target = { tags: [], measurement, policy }; - const data = await runExploreQuery('FIELDS', undefined, undefined, target, datasource); + const data = await runExploreQuery({ type: 'FIELDS', datasource, measurement, retentionPolicy }); return data.map((item) => item.text); } diff --git a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts index 3d99d2a7080..c24dd26e419 100644 --- a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts @@ -1,225 +1,281 @@ -import { InfluxQueryBuilder } from './influxql_query_builder'; +import { buildMetadataQuery } from './influxql_query_builder'; +import { templateSrvStub as templateService } from './specs/mocks'; -describe('InfluxQueryBuilder', () => { - describe('when building explore queries', () => { - it('should only have measurement condition in tag keys query given query with measurement', () => { - const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); - const query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS FROM "cpu"'); - }); - - it('should handle regex measurement in tag keys query', () => { - const builder = new InfluxQueryBuilder({ - measurement: '/.*/', - tags: [], +describe('influx-query-builder', () => { + describe('RETENTION_POLICIES', () => { + it('should build retention policies query', () => { + const query = buildMetadataQuery({ + type: 'RETENTION_POLICIES', + templateService, + database: 'site', }); - const query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS FROM /.*/'); - }); - - it('should have no conditions in tags keys query given query with no measurement or tag', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - const query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS'); - }); - - it('should have where condition in tag keys query with tags', () => { - const builder = new InfluxQueryBuilder({ - measurement: '', - tags: [{ key: 'host', value: 'se1' }], - }); - const query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); - }); - - it('should ignore condition if operator is a value operator', () => { - const builder = new InfluxQueryBuilder({ - measurement: '', - tags: [{ key: 'value', value: '10', operator: '>' }], - }); - const query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS'); - }); - - it('should have no conditions in measurement query for query with no tags', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - const query = builder.buildExploreQuery('MEASUREMENTS'); - expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); - }); - - it('should have no conditions in measurement query for query with no tags and empty query', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - const query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); - expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); - }); - - it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); - expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)something/ LIMIT 100'); - }); - - it('should escape the regex value in measurement query', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'abc/edf/'); - expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)abc\\/edf\\// LIMIT 100'); - }); - - it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => { - const builder = new InfluxQueryBuilder({ - measurement: '', - tags: [{ key: 'app', value: 'email' }], - }); - const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); - expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)something/ WHERE "app" = \'email\' LIMIT 100'); - }); - - it('should have where condition in measurement query for query with tags', () => { - const builder = new InfluxQueryBuilder({ - measurement: '', - tags: [{ key: 'app', value: 'email' }], - }); - const query = builder.buildExploreQuery('MEASUREMENTS'); - expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100'); - }); - - it('should have where tag name IN filter in tag values query for query with one tag', () => { - const builder = new InfluxQueryBuilder({ - measurement: '', - tags: [{ key: 'app', value: 'asdsadsad' }], - }); - const query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"'); - }); - - it('should have measurement tag condition and tag name IN filter in tag values query', () => { - const builder = new InfluxQueryBuilder({ - measurement: 'cpu', - tags: [ - { key: 'app', value: 'email' }, - { key: 'host', value: 'server1' }, - ], - }); - const query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); - }); - - it('should select from policy correctly if policy is specified', () => { - const builder = new InfluxQueryBuilder({ - measurement: 'cpu', - policy: 'one_week', - tags: [ - { key: 'app', value: 'email' }, - { key: 'host', value: 'server1' }, - ], - }); - const query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\''); - }); - - it('should not include policy when policy is default', () => { - const builder = new InfluxQueryBuilder({ - measurement: 'cpu', - policy: 'default', - tags: [], - }); - const query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"'); - }); - - it('should switch to regex operator in tag condition', () => { - const builder = new InfluxQueryBuilder({ - measurement: 'cpu', - tags: [{ key: 'host', value: '/server.*/' }], - }); - const query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); + expect(query).toBe('SHOW RETENTION POLICIES on "site"'); }); + }); + describe('FIELDS', () => { it('should build show field query', () => { - const builder = new InfluxQueryBuilder({ + const query = buildMetadataQuery({ + type: 'FIELDS', + templateService, measurement: 'cpu', tags: [{ key: 'app', value: 'email' }], }); - const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM "cpu"'); }); it('should build show field query with regexp', () => { - const builder = new InfluxQueryBuilder({ + const query = buildMetadataQuery({ + type: 'FIELDS', + templateService, measurement: '/$var/', tags: [{ key: 'app', value: 'email' }], }); - const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM /$var/'); }); + }); - it('should build show retention policies query', () => { - const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); - const query = builder.buildExploreQuery('RETENTION POLICIES'); - expect(query).toBe('SHOW RETENTION POLICIES on "site"'); + describe('TAG_KEYS', () => { + it('should only have measurement condition in tag keys query given query with measurement', () => { + const query = buildMetadataQuery({ type: 'TAG_KEYS', templateService, measurement: 'cpu', tags: [] }); + expect(query).toBe('SHOW TAG KEYS FROM "cpu"'); }); - it('should handle tag-value=number-ish when getting measurements', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: '42', operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('MEASUREMENTS'); - expect(query).toBe(`SHOW MEASUREMENTS WHERE "app" == '42' LIMIT 100`); + it('should handle regex measurement in tag keys query', () => { + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: '/.*/', + tags: [], + }); + expect(query).toBe('SHOW TAG KEYS FROM /.*/'); }); + it('should have no conditions in tags keys query given query with no measurement or tag', () => { + const query = buildMetadataQuery({ type: 'TAG_KEYS', templateService, measurement: '', tags: [] }); + expect(query).toBe('SHOW TAG KEYS'); + }); + + it('should have where condition in tag keys query with tags', () => { + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: '', + tags: [{ key: 'host', value: 'se1' }], + }); + expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); + }); + + it('should ignore condition if operator is a value operator', () => { + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: '', + tags: [{ key: 'value', value: '10', operator: '>' }], + }); + expect(query).toBe('SHOW TAG KEYS'); + }); it('should handle tag-value=number-ish getting tag-keys', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: '42', operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('TAG_KEYS'); + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: undefined, + tags: [{ key: 'app', value: '42', operator: '==' }], + database: undefined, + }); expect(query).toBe(`SHOW TAG KEYS WHERE "app" == '42'`); }); it('should handle tag-value-contains-backslash-character getting tag-keys', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: 'lab\\el', operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('TAG_KEYS'); + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: undefined, + tags: [{ key: 'app', value: 'lab\\el', operator: '==' }], + database: undefined, + }); expect(query).toBe(`SHOW TAG KEYS WHERE "app" == 'lab\\\\el'`); }); it('should handle tag-value-contains-single-quote-character getting tag-keys', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: "lab'el", operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('TAG_KEYS'); + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: undefined, + tags: [{ key: 'app', value: "lab'el", operator: '==' }], + database: undefined, + }); expect(query).toBe(`SHOW TAG KEYS WHERE "app" == 'lab\\'el'`); }); - it('should handle tag-value=emptry-string when getting measurements', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: '', operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('MEASUREMENTS'); - expect(query).toBe(`SHOW MEASUREMENTS WHERE "app" == '' LIMIT 100`); - }); - it('should handle tag-value=emptry-string when getting tag-keys', () => { - const builder = new InfluxQueryBuilder( - { measurement: undefined, tags: [{ key: 'app', value: '', operator: '==' }] }, - undefined - ); - const query = builder.buildExploreQuery('TAG_KEYS'); + const query = buildMetadataQuery({ + type: 'TAG_KEYS', + templateService, + measurement: undefined, + tags: [{ key: 'app', value: '', operator: '==' }], + database: undefined, + }); expect(query).toBe(`SHOW TAG KEYS WHERE "app" == ''`); }); + }); - it('should not add FROM statement if the measurement empty', () => { - const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - let query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).toBe('SHOW TAG KEYS'); - query = builder.buildExploreQuery('FIELDS'); - expect(query).toBe('SHOW FIELD KEYS'); + describe('TAG_VALUES', () => { + it('should have where tag name IN filter in tag values query for query with one tag', () => { + const query = buildMetadataQuery({ + type: 'TAG_VALUES', + templateService, + withKey: 'app', + measurement: '', + tags: [{ key: 'app', value: 'asdsadsad' }], + }); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"'); + }); + + it('should have measurement tag condition and tag name IN filter in tag values query', () => { + const query = buildMetadataQuery({ + type: 'TAG_VALUES', + templateService, + withKey: 'app', + measurement: 'cpu', + tags: [ + { key: 'app', value: 'email' }, + { key: 'host', value: 'server1' }, + ], + }); + expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); + }); + + it('should select from policy correctly if policy is specified', () => { + const query = buildMetadataQuery({ + type: 'TAG_VALUES', + templateService, + withKey: 'app', + measurement: 'cpu', + retentionPolicy: 'one_week', + tags: [ + { key: 'app', value: 'email' }, + { key: 'host', value: 'server1' }, + ], + }); + expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\''); + }); + + it('should not include policy when policy is default', () => { + const query = buildMetadataQuery({ + type: 'TAG_VALUES', + templateService, + withKey: 'app', + measurement: 'cpu', + retentionPolicy: 'default', + tags: [], + }); + expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"'); + }); + + it('should switch to regex operator in tag condition', () => { + const query = buildMetadataQuery({ + type: 'TAG_VALUES', + templateService, + withKey: 'app', + measurement: 'cpu', + tags: [{ key: 'host', value: '/server.*/' }], + }); + expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); }); }); + + describe('MEASUREMENTS', () => { + it('should have no conditions in measurement query for query with no tags', () => { + const query = buildMetadataQuery({ type: 'MEASUREMENTS', templateService, measurement: '', tags: [] }); + expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); + }); + + it('should have no conditions in measurement query for query with no tags and empty query', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + withKey: undefined, + withMeasurementFilter: '', + measurement: '', + tags: [], + }); + expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); + }); + + it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + withKey: undefined, + withMeasurementFilter: 'something', + measurement: '', + tags: [], + }); + expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)something/ LIMIT 100'); + }); + + it('should escape the regex value in measurement query', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + withKey: undefined, + withMeasurementFilter: 'abc/edf/', + measurement: '', + tags: [], + }); + expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)abc\\/edf\\// LIMIT 100'); + }); + + it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + withKey: undefined, + withMeasurementFilter: 'something', + measurement: '', + tags: [{ key: 'app', value: 'email' }], + }); + expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)something/ WHERE "app" = \'email\' LIMIT 100'); + }); + + it('should have where condition in measurement query for query with tags', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + measurement: '', + tags: [{ key: 'app', value: 'email' }], + }); + expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100'); + }); + + it('should handle tag-value=number-ish when getting measurements', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + database: undefined, + measurement: undefined, + tags: [{ key: 'app', value: '42', operator: '==' }], + }); + expect(query).toBe(`SHOW MEASUREMENTS WHERE "app" == '42' LIMIT 100`); + }); + + it('should handle tag-value=emptry-string when getting measurements', () => { + const query = buildMetadataQuery({ + type: 'MEASUREMENTS', + templateService, + database: undefined, + measurement: undefined, + tags: [{ key: 'app', value: '', operator: '==' }], + }); + expect(query).toBe(`SHOW MEASUREMENTS WHERE "app" == '' LIMIT 100`); + }); + }); + + it('should not add FROM statement if the measurement empty', () => { + let query = buildMetadataQuery({ type: 'TAG_KEYS', templateService, measurement: '', tags: [] }); + expect(query).toBe('SHOW TAG KEYS'); + query = buildMetadataQuery({ type: 'FIELDS', templateService }); + expect(query).toBe('SHOW FIELD KEYS'); + }); }); diff --git a/public/app/plugins/datasource/influxdb/influxql_query_builder.ts b/public/app/plugins/datasource/influxdb/influxql_query_builder.ts index 60d190602da..97ad4f07d6f 100644 --- a/public/app/plugins/datasource/influxdb/influxql_query_builder.ts +++ b/public/app/plugins/datasource/influxdb/influxql_query_builder.ts @@ -1,9 +1,138 @@ import { reduce } from 'lodash'; -import { escapeRegex } from '@grafana/data'; +import { escapeRegex, ScopedVars } from '@grafana/data/src'; -function renderTagCondition(tag: { operator: any; value: string; condition: any; key: string }, index: number) { - // FIXME: merge this function with influx_query_model/renderTagCondition +import { TemplateSrv } from '../../../features/templating/template_srv'; + +import { InfluxQueryTag, MetadataQueryType } from './types'; + +export const buildMetadataQuery = (params: { + type: MetadataQueryType; + templateService: TemplateSrv; + scopedVars?: ScopedVars; + database?: string; + measurement?: string; + retentionPolicy?: string; + tags?: InfluxQueryTag[]; + withKey?: string; + withMeasurementFilter?: string; +}): string => { + let query = ''; + let { + type, + templateService, + scopedVars, + database, + measurement, + retentionPolicy, + tags, + withKey, + withMeasurementFilter, + } = params; + + switch (type) { + case 'RETENTION_POLICIES': + return 'SHOW RETENTION POLICIES on "' + database + '"'; + case 'FIELDS': + if (!measurement || measurement === '') { + return 'SHOW FIELD KEYS'; + } + + // If there is a measurement and it is not empty string + if (measurement && !measurement.match(/^\/.*\/|^$/)) { + measurement = '"' + measurement + '"'; + + if (retentionPolicy && retentionPolicy !== 'default') { + retentionPolicy = '"' + retentionPolicy + '"'; + measurement = retentionPolicy + '.' + measurement; + } + } + + return 'SHOW FIELD KEYS FROM ' + measurement; + case 'TAG_KEYS': + query = 'SHOW TAG KEYS'; + break; + case 'TAG_VALUES': + query = 'SHOW TAG VALUES'; + break; + case 'MEASUREMENTS': + query = 'SHOW MEASUREMENTS'; + if (withMeasurementFilter) { + // we do a case-insensitive regex-based lookup + query += ' WITH MEASUREMENT =~ /(?i)' + escapeRegex(withMeasurementFilter) + '/'; + } + break; + default: + return query; + } + if (measurement) { + if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { + measurement = '"' + measurement + '"'; + } + + if (retentionPolicy && retentionPolicy !== 'default') { + retentionPolicy = '"' + retentionPolicy + '"'; + measurement = retentionPolicy + '.' + measurement; + } + + if (measurement !== '') { + query += ' FROM ' + measurement; + } + } + + if (withKey) { + let keyIdentifier = withKey; + + if (keyIdentifier.endsWith('::tag')) { + keyIdentifier = keyIdentifier.slice(0, -5); + } + + query += ' WITH KEY = "' + keyIdentifier + '"'; + } + + if (tags && tags.length > 0) { + const whereConditions = reduce( + tags, + (memo, tag) => { + // do not add a condition for the key we want to explore for + if (tag.key && tag.key === withKey) { + return memo; + } + + // value operators not supported in these types of queries + if (tag.operator === '>' || tag.operator === '<') { + return memo; + } + + memo.push(renderTagCondition(tag, memo.length, templateService, scopedVars, true)); + return memo; + }, + [] + ); + + if (whereConditions.length > 0) { + query += ' WHERE ' + whereConditions.join(' '); + } + } + + if (type === 'MEASUREMENTS') { + query += ' LIMIT 100'; + //Solve issue #2524 by limiting the number of measurements returned + //LIMIT must be after WITH MEASUREMENT and WHERE clauses + //This also could be used for TAG KEYS and TAG VALUES, if desired + } + + return query; +}; + +// A merge of query_builder/renderTagCondition and influx_query_model/renderTagCondition +export function renderTagCondition( + tag: InfluxQueryTag, + index: number, + templateSrv: TemplateSrv, + scopedVars?: ScopedVars, + interpolate?: boolean +) { let str = ''; let operator = tag.operator; let value = tag.value; @@ -25,6 +154,17 @@ function renderTagCondition(tag: { operator: any; value: string; condition: any; value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'"; } + // quote value unless regex + if (operator !== '=~' && operator !== '!~') { + if (interpolate) { + value = templateSrv.replace(value, scopedVars); + } else if (operator !== '>' && operator !== '<') { + value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'"; + } + } else if (interpolate) { + value = templateSrv.replace(value, scopedVars, 'regex'); + } + let escapedKey = `"${tag.key}"`; if (tag.key.endsWith('::tag')) { @@ -37,110 +177,3 @@ function renderTagCondition(tag: { operator: any; value: string; condition: any; return str + escapedKey + ' ' + operator + ' ' + value; } - -export class InfluxQueryBuilder { - constructor(private target: { measurement: any; tags: any; policy?: any }, private database?: string) {} - - buildExploreQuery(type: string, withKey?: string, withMeasurementFilter?: string): string { - let query = ''; - let measurement; - let policy; - - if (type === 'TAG_KEYS') { - query = 'SHOW TAG KEYS'; - measurement = this.target.measurement; - policy = this.target.policy; - } else if (type === 'TAG_VALUES') { - query = 'SHOW TAG VALUES'; - measurement = this.target.measurement; - policy = this.target.policy; - } else if (type === 'MEASUREMENTS') { - query = 'SHOW MEASUREMENTS'; - if (withMeasurementFilter) { - // we do a case-insensitive regex-based lookup - query += ' WITH MEASUREMENT =~ /(?i)' + escapeRegex(withMeasurementFilter) + '/'; - } - } else if (type === 'FIELDS') { - measurement = this.target.measurement; - policy = this.target.policy; - - // If there is a measurement and it is not empty string - if (!measurement.match(/^\/.*\/|^$/)) { - measurement = '"' + measurement + '"'; - - if (policy && policy !== 'default') { - policy = '"' + policy + '"'; - measurement = policy + '.' + measurement; - } - } - - if (measurement === '') { - return 'SHOW FIELD KEYS'; - } - - return 'SHOW FIELD KEYS FROM ' + measurement; - } else if (type === 'RETENTION POLICIES') { - query = 'SHOW RETENTION POLICIES on "' + this.database + '"'; - return query; - } - - if (measurement) { - if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { - measurement = '"' + measurement + '"'; - } - - if (policy && policy !== 'default') { - policy = '"' + policy + '"'; - measurement = policy + '.' + measurement; - } - - if (measurement !== '') { - query += ' FROM ' + measurement; - } - } - - if (withKey) { - let keyIdentifier = withKey; - - if (keyIdentifier.endsWith('::tag')) { - keyIdentifier = keyIdentifier.slice(0, -5); - } - - query += ' WITH KEY = "' + keyIdentifier + '"'; - } - - if (this.target.tags && this.target.tags.length > 0) { - const whereConditions = reduce( - this.target.tags, - (memo, tag) => { - // do not add a condition for the key we want to explore for - if (tag.key === withKey) { - return memo; - } - - // value operators not supported in these types of queries - if (tag.operator === '>' || tag.operator === '<') { - return memo; - } - - memo.push(renderTagCondition(tag, memo.length)); - return memo; - }, - [] as string[] - ); - - if (whereConditions.length > 0) { - query += ' WHERE ' + whereConditions.join(' '); - } - } - - if (type === 'MEASUREMENTS') { - query += ' LIMIT 100'; - //Solve issue #2524 by limiting the number of measurements returned - //LIMIT must be after WITH MEASUREMENT and WHERE clauses - //This also could be used for TAG KEYS and TAG VALUES, if desired - } - - return query; - } -} diff --git a/public/app/plugins/datasource/influxdb/specs/mocks.ts b/public/app/plugins/datasource/influxdb/specs/mocks.ts new file mode 100644 index 00000000000..9402f08aa44 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/mocks.ts @@ -0,0 +1,63 @@ +import { of } from 'rxjs'; + +import { DataSourceInstanceSettings, PluginType } from '@grafana/data/src'; +import { FetchResponse, getBackendSrv, setBackendSrv } from '@grafana/runtime/src'; + +import { TemplateSrv } from '../../../../features/templating/template_srv'; +import InfluxDatasource from '../datasource'; +import { InfluxOptions, InfluxVersion } from '../types'; + +const getAdhocFiltersMock = jest.fn().mockImplementation(() => []); +const replaceMock = jest.fn().mockImplementation((a: string, ...rest: unknown[]) => a); + +export const templateSrvStub = { + getAdhocFilters: getAdhocFiltersMock, + replace: replaceMock, +} as unknown as TemplateSrv; + +export function mockBackendService(response: any) { + const fetchMock = jest.fn().mockReturnValue(of(response as FetchResponse)); + const origBackendSrv = getBackendSrv(); + setBackendSrv({ + ...origBackendSrv, + fetch: fetchMock, + }); +} + +export function getMockDS(instanceSettings: DataSourceInstanceSettings): InfluxDatasource { + return new InfluxDatasource(instanceSettings, templateSrvStub); +} + +export function getMockDSInstanceSettings(): DataSourceInstanceSettings { + return { + id: 123, + url: 'proxied', + access: 'proxy', + name: 'influxDb', + readOnly: false, + uid: 'influxdb-test', + type: 'influxdb', + meta: { + id: 'influxdb-meta', + type: PluginType.datasource, + name: 'influxdb-test', + info: { + author: { + name: 'observability-metrics', + }, + version: 'v0.0.1', + description: 'test', + links: [], + logos: { + large: '', + small: '', + }, + updated: '', + screenshots: [], + }, + module: '', + baseUrl: '', + }, + jsonData: { version: InfluxVersion.InfluxQL, httpMode: 'POST' }, + }; +} diff --git a/public/app/plugins/datasource/influxdb/types.ts b/public/app/plugins/datasource/influxdb/types.ts index ea588d9a223..9de94c0c8a0 100644 --- a/public/app/plugins/datasource/influxdb/types.ts +++ b/public/app/plugins/datasource/influxdb/types.ts @@ -77,3 +77,5 @@ export interface InfluxQuery extends DataQuery { textEditor?: boolean; adhocFilters?: AdHocVariableFilter[]; } + +export type MetadataQueryType = 'TAG_KEYS' | 'TAG_VALUES' | 'MEASUREMENTS' | 'FIELDS' | 'RETENTION_POLICIES';