From d61852603797ff7af8d7daf05d14831e9d18d778 Mon Sep 17 00:00:00 2001 From: Adam Heinz Date: Wed, 1 Jul 2015 14:45:55 -0400 Subject: [PATCH] Copy/paste influxdb query editor. --- .../datasource/elasticsearch/datasource.js | 8 +- .../elasticsearch/partials/query.editor.html | 243 +++++++++++++++ .../datasource/elasticsearch/plugin.json | 1 + .../datasource/elasticsearch/queryBuilder.js | 1 - .../datasource/elasticsearch/queryCtrl.js | 289 ++++++++++++++++++ 5 files changed, 536 insertions(+), 6 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/partials/query.editor.html create mode 100644 public/app/plugins/datasource/elasticsearch/queryCtrl.js diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 9c1eacc78c2..d70c781ca6d 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -5,6 +5,7 @@ define([ 'kbn', 'moment', './queryBuilder', + './queryCtrl', './directives' ], function (angular, _, config, kbn, moment, ElasticQueryBuilder) { @@ -295,21 +296,18 @@ function (angular, _, config, kbn, moment, ElasticQueryBuilder) { ElasticDatasource.prototype.query = function(options) { var self = this; - var allQueries = _.map(options.targets, function(target) { + var allQueries = options.targets.map(function(target) { if (target.hide) { return []; } - var queryBuilder = new ElasticQueryBuilder(target); var query = queryBuilder.build(); query = query.replace(/\$interval/g, target.interval || options.interval); query = query.replace(/\$rangeFrom/g, options.range.from); query = query.replace(/\$rangeTo/g, options.range.to); query = query.replace(/\$maxDataPoints/g, options.maxDataPoints); + query = templateSrv.replace(query, options.scopedVars); return query; - }).join("\n"); - // replace templated variables - // allQueries = templateSrv.replace(allQueries, options.scopedVars); return this._post('/_search?search_type=count', allQueries).then(function(results) { if (!results || !results.facets) { return { data: [] }; diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html new file mode 100644 index 00000000000..ac74c3d661d --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html @@ -0,0 +1,243 @@ +
+ +
+
+ + + + + + + + +
+
+ +
+
    +
  • + +
  • +
  • + FROM +
  • +
  • + +
  • +
+ +
+
+ +
+
    +
  • + +
  • + +
  • + WHERE +
  • + +
  • + +
  • +
+
+
+ +
+ +
    +
  • + Alias pattern +
  • +
  • + +
  • +
+
+
+ +
+
+ +
+
+
    +
  • + +
  • +
  • + Group by time interval +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + +
+ +
+
+ +
+
Alias patterns
+
    +
  • $m = replaced with measurement name
  • +
  • $measurement = replaced with measurement name
  • +
  • $tag_hostname = replaced with the value of the hostname tag
  • +
  • You can also use [[tag_hostname]] pattern replacement syntax
  • +
+
+ +
+
Stacking and fill
+
    +
  • When stacking is enabled it important that points align
  • +
  • If there are missing points for one series it can cause gaps or missing bars
  • +
  • You must use fill(0), and select a group by time low limit
  • +
  • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
  • +
  • This will insert zeros for series that are missing measurements and will make stacking work properly
  • +
+
+ +
+
Group by time
+
    +
  • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
  • +
  • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
  • +
  • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
  • +
  • The low limit can only be set in the group by time option below your queries
  • +
  • You set a low limit by adding a greater sign before the interval
  • +
  • Example: >60s if you write metrics to ElasticDB every 60 seconds
  • +
+
+ + +
+
+ + diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index bd467222a93..5a0ca88fba4 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -9,6 +9,7 @@ "partials": { "config": "app/plugins/datasource/elasticsearch/partials/config.html", + "query": "app/plugins/datasource/elasticsearch/partials/query.editor.html", "annotations": "app/plugins/datasource/elasticsearch/partials/annotations.editor.html" }, diff --git a/public/app/plugins/datasource/elasticsearch/queryBuilder.js b/public/app/plugins/datasource/elasticsearch/queryBuilder.js index 0d77be464f5..4c5c886c73d 100644 --- a/public/app/plugins/datasource/elasticsearch/queryBuilder.js +++ b/public/app/plugins/datasource/elasticsearch/queryBuilder.js @@ -20,7 +20,6 @@ function () { } } }, - "fields": [], "query": { "filtered": { "filter": { diff --git a/public/app/plugins/datasource/elasticsearch/queryCtrl.js b/public/app/plugins/datasource/elasticsearch/queryCtrl.js new file mode 100644 index 00000000000..36eaa74cafc --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/queryCtrl.js @@ -0,0 +1,289 @@ +define([ + 'angular', + 'lodash', + './queryBuilder', +], +function (angular, _, ElasticQueryBuilder) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ElasticQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { + + $scope.functionList = ['count', 'min', 'max', 'total', 'mean']; + + $scope.functionMenu = _.map($scope.functionList, function(func) { + return { text: func, click: "changeFunction('" + func + "');" }; + }); + + $scope.init = function() { + var target = $scope.target; + target.function = target.function || 'mean'; + target.tags = target.tags || []; + target.groupByTags = target.groupByTags || []; + + $scope.queryBuilder = new ElasticQueryBuilder(target); + + if (!target.measurement) { + $scope.measurementSegment = MetricSegment.newSelectMeasurement(); + } else { + $scope.measurementSegment = new MetricSegment(target.measurement); + } + + $scope.tagSegments = []; + _.each(target.tags, function(tag) { + if (tag.condition) { + $scope.tagSegments.push(MetricSegment.newCondition(tag.condition)); + } + $scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key', cssClass: 'query-segment-key' })); + $scope.tagSegments.push(new MetricSegment.newOperator("=")); + $scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value', cssClass: 'query-segment-value'})); + }); + + $scope.fixTagSegments(); + + $scope.groupBySegments = []; + _.each(target.groupByTags, function(tag) { + $scope.groupBySegments.push(new MetricSegment(tag)); + }); + + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + + $scope.removeTagFilterSegment = new MetricSegment({fake: true, value: '-- remove tag filter --'}); + $scope.removeGroupBySegment = new MetricSegment({fake: true, value: '-- remove group by --'}); + }; + + $scope.fixTagSegments = function() { + var count = $scope.tagSegments.length; + var lastSegment = $scope.tagSegments[Math.max(count-1, 0)]; + + if (!lastSegment || lastSegment.type !== 'plus-button') { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + }; + + $scope.groupByTagUpdated = function(segment, index) { + if (segment.value === $scope.removeGroupBySegment.value) { + $scope.target.groupByTags.splice(index, 1); + $scope.groupBySegments.splice(index, 1); + $scope.$parent.get_data(); + return; + } + + if (index === $scope.groupBySegments.length-1) { + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + } + + segment.type = 'group-by-key'; + segment.fake = false; + + $scope.target.groupByTags[index] = segment.value; + $scope.$parent.get_data(); + }; + + $scope.changeFunction = function(func) { + $scope.target.function = func; + $scope.$parent.get_data(); + }; + + $scope.measurementChanged = function() { + $scope.target.measurement = $scope.measurementSegment.value; + $scope.$parent.get_data(); + }; + + $scope.toggleQueryMode = function () { + $scope.target.rawQuery = !$scope.target.rawQuery; + }; + + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + $scope.getMeasurements = function () { + var query = $scope.queryBuilder.buildExploreQuery('MEASUREMENTS'); + return $scope.datasource.metricFindQuery(query) + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(null, $scope.handleQueryError); + }; + + $scope.handleQueryError = function(err) { + $scope.parserError = err.message || 'Failed to issue metric query'; + return []; + }; + + $scope.transformToSegments = function(results) { + return _.map(results, function(segment) { + return new MetricSegment({ value: segment.text, expandable: segment.expandable }); + }); + }; + + $scope.addTemplateVariableSegments = function(segments) { + _.each(templateSrv.variables, function(variable) { + segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true })); + }); + return segments; + }; + + $scope.getTagsOrValues = function(segment, index) { + var query; + + if (segment.type === 'key' || segment.type === 'plus-button') { + query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); + } else if (segment.type === 'value') { + query = $scope.queryBuilder.buildExploreQuery('TAG_VALUES', $scope.tagSegments[index-2].value); + } else if (segment.type === 'condition') { + return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]); + } + else { + return $q.when([]); + } + + return $scope.datasource.metricFindQuery(query) + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(function(results) { + if (segment.type === 'key') { + results.splice(0, 0, angular.copy($scope.removeTagFilterSegment)); + } + return results; + }) + .then(null, $scope.handleQueryError); + }; + + $scope.getGroupByTagSegments = function(segment) { + var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); + + return $scope.datasource.metricFindQuery(query) + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(function(results) { + if (segment.type !== 'plus-button') { + results.splice(0, 0, angular.copy($scope.removeGroupBySegment)); + } + return results; + }) + .then(null, $scope.handleQueryError); + }; + + $scope.tagSegmentUpdated = function(segment, index) { + $scope.tagSegments[index] = segment; + + // handle remove tag condition + if (segment.value === $scope.removeTagFilterSegment.value) { + $scope.tagSegments.splice(index, 3); + if ($scope.tagSegments.length === 0) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } else if ($scope.tagSegments.length > 2) { + $scope.tagSegments.splice(Math.max(index-1, 0), 1); + if ($scope.tagSegments[$scope.tagSegments.length-1].type !== 'plus-button') { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + } + else { + if (segment.type === 'plus-button') { + if (index > 2) { + $scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND')); + } + $scope.tagSegments.push(MetricSegment.newOperator('=')); + $scope.tagSegments.push(MetricSegment.newFake('select tag value', 'value', 'query-segment-value')); + segment.type = 'key'; + segment.cssClass = 'query-segment-key'; + } + + if ((index+1) === $scope.tagSegments.length) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + + $scope.rebuildTargetTagConditions(); + }; + + $scope.rebuildTargetTagConditions = function() { + var tags = []; + var tagIndex = 0; + _.each($scope.tagSegments, function(segment2, index) { + if (segment2.type === 'key') { + if (tags.length === 0) { + tags.push({}); + } + tags[tagIndex].key = segment2.value; + } + else if (segment2.type === 'value') { + tags[tagIndex].value = segment2.value; + $scope.tagSegments[index-1] = $scope.getTagValueOperator(segment2.value); + } + else if (segment2.type === 'condition') { + tags.push({ condition: segment2.value }); + tagIndex += 1; + } + }); + + $scope.target.tags = tags; + $scope.$parent.get_data(); + }; + + $scope.getTagValueOperator = function(tagValue) { + if (tagValue[0] === '/' && tagValue[tagValue.length - 1] === '/') { + return MetricSegment.newOperator('=~'); + } + + return MetricSegment.newOperator('='); + }; + + function MetricSegment(options) { + if (options === '*' || options.value === '*') { + this.value = '*'; + this.html = $sce.trustAsHtml(''); + this.expandable = true; + return; + } + + if (_.isString(options)) { + this.value = options; + this.html = $sce.trustAsHtml(this.value); + return; + } + + this.cssClass = options.cssClass; + this.type = options.type; + this.fake = options.fake; + this.value = options.value; + this.type = options.type; + this.expandable = options.expandable; + this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); + } + + MetricSegment.newSelectMeasurement = function() { + return new MetricSegment({value: 'select measurement', fake: true}); + }; + + MetricSegment.newFake = function(text, type, cssClass) { + return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass}); + }; + + MetricSegment.newCondition = function(condition) { + return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' }); + }; + + MetricSegment.newOperator = function(op) { + return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' }); + }; + + MetricSegment.newPlusButton = function() { + return new MetricSegment({fake: true, html: '', type: 'plus-button' }); + }; + + MetricSegment.newSelectTagValue = function() { + return new MetricSegment({value: 'select tag value', fake: true}); + }; + + }); + +});