InfluxDB: Refactor query_builder and metadata_query (#69550)

* Move useUniqueId to a general place

* Use new built-in useId hook

* Rename query builder and metadata query

* Move and rename the query builder tests

* Refactor query_builder and metadata_query

* Fix test

* Fix test
This commit is contained in:
ismail simsek 2023-06-06 14:58:51 +03:00 committed by GitHub
parent f1178e0b81
commit ae0f94e616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 569 additions and 367 deletions

View File

@ -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"]
],

View File

@ -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' }));

View File

@ -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(
<Editor query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
);

View File

@ -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
)
}

View File

@ -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<InfluxQuery,
constructor(
instanceSettings: DataSourceInstanceSettings<InfluxOptions>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
@ -310,6 +310,19 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
};
}
async runMetadataQuery(target: InfluxQuery): Promise<MetricFindValue[]> {
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<MetricFindValue[]> {
if (this.isFlux || this.isMigrationToggleOnAndIsAccessProxy()) {
const target: InfluxQuery = {
@ -348,14 +361,25 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
// Used in public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx::fetchFilterKeys
getTagKeys(options: any = {}) {
const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
const query = queryBuilder.buildExploreQuery('TAG_KEYS');
const query = buildMetadataQuery({
type: 'TAG_KEYS',
templateService: this.templateSrv,
database: this.database,
measurement: options.measurement || '',
tags: [],
});
return this.metricFindQuery(query, options);
}
getTagValues(options: any = {}) {
const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
const query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
const query = buildMetadataQuery({
type: 'TAG_VALUES',
templateService: this.templateSrv,
database: this.database,
withKey: options.key,
measurement: options.measurement || '',
tags: [],
});
return this.metricFindQuery(query, options);
}

View File

@ -1,71 +1,99 @@
import InfluxDatasource from './datasource';
import { InfluxQueryBuilder } from './influxql_query_builder';
import { replaceHardCodedRetentionPolicy } from './queryUtils';
import { InfluxQueryTag } from './types';
import { ScopedVars } from '@grafana/data/src';
import config from 'app/core/config';
const runExploreQuery = (
type: string,
withKey: string | undefined,
withMeasurementFilter: string | undefined,
target: { measurement: string | undefined; tags: InfluxQueryTag[]; policy: string | undefined },
datasource: InfluxDatasource
): Promise<Array<{ text: string }>> => {
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<Array<{ text: string }>> => {
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<string[]> {
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<string[]> {
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<string[]> {
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<string[]> {
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<string[]> {
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);
}

View File

@ -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');
});
});

View File

@ -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<InfluxQueryTag, string[]>(
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;
}
}

View File

@ -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<InfluxOptions>): InfluxDatasource {
return new InfluxDatasource(instanceSettings, templateSrvStub);
}
export function getMockDSInstanceSettings(): DataSourceInstanceSettings<InfluxOptions> {
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' },
};
}

View File

@ -77,3 +77,5 @@ export interface InfluxQuery extends DataQuery {
textEditor?: boolean;
adhocFilters?: AdHocVariableFilter[];
}
export type MetadataQueryType = 'TAG_KEYS' | 'TAG_VALUES' | 'MEASUREMENTS' | 'FIELDS' | 'RETENTION_POLICIES';