Commit c77d72b2 by Torkel Ödegaard

feat(prometheus): progress on new prometheus query editor, #5117

parent a3ee388b
...@@ -11,27 +11,33 @@ function (_) { ...@@ -11,27 +11,33 @@ function (_) {
} }
PrometheusMetricFindQuery.prototype.process = function() { PrometheusMetricFindQuery.prototype.process = function() {
var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; var labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/;
var metric_names_regex = /^metrics\((.+)\)$/; var metricNamesRegex = /^metrics\((.+)\)$/;
var query_result_regex = /^query_result\((.+)\)$/; var labelsRegex = /^labels\((.+)\)$/;
var queryResultRegex = /^query_result\((.+)\)$/;
var label_values_query = this.query.match(label_values_regex);
if (label_values_query) { var labelsQuery = this.query.match(labelsRegex);
if (label_values_query[1]) { if (labelsQuery) {
return this.labelValuesQuery(label_values_query[2], label_values_query[1]); return this.labelsQuery(labelsQuery[1]);
}
var labelValuesQuery = this.query.match(labelValuesRegex);
if (labelValuesQuery) {
if (labelValuesQuery[1]) {
return this.labelValuesQuery(labelValuesQuery[2], labelValuesQuery[1]);
} else { } else {
return this.labelValuesQuery(label_values_query[2], null); return this.labelValuesQuery(labelValuesQuery[2], null);
} }
} }
var metric_names_query = this.query.match(metric_names_regex); var metricNamesQuery = this.query.match(metricNamesRegex);
if (metric_names_query) { if (metricNamesQuery) {
return this.metricNameQuery(metric_names_query[1]); return this.metricNameQuery(metricNamesQuery[1]);
} }
var query_result_query = this.query.match(query_result_regex); var queryResultQuery = this.query.match(queryResultRegex);
if (query_result_query) { if (queryResultQuery) {
return this.queryResultQuery(query_result_query[1]); return this.queryResultQuery(queryResultQuery[1]);
} }
// if query contains full metric name, return metric name and label list // if query contains full metric name, return metric name and label list
...@@ -67,45 +73,71 @@ function (_) { ...@@ -67,45 +73,71 @@ function (_) {
} }
}; };
PrometheusMetricFindQuery.prototype.labelsQuery = function(metric) {
var url;
url = '/api/v1/series?match[]=' + encodeURIComponent(metric)
+ '&start=' + (this.range.from.valueOf() / 1000)
+ '&end=' + (this.range.to.valueOf() / 1000);
return this.datasource._request('GET', url)
.then(function(result) {
var tags = {};
_.each(result.data.data, function(metric) {
_.each(metric, function(value, key) {
if (key === "__name__") {
return;
}
tags[key] = key;
});
});
return _.map(tags, function(value) {
return {text: value, value: value};
});
});
};
PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) { PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) {
var url = '/api/v1/label/__name__/values'; var url = '/api/v1/label/__name__/values';
return this.datasource._request('GET', url) return this.datasource._request('GET', url)
.then(function(result) { .then(function(result) {
return _.chain(result.data.data) return _.chain(result.data.data)
.filter(function(metricName) { .filter(function(metricName) {
var r = new RegExp(metricFilterPattern); var r = new RegExp(metricFilterPattern);
return r.test(metricName); return r.test(metricName);
}) })
.map(function(matchedMetricName) { .map(function(matchedMetricName) {
return { return {
text: matchedMetricName, text: matchedMetricName,
expandable: true expandable: true
}; };
}) })
.value(); .value();
}); });
}; };
PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) { PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) {
var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000); var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000);
return this.datasource._request('GET', url) return this.datasource._request('GET', url)
.then(function(result) { .then(function(result) {
return _.map(result.data.data.result, function(metricData) { return _.map(result.data.data.result, function(metricData) {
var text = metricData.metric.__name__ || ''; var text = metricData.metric.__name__ || '';
delete metricData.metric.__name__; delete metricData.metric.__name__;
text += '{' + text += '{' +
_.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') +
'}'; '}';
text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000;
return { return {
text: text, text: text,
expandable: true expandable: true
}; };
});
}); });
});
}; };
PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) { PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) {
...@@ -115,14 +147,14 @@ function (_) { ...@@ -115,14 +147,14 @@ function (_) {
var self = this; var self = this;
return this.datasource._request('GET', url) return this.datasource._request('GET', url)
.then(function(result) { .then(function(result) {
return _.map(result.data.data, function(metric) { return _.map(result.data.data, function(metric) {
return { return {
text: self.datasource.getOriginalMetricName(metric), text: self.datasource.getOriginalMetricName(metric),
expandable: true expandable: true
}; };
});
}); });
});
}; };
return PrometheusMetricFindQuery; return PrometheusMetricFindQuery;
......
<query-editor-row query-ctrl="ctrl" can-collapse="false"> <query-editor-row query-ctrl="ctrl" can-collapse="false" has-text-edit-mode="true">
<div class="gf-form-inline">
<div class="gf-form" ng-if="!ctrl.target.editorMode">
<input type="text" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck="false" ng-blur="ctrl.refresh()"></input>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.editorMode">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-8 query-keyword">Query</label> <label class="gf-form-label width-8 query-keyword">Query</label>
<metric-segment segment="ctrl.metricSegment" get-options="ctrl.getMetricOptions()" on-change="ctrl.queryChanged()"></metric-segment> <metric-segment segment="ctrl.metricSegment" get-options="ctrl.getMetricOptions()" on-change="ctrl.queryChanged()"></metric-segment>
......
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
QueryPartDef, QueryPartDef,
QueryPart, QueryPart,
functionRenderer, functionRenderer,
suffixRenderer,
identityRenderer, identityRenderer,
quotedIdentityRenderer, quotedIdentityRenderer,
} from 'app/core/components/query_part/query_part'; } from 'app/core/components/query_part/query_part';
...@@ -12,6 +11,7 @@ import _ from 'lodash'; ...@@ -12,6 +11,7 @@ import _ from 'lodash';
var index = []; var index = [];
var categories = { var categories = {
Functions: [], Functions: [],
GroupBy: [],
}; };
export class PromQuery { export class PromQuery {
...@@ -29,6 +29,7 @@ export class PromQuery { ...@@ -29,6 +29,7 @@ export class PromQuery {
this.target.expr = this.target.expr || ''; this.target.expr = this.target.expr || '';
this.target.intervalFactor = this.target.intervalFactor || 2; this.target.intervalFactor = this.target.intervalFactor || 2;
this.target.functions = this.target.functions || []; this.target.functions = this.target.functions || [];
this.target.editorMode = this.target.editorMode || true;
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.scopedVars = scopedVars; this.scopedVars = scopedVars;
...@@ -80,6 +81,10 @@ function addFunctionStrategy(model, partModel) { ...@@ -80,6 +81,10 @@ function addFunctionStrategy(model, partModel) {
model.target.functions.push(partModel.part); model.target.functions.push(partModel.part);
} }
function groupByLabelRenderer(part, innerExpr) {
return innerExpr + ' by(' + part.params.join(',') + ')';
}
register({ register({
type: 'rate', type: 'rate',
addStrategy: addFunctionStrategy, addStrategy: addFunctionStrategy,
...@@ -89,6 +94,26 @@ register({ ...@@ -89,6 +94,26 @@ register({
renderer: functionRenderer, renderer: functionRenderer,
}); });
register({
type: 'sum',
addStrategy: addFunctionStrategy,
category: categories.Functions,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
register({
type: 'by',
addStrategy: addFunctionStrategy,
category: categories.Functions,
params: [
{name: "label", type: "string", dynamicLookup: true}
],
defaultParams: [],
renderer: groupByLabelRenderer,
});
export function getQueryPartCategories() { export function getQueryPartCategories() {
return categories; return categories;
} }
...@@ -20,7 +20,7 @@ class PrometheusQueryCtrl extends QueryCtrl { ...@@ -20,7 +20,7 @@ class PrometheusQueryCtrl extends QueryCtrl {
linkToPrometheus: any; linkToPrometheus: any;
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, private templateSrv, private uiSegmentSrv) { constructor($scope, $injector, private templateSrv, private uiSegmentSrv, private $rootScope) {
super($scope, $injector); super($scope, $injector);
this.query = new PromQuery(this.target, templateSrv); this.query = new PromQuery(this.target, templateSrv);
...@@ -58,6 +58,39 @@ class PrometheusQueryCtrl extends QueryCtrl { ...@@ -58,6 +58,39 @@ class PrometheusQueryCtrl extends QueryCtrl {
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
getPartOptions(part) {
if (part.def.type === 'by') {
return this.datasource.metricFindQuery('labels(' + this.target.metric + ')')
.then(this.transformToSegments(true))
.catch(this.handleQueryError.bind(true));
}
}
partUpdated(part) {
this.target.expr = this.query.render();
this.panelCtrl.refresh();
}
handleQueryError(err) {
this.$rootScope.appEvent('alert-error', ['Query failed', err.message]);
}
transformToSegments(addTemplateVars) {
return (results) => {
var segments = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
for (let variable of this.templateSrv.variables) {
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true }));
}
}
return segments;
};
}
getMetricOptions() { getMetricOptions() {
return this.datasource.performSuggestQuery('').then(res => { return this.datasource.performSuggestQuery('').then(res => {
return _.map(res, metric => { return _.map(res, metric => {
...@@ -68,6 +101,13 @@ class PrometheusQueryCtrl extends QueryCtrl { ...@@ -68,6 +101,13 @@ class PrometheusQueryCtrl extends QueryCtrl {
queryChanged() { queryChanged() {
this.target.metric = this.metricSegment.value; this.target.metric = this.metricSegment.value;
this.target.expr = this.query.render();
this.refresh();
}
toggleEditorMode() {
this.target.expr = this.query.render(false);
this.target.editorMode = !this.target.editorMode;
} }
refreshMetricData() { refreshMetricData() {
......
...@@ -22,18 +22,22 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -22,18 +22,22 @@ describe('PrometheusMetricFindQuery', function() {
describe('When performing metricFindQuery', function() { describe('When performing metricFindQuery', function() {
var results; var results;
var response; var response;
it('label_values(resource) should generate label search query', function() { it('label_values(resource) should generate label search query', function() {
response = { response = {
status: "success", status: "success",
data: ["value1", "value2", "value3"] data: ["value1", "value2", "value3"]
}; };
ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv);
pm.process().then(function(data) { results = data; }); pm.process().then(function(data) { results = data; });
ctx.$httpBackend.flush(); ctx.$httpBackend.flush();
ctx.$rootScope.$apply(); ctx.$rootScope.$apply();
expect(results.length).to.be(3); expect(results.length).to.be(3);
}); });
it('label_values(metric, resource) should generate series query', function() { it('label_values(metric, resource) should generate series query', function() {
response = { response = {
status: "success", status: "success",
...@@ -43,13 +47,16 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -43,13 +47,16 @@ describe('PrometheusMetricFindQuery', function() {
{__name__: "metric", resource: "value3"} {__name__: "metric", resource: "value3"}
] ]
}; };
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) { results = data; }); pm.process().then(function(data) { results = data; });
ctx.$httpBackend.flush(); ctx.$httpBackend.flush();
ctx.$rootScope.$apply(); ctx.$rootScope.$apply();
expect(results.length).to.be(3); expect(results.length).to.be(3);
}); });
it('label_values(metric, resource) should pass correct time', function() { it('label_values(metric, resource) should pass correct time', function() {
ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') }); ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') });
ctx.$httpBackend.expect('GET', ctx.$httpBackend.expect('GET',
...@@ -59,6 +66,7 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -59,6 +66,7 @@ describe('PrometheusMetricFindQuery', function() {
ctx.$httpBackend.flush(); ctx.$httpBackend.flush();
ctx.$rootScope.$apply(); ctx.$rootScope.$apply();
}); });
it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() {
response = { response = {
status: "success", status: "success",
...@@ -68,6 +76,7 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -68,6 +76,7 @@ describe('PrometheusMetricFindQuery', function() {
{__name__: "metric", resource: "value3"} {__name__: "metric", resource: "value3"}
] ]
}; };
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) { results = data; }); pm.process().then(function(data) { results = data; });
...@@ -75,18 +84,22 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -75,18 +84,22 @@ describe('PrometheusMetricFindQuery', function() {
ctx.$rootScope.$apply(); ctx.$rootScope.$apply();
expect(results.length).to.be(3); expect(results.length).to.be(3);
}); });
it('metrics(metric.*) should generate metric name query', function() { it('metrics(metric.*) should generate metric name query', function() {
response = { response = {
status: "success", status: "success",
data: ["metric1","metric2","metric3","nomatch"] data: ["metric1","metric2","metric3","nomatch"]
}; };
ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv);
pm.process().then(function(data) { results = data; }); pm.process().then(function(data) { results = data; });
ctx.$httpBackend.flush(); ctx.$httpBackend.flush();
ctx.$rootScope.$apply(); ctx.$rootScope.$apply();
expect(results.length).to.be(3); expect(results.length).to.be(3);
}); });
it('query_result(metric) should generate metric name query', function() { it('query_result(metric) should generate metric name query', function() {
response = { response = {
status: "success", status: "success",
...@@ -98,6 +111,7 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -98,6 +111,7 @@ describe('PrometheusMetricFindQuery', function() {
}] }]
} }
}; };
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv);
pm.process().then(function(data) { results = data; }); pm.process().then(function(data) { results = data; });
...@@ -106,5 +120,28 @@ describe('PrometheusMetricFindQuery', function() { ...@@ -106,5 +120,28 @@ describe('PrometheusMetricFindQuery', function() {
expect(results.length).to.be(1); expect(results.length).to.be(1);
expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000');
}); });
it('labels(metric) should generate series query', function() {
response = {
status: "success",
data: [
{__name__: "metric", resource: "value1", app: "app1"},
{__name__: "metric", resource: "value2", app: "app2"},
{__name__: "metric", resource: "value3", server: "server1"}
]
};
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'labels(metric)', ctx.timeSrv);
pm.process().then(function(data) { results = data; });
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
expect(results[0].text).to.be('resource');
expect(results[1].text).to.be('app');
expect(results[2].text).to.be('server');
});
}); });
}); });
...@@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; ...@@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {PromQuery} from '../prom_query'; import {PromQuery} from '../prom_query';
describe.only('PromQuery', function() { describe('PromQuery', function() {
var templateSrv = {replace: val => val}; var templateSrv = {replace: val => val};
describe('render series with mesurement only', function() { describe('render series with mesurement only', function() {
...@@ -20,4 +20,19 @@ describe.only('PromQuery', function() { ...@@ -20,4 +20,19 @@ describe.only('PromQuery', function() {
}); });
}); });
describe('render series with group by label', function() {
it('should generate correct query', function() {
var query = new PromQuery({
metric: 'cpu',
functions: [
{type: 'sum', params: []},
{type: 'by', params: ['app']},
]
}, templateSrv, {});
var queryText = query.render();
expect(queryText).to.be('sum(cpu) by(app)');
});
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment