mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Template variable support for ifql datasource
* Implements findMetricQuery() * Macros for template queries: measurements(), tags(), tag_values(), field_keys() * Tests for macro expansion
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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})`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user