diff --git a/public/app/plugins/datasource/influxdb-ifql/README.md b/public/app/plugins/datasource/influxdb-ifql/README.md index 515709f70fa..ce3917c1be2 100644 --- a/public/app/plugins/datasource/influxdb-ifql/README.md +++ b/public/app/plugins/datasource/influxdb-ifql/README.md @@ -14,9 +14,15 @@ Read more about InfluxDB here: [http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) +## Supported Template Variable Macros: + +* List all measurements for a given database: `measurements(database)` +* List all tags for a given database and measurement: `tags(database, measurement)` +* List all tag values for a given database, measurement, and tag: `tag_valuess(database, measurement, tag)` +* List all field keys for a given database and measurement: `field_keys(database, measurement)` + ## Roadmap -- metricFindQuery - Syntax highlighting - Tab completion (functions, values) - Annotations support diff --git a/public/app/plugins/datasource/influxdb-ifql/datasource.ts b/public/app/plugins/datasource/influxdb-ifql/datasource.ts index 2175b36bc36..ab9886baaca 100644 --- a/public/app/plugins/datasource/influxdb-ifql/datasource.ts +++ b/public/app/plugins/datasource/influxdb-ifql/datasource.ts @@ -2,7 +2,8 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; -import { getTableModelFromResult, getTimeSeriesFromResult, parseResults } from './response_parser'; +import { getTableModelFromResult, getTimeSeriesFromResult, getValuesFromResult, parseResults } from './response_parser'; +import expandMacros from './metric_find_query'; function serializeParams(params) { if (!params) { @@ -54,25 +55,21 @@ export default class InfluxDatasource { this.supportMetrics = true; } - prepareQueries(options) { - const targets = _.cloneDeep(options.targets); + prepareQueryTarget(target, options) { + // Replace grafana variables const timeFilter = this.getTimeFilter(options); options.scopedVars.range = { value: timeFilter }; - - // Filter empty queries and replace grafana variables - const queryTargets = targets.filter(t => t.query).map(t => { - const interpolated = this.templateSrv.replace(t.query, options.scopedVars); - return { - ...t, - query: interpolated, - }; - }); - - return queryTargets; + const interpolated = this.templateSrv.replace(target.query, options.scopedVars); + return { + ...target, + query: interpolated, + }; } query(options) { - const queryTargets = this.prepareQueries(options); + const queryTargets = options.targets + .filter(target => target.query) + .map(target => this.prepareQueryTarget(target, options)); if (queryTargets.length === 0) { return Promise.resolve({ data: [] }); } @@ -112,10 +109,23 @@ export default class InfluxDatasource { } metricFindQuery(query: string, options?: any) { - // TODO not implemented - var interpolated = this.templateSrv.replace(query, null, 'regex'); + const interpreted = expandMacros(query); - return this._seriesQuery(interpolated, options).then(_.curry(parseResults)(query)); + // Use normal querier in silent mode + const queryOptions = { + rangeRaw: { to: 'now', from: 'now - 1h' }, + scopedVars: {}, + ...options, + silent: true, + }; + const target = this.prepareQueryTarget({ query: interpreted }, queryOptions); + return this._seriesQuery(target.query, queryOptions).then(response => { + const results = parseResults(response.data); + const values = _.uniq(_.flatten(results.map(getValuesFromResult))); + return values + .filter(value => value && value[0] !== '_') // Ignore internal fields + .map(value => ({ text: value })); + }); } _seriesQuery(query: string, options?: any) { diff --git a/public/app/plugins/datasource/influxdb-ifql/metric_find_query.ts b/public/app/plugins/datasource/influxdb-ifql/metric_find_query.ts new file mode 100644 index 00000000000..4356fbdd48e --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/metric_find_query.ts @@ -0,0 +1,63 @@ +// MACROS + +// List all measurements for a given database: `measurements(database)` +const MEASUREMENTS_REGEXP = /^\s*measurements\((.+)\)\s*$/; + +// List all tags for a given database and measurement: `tags(database, measurement)` +const TAGS_REGEXP = /^\s*tags\((.+)\s*,\s*(.+)\)\s*$/; + +// List all tag values for a given database, measurement, and tag: `tag_valuess(database, measurement, tag)` +const TAG_VALUES_REGEXP = /^\s*tag_values\((.+)\s*,\s*(.+)\s*,\s*(.+)\)\s*$/; + +// List all field keys for a given database and measurement: `field_keys(database, measurement)` +const FIELD_KEYS_REGEXP = /^\s*field_keys\((.+)\s*,\s*(.+)\)\s*$/; + +export default function expandMacros(query) { + const measurementsQuery = query.match(MEASUREMENTS_REGEXP); + if (measurementsQuery) { + const database = measurementsQuery[1]; + return `from(db:"${database}") + |> range($range) + |> group(by:["_measurement"]) + |> distinct(column:"_measurement") + |> group(none:true)`; + } + + const tagsQuery = query.match(TAGS_REGEXP); + if (tagsQuery) { + const database = tagsQuery[1]; + const measurement = tagsQuery[2]; + return `from(db:"${database}") + |> range($range) + |> filter(fn:(r) => r._measurement == "${measurement}") + |> keys()`; + } + + const tagValuesQuery = query.match(TAG_VALUES_REGEXP); + if (tagValuesQuery) { + const database = tagValuesQuery[1]; + const measurement = tagValuesQuery[2]; + const tag = tagValuesQuery[3]; + return `from(db:"${database}") + |> range($range) + |> filter(fn:(r) => r._measurement == "${measurement}") + |> group(by:["${tag}"]) + |> distinct(column:"${tag}") + |> group(none:true)`; + } + + const fieldKeysQuery = query.match(FIELD_KEYS_REGEXP); + if (fieldKeysQuery) { + const database = fieldKeysQuery[1]; + const measurement = fieldKeysQuery[2]; + return `from(db:"${database}") + |> range($range) + |> filter(fn:(r) => r._measurement == "${measurement}") + |> group(by:["_field"]) + |> distinct(column:"_field") + |> group(none:true)`; + } + + // By default return pure query + return query; +} diff --git a/public/app/plugins/datasource/influxdb-ifql/response_parser.ts b/public/app/plugins/datasource/influxdb-ifql/response_parser.ts index e2ef753392c..5481b197726 100644 --- a/public/app/plugins/datasource/influxdb-ifql/response_parser.ts +++ b/public/app/plugins/datasource/influxdb-ifql/response_parser.ts @@ -86,3 +86,8 @@ export function getTimeSeriesFromResult(result: string) { return seriesList; } + +export function getValuesFromResult(result: string) { + const data = parseCSV(result); + return data.map(record => record['_value']); +} diff --git a/public/app/plugins/datasource/influxdb-ifql/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb-ifql/specs/datasource.jest.ts index d20e3884882..4d01e1f4d35 100644 --- a/public/app/plugins/datasource/influxdb-ifql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/influxdb-ifql/specs/datasource.jest.ts @@ -13,41 +13,27 @@ describe('InfluxDB (IFQL)', () => { targets: [], }; - let queries: any[]; - - describe('prepareQueries()', () => { - it('filters empty queries', () => { - queries = ds.prepareQueries(DEFAULT_OPTIONS); - expect(queries.length).toBe(0); - - queries = ds.prepareQueries({ - ...DEFAULT_OPTIONS, - targets: [{ query: '' }], - }); - expect(queries.length).toBe(0); - }); + describe('prepareQueryTarget()', () => { + let target: any; it('replaces $range variable', () => { - queries = ds.prepareQueries({ - ...DEFAULT_OPTIONS, - targets: [{ query: 'from(db: "test") |> range($range)' }], - }); - expect(queries.length).toBe(1); - expect(queries[0].query).toBe('from(db: "test") |> range(start: -3h)'); + target = ds.prepareQueryTarget({ query: 'from(db: "test") |> range($range)' }, DEFAULT_OPTIONS); + expect(target.query).toBe('from(db: "test") |> range(start: -3h)'); }); it('replaces $range variable with custom dates', () => { const to = moment(); const from = moment().subtract(1, 'hours'); - queries = ds.prepareQueries({ - ...DEFAULT_OPTIONS, - rangeRaw: { to, from }, - targets: [{ query: 'from(db: "test") |> range($range)' }], - }); - expect(queries.length).toBe(1); + target = ds.prepareQueryTarget( + { query: 'from(db: "test") |> range($range)' }, + { + ...DEFAULT_OPTIONS, + rangeRaw: { to, from }, + } + ); const start = from.toISOString(); const stop = to.toISOString(); - expect(queries[0].query).toBe(`from(db: "test") |> range(start: ${start}, stop: ${stop})`); + expect(target.query).toBe(`from(db: "test") |> range(start: ${start}, stop: ${stop})`); }); }); }); diff --git a/public/app/plugins/datasource/influxdb-ifql/specs/metric_find_query.jest.ts b/public/app/plugins/datasource/influxdb-ifql/specs/metric_find_query.jest.ts new file mode 100644 index 00000000000..e56874b475f --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/specs/metric_find_query.jest.ts @@ -0,0 +1,43 @@ +import expandMacros from '../metric_find_query'; + +describe('metric find query', () => { + describe('expandMacros()', () => { + it('returns a non-macro query unadulterated', () => { + const query = 'from(db:"telegraf") |> last()'; + const result = expandMacros(query); + expect(result).toBe(query); + }); + + it('returns a measurement query for measurements()', () => { + const query = ' measurements(mydb) '; + const result = expandMacros(query).replace(/\s/g, ''); + expect(result).toBe( + 'from(db:"mydb")|>range($range)|>group(by:["_measurement"])|>distinct(column:"_measurement")|>group(none:true)' + ); + }); + + it('returns a tags query for tags()', () => { + const query = ' tags(mydb , mymetric) '; + const result = expandMacros(query).replace(/\s/g, ''); + expect(result).toBe('from(db:"mydb")|>range($range)|>filter(fn:(r)=>r._measurement=="mymetric")|>keys()'); + }); + + it('returns a tag values query for tag_values()', () => { + const query = ' tag_values(mydb , mymetric, mytag) '; + const result = expandMacros(query).replace(/\s/g, ''); + expect(result).toBe( + 'from(db:"mydb")|>range($range)|>filter(fn:(r)=>r._measurement=="mymetric")' + + '|>group(by:["mytag"])|>distinct(column:"mytag")|>group(none:true)' + ); + }); + + it('returns a field keys query for field_keys()', () => { + const query = ' field_keys(mydb , mymetric) '; + const result = expandMacros(query).replace(/\s/g, ''); + expect(result).toBe( + 'from(db:"mydb")|>range($range)|>filter(fn:(r)=>r._measurement=="mymetric")' + + '|>group(by:["_field"])|>distinct(column:"_field")|>group(none:true)' + ); + }); + }); +}); diff --git a/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts index bac154c0760..cfd88729fa0 100644 --- a/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts +++ b/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts @@ -2,6 +2,7 @@ import { getNameFromRecord, getTableModelFromResult, getTimeSeriesFromResult, + getValuesFromResult, parseResults, parseValue, } from '../response_parser'; @@ -33,6 +34,14 @@ describe('influxdb ifql response parser', () => { }); }); + describe('getValuesFromResult()', () => { + it('returns all values from the _value field in the response', () => { + const results = parseResults(response); + const values = getValuesFromResult(results[0]); + expect(values.length).toBe(300); + }); + }); + describe('getNameFromRecord()', () => { it('expects name based on measurements and tags', () => { const record = {