├── .gitignore
├── README.md
├── datasources
├── atlas
│ ├── atlas.js
│ ├── atlasTargetCtrl.js
│ ├── editor.html
│ └── specs
│ │ └── atlasDatasource-specs.js
└── kairosdb
│ ├── kairosdb.Datasource.js
│ ├── kairosdb.TargetCtrl.js
│ └── kairosdb.editor.html
└── features
└── druid
├── README.md
├── datasource.js
├── partials
└── query.editor.html
└── queryCtrl.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
27 | #Mac OS X
28 | .DS_Store
29 |
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Note: The Druid plugin is in [features/druid](features/druid)
2 |
3 | grafana-plugins
4 | ===============
5 |
6 | Extensions, custom & experimental panels
7 |
8 | Examples
9 | ===========
10 |
11 | ### Adding Custom Panels
12 |
13 | ~~~
14 | plugins: {
15 | // list of plugin panels
16 | panels: {
17 | 'custom panel name': { path: '../plugins/panels/custom.panel.example' }
18 | },
19 | // requirejs modules in plugins folder that should be loaded
20 | // for example custom datasources
21 | dependencies: [],
22 | }
23 | ~~~
24 |
25 | ### Adding custom data sources
26 |
27 | ~~~
28 | datasources: {
29 | custom: {
30 | type: 'CustomDatasource',
31 | hello: 'some property'
32 | },
33 | },
34 |
35 | plugins: {
36 | dependencies: ['datasource.example']
37 | },
38 | ~~~
39 |
40 |
--------------------------------------------------------------------------------
/datasources/atlas/atlas.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'angular',
3 | 'lodash',
4 | 'kbn'
5 | ],
6 | function (angular, _, kbn) {
7 | 'use strict';
8 |
9 | var module = angular.module('grafana.services');
10 |
11 | module.factory('AtlasDatasource', function($q, $http) {
12 | function AtlasDatasource(datasource) {
13 | this.name = datasource.name;
14 | this.type = 'AtlasDatasource';
15 | this.url = datasource.url;
16 |
17 | this.supportMetrics = true;
18 | this.partials = datasource.partials || 'plugins/grafana-plugins/datasources/atlas';
19 | this.editorSrc = this.partials + '/editor.html';
20 |
21 | this.minimumInterval = datasource.minimumInterval || 1000;
22 | }
23 |
24 | AtlasDatasource.prototype.query = function(options) {
25 | // Atlas can take multiple concatenated stack queries
26 | var fullQuery = _(options.targets).reject('hide').pluck('query').value().join(',');
27 |
28 | var interval = options.interval;
29 |
30 | if (kbn.interval_to_ms(interval) < this.minimumInterval) {
31 | interval = kbn.secondsToHms(this.minimumInterval / 1000);
32 | }
33 |
34 | var params = {
35 | q: fullQuery,
36 | step: interval,
37 | s: options.range.from,
38 | e: options.range.to,
39 | format: options.format || 'json'
40 | };
41 |
42 | var httpOptions = {
43 | method: 'GET',
44 | url: this.url + '/api/v1/graph',
45 | params: params,
46 | inspect: { type: 'atlas' }
47 | };
48 |
49 | // Note: while Atlas supports PNGs, Grafana can only provide graphite-specific dimension params
50 | // See https://github.com/grafana/grafana/issues/1273 for status
51 | if (options.format === "png") {
52 | var encodedParams = _.map(httpOptions.params, function (v, k) {
53 | return [k, encodeURIComponent(v)].join("=");
54 | });
55 |
56 | return $q.when(httpOptions.url + "?" + encodedParams.join("&"));
57 | } else {
58 | var deferred = $q.defer();
59 | $http(httpOptions)
60 | .success(function (response) {
61 | deferred.resolve(convertToTimeseries(response))
62 | })
63 | .error(function (data, status, headers, config) {
64 | var error = new Error(data.message);
65 | error.config = config;
66 | error.data = JSON.stringify(data);
67 | deferred.reject(error);
68 | });
69 | return deferred.promise;
70 | }
71 | };
72 |
73 | function convertToTimeseries (result) {
74 | var timeseriesData = _.map(result.legend, function (legend, index) {
75 | var series = {target: legend, datapoints: []};
76 | var values = _.pluck(result.values, index);
77 |
78 | for (var i = 0; i < values.length; i++) {
79 | var value = values[i];
80 | var timestamp = result.start + (i * result.step);
81 | series.datapoints.push([value, timestamp]);
82 | }
83 |
84 | return series;
85 | });
86 |
87 | return {data: timeseriesData};
88 | }
89 |
90 | return AtlasDatasource;
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/datasources/atlas/atlasTargetCtrl.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'angular',
3 | 'lodash'
4 | ],
5 | function (angular, _) {
6 | 'use strict';
7 |
8 | var module = angular.module('grafana.controllers');
9 |
10 | var seriesList = null;
11 |
12 | module.controller('AtlasTargetCtrl', function($scope) {
13 |
14 | $scope.init = function() {
15 | };
16 |
17 | $scope.moveMetricQuery = function(fromIndex, toIndex) {
18 | _.move($scope.panel.targets, fromIndex, toIndex);
19 | $scope.get_data();
20 | };
21 |
22 | $scope.duplicate = function() {
23 | var clone = angular.copy($scope.target);
24 | $scope.panel.targets.push(clone);
25 | $scope.get_data();
26 | };
27 |
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/datasources/atlas/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
30 |
31 |
38 |
39 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/datasources/atlas/specs/atlasDatasource-specs.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'helpers',
3 | '../datasources/atlas'
4 | ], function(helpers) {
5 | 'use strict';
6 |
7 | describe('AtlasDatasource', function() {
8 | var ctx = new helpers.ServiceTestContext();
9 |
10 | beforeEach(module('grafana.services'));
11 | beforeEach(ctx.providePhase(['templateSrv']));
12 | beforeEach(ctx.createService('AtlasDatasource'));
13 | beforeEach(function() {
14 | ctx.ds = new ctx.service({ url: [''] });
15 | });
16 |
17 | describe('When querying atlas with multiple stack query targets', function() {
18 | var results;
19 | var urlExpected = "/api/v1/graph?e=now&format=json&q=name,sps,:eq,:sum,name,ssCpuUser,:eq,:sum&s=now-1h&step=10m";
20 | var query = {
21 | range: { from: 'now-1h', to: 'now' },
22 | targets: [
23 | { query: 'name,sps,:eq,:sum' },
24 | { query: 'name,ssCpuUser,:eq,:sum' }
25 | ],
26 | interval: '10m'
27 | };
28 |
29 | var response = {
30 | start: 1419312000000,
31 | step: 600000,
32 | legend: [
33 | "atlas.legacy=epic, name=sps, nf.app=nccp",
34 | "atlas.legacy=epic, name=ssCpuUser, nf.app=alerttest, nf.asg=alerttest-v042, nf.cluster=alerttest, nf.node=alert1"
35 | ],
36 | metrics: [{}],
37 | values:[
38 | [604930.164776, 18.537333],
39 | [593086.155587, 20.939233],
40 | [605539.831071, 20.855982]
41 | ],
42 | notices: []
43 | };
44 |
45 | beforeEach(function() {
46 | ctx.$httpBackend.expect('GET', urlExpected).respond(response);
47 | ctx.ds.query(query).then(function(data) { results = data; });
48 | ctx.$httpBackend.flush();
49 | });
50 |
51 | it('should generate the correct query', function() {
52 | ctx.$httpBackend.verifyNoOutstandingExpectation();
53 | });
54 |
55 | it('should return series list', function() {
56 | expect(results.data.length).to.be(2);
57 | expect(results.data[0].target).to.be('atlas.legacy=epic, name=sps, nf.app=nccp');
58 | expect(results.data[0].datapoints[0]).to.eql([604930.164776, 1419312000000]);
59 | expect(results.data[0].datapoints[1]).to.eql([593086.155587, 1419312000000 + 600000]);
60 | });
61 | });
62 |
63 | describe('when querying for a png', function () {
64 | var query = {
65 | range: { from: 'now-1h', to: 'now' },
66 | targets: [
67 | { query: 'name,sps,:eq,:sum' },
68 | ],
69 | interval: '10m',
70 | format: 'png'
71 | };
72 |
73 | it('should generate the correct query', function () {
74 | var promise = ctx.ds.query(query);
75 | promise.then(function(urlReturned) {
76 | expect(urlReturned).to.match(/format=png/);
77 | });
78 | ctx.$rootScope.$digest();
79 | });
80 | })
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/datasources/kairosdb/kairosdb.Datasource.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'angular',
3 | 'lodash',
4 | 'kbn'
5 | ],
6 | function (angular, _, kbn) {
7 | 'use strict';
8 |
9 | var module = angular.module('grafana.services');
10 | var tagList = null;
11 |
12 | module.factory('KairosDBDatasource', function($q, $http) {
13 |
14 | function KairosDBDatasource(datasource) {
15 | this.type = datasource.type;
16 | this.editorSrc = 'plugins/datasources/kairosdb/kairosdb.editor.html';
17 | this.url = datasource.url;
18 | this.name = datasource.name;
19 | this.supportMetrics = true;
20 | this.grafanaDB = datasource.grafanaDB;
21 | }
22 |
23 | // Called once per panel (graph)
24 | KairosDBDatasource.prototype.query = function(options) {
25 | var start = options.range.from;
26 | var end = options.range.to;
27 |
28 | var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options)));
29 | var plotParams = _.compact(_.map(options.targets, function(target){
30 | var alias = target.alias;
31 | if (typeof target.alias == 'undefined' || target.alias == "")
32 | alias = target.metric;
33 | return !target.hide
34 | ? {alias: alias,
35 | exouter: target.exOuter}
36 | : null;
37 | }));
38 | var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams);
39 | // No valid targets, return the empty result to save a round trip.
40 | if (_.isEmpty(queries)) {
41 | var d = $q.defer();
42 | d.resolve({ data: [] });
43 | return d.promise;
44 | }
45 | return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias,handleQueryError);
46 | };
47 |
48 | ///////////////////////////////////////////////////////////////////////
49 | /// Query methods
50 | ///////////////////////////////////////////////////////////////////////
51 |
52 | KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
53 | var reqBody = {
54 | metrics: queries
55 | };
56 | reqBody.cache_time=0;
57 | convertToKairosTime(start,reqBody,'start');
58 | convertToKairosTime(end,reqBody,'end');
59 | var options = {
60 | method: 'POST',
61 | url: '/api/v1/datapoints/query',
62 | data: reqBody
63 | };
64 |
65 | options.url = this.url + options.url;
66 | return $http(options);
67 | };
68 |
69 | /**
70 | * Gets the list of metrics
71 | * @returns {*|Promise}
72 | */
73 | KairosDBDatasource.prototype.performMetricSuggestQuery = function() {
74 | var options = {
75 | url : this.url + '/api/v1/metricnames',
76 | method : 'GET'
77 | };
78 | return $http(options).then(function(results) {
79 | if (!results.data) {
80 | return [];
81 | }
82 | return results.data.results;
83 | });
84 |
85 | };
86 |
87 | KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname,range,type,keyValue) {
88 | if(tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) &&
89 | (range.to === tagList.range.to)) {
90 | return getTagListFromResponse(tagList.results,type,keyValue);
91 | }
92 | tagList = {
93 | metricName:metricname,
94 | range:range
95 | };
96 | var body = {
97 | metrics : [{name : metricname}]
98 | };
99 | convertToKairosTime(range.from,body,'start');
100 | convertToKairosTime(range.to,body,'end');
101 | var options = {
102 | url : this.url + '/api/v1/datapoints/query/tags',
103 | method : 'POST',
104 | data : body
105 | };
106 | return $http(options).then(function(results) {
107 | tagList.results = results;
108 | return getTagListFromResponse(results,type,keyValue);
109 | });
110 |
111 | };
112 |
113 | /////////////////////////////////////////////////////////////////////////
114 | /// Formatting methods
115 | ////////////////////////////////////////////////////////////////////////
116 |
117 | function getTagListFromResponse(results,type,keyValue) {
118 | if (!results.data) {
119 | return [];
120 | }
121 | if(type==="key") {
122 | return _.keys(results.data.queries[0].results[0].tags);
123 | }
124 | else if(type==="value" && _.has(results.data.queries[0].results[0].tags,keyValue)) {
125 | return results.data.queries[0].results[0].tags[keyValue];
126 | }
127 | return [];
128 | }
129 |
130 | /**
131 | * Requires a verion of KairosDB with every CORS defects fixed
132 | * @param results
133 | * @returns {*}
134 | */
135 | function handleQueryError(results) {
136 | if(results.data.errors && !_.isEmpty(results.data.errors)) {
137 | var errors = {
138 | message: results.data.errors[0]
139 | };
140 | return $q.reject(errors);
141 | }
142 | else{
143 | return $q.reject(results);
144 | }
145 | }
146 |
147 | function handleKairosDBQueryResponse(plotParams, results) {
148 | var output = [];
149 | var index = 0;
150 | _.each(results.data.queries, function (series) {
151 | var sample_size = series.sample_size;
152 | console.log("sample_size:" + sample_size + " samples");
153 |
154 | _.each(series.results, function (result) {
155 |
156 | //var target = result.name;
157 | var target = plotParams[index].alias;
158 | var details = " ( ";
159 | _.each(result.group_by,function(element) {
160 | if(element.name==="tag") {
161 | _.each(element.group,function(value, key) {
162 | details+= key+"="+value+" ";
163 | });
164 | }
165 | else if(element.name==="value") {
166 | details+= 'value_group='+element.group.group_number+" ";
167 | }
168 | else if(element.name==="time") {
169 | details+= 'time_group='+element.group.group_number+" ";
170 | }
171 | });
172 | details+= ") ";
173 | if (details != " ( ) ")
174 | target += details;
175 | var datapoints = [];
176 |
177 | for (var i = 0; i < result.values.length; i++) {
178 | var t = Math.floor(result.values[i][0]);
179 | var v = result.values[i][1];
180 | datapoints[i] = [v, t];
181 | }
182 | if (plotParams[index].exouter)
183 | datapoints = PeakFilter(datapoints, 10);
184 | output.push({ target: target, datapoints: datapoints });
185 | });
186 | index ++;
187 | });
188 | var output2 = { data: _.flatten(output) };
189 |
190 | return output2;
191 | }
192 |
193 | function convertTargetToQuery(options,target) {
194 | if (!target.metric || target.hide) {
195 | return null;
196 | }
197 |
198 | var query = {
199 | name: target.metric
200 | };
201 |
202 | query.aggregators = [];
203 | if(target.downsampling!=='(NONE)') {
204 | query.aggregators.push({
205 | name: target.downsampling,
206 | align_sampling: true,
207 | align_start_time: true,
208 | sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval)
209 | });
210 | }
211 | if(target.horizontalAggregators) {
212 | _.each(target.horizontalAggregators,function(chosenAggregator) {
213 | var returnedAggregator = {
214 | name:chosenAggregator.name
215 | };
216 | if(chosenAggregator.sampling_rate) {
217 | returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate);
218 | returnedAggregator.align_sampling = true;
219 | returnedAggregator.align_start_time=true;
220 | }
221 | if(chosenAggregator.unit) {
222 | returnedAggregator.unit = chosenAggregator.unit+'s';
223 | }
224 | if(chosenAggregator.factor && chosenAggregator.name==='div') {
225 | returnedAggregator.divisor = chosenAggregator.factor;
226 | }
227 | else if(chosenAggregator.factor && chosenAggregator.name==='scale') {
228 | returnedAggregator.factor = chosenAggregator.factor;
229 | }
230 | if(chosenAggregator.percentile) {
231 | returnedAggregator.percentile = chosenAggregator.percentile;
232 | }
233 | query.aggregators.push(returnedAggregator);
234 | });
235 | }
236 | if(_.isEmpty(query.aggregators)) {
237 | delete query.aggregators;
238 | }
239 |
240 | if(target.tags) {
241 | query.tags = angular.copy(target.tags);
242 | }
243 |
244 | if(target.groupByTags || target.nonTagGroupBys) {
245 | query.group_by = [];
246 | if(target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});}
247 | if(target.nonTagGroupBys) {
248 | _.each(target.nonTagGroupBys,function(rawGroupBy) {
249 | var formattedGroupBy = angular.copy(rawGroupBy);
250 | if(formattedGroupBy.name==='time') {
251 | formattedGroupBy.range_size=KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size);
252 | }
253 | query.group_by.push(formattedGroupBy);
254 | });
255 | }
256 | }
257 | return query;
258 | }
259 |
260 | ///////////////////////////////////////////////////////////////////////
261 | /// Time conversion functions specifics to KairosDB
262 | //////////////////////////////////////////////////////////////////////
263 |
264 | KairosDBDatasource.prototype.convertToKairosInterval = function(intervalString) {
265 | var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;
266 | var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/;
267 | var matches = intervalString.match(interval_regex_ms);
268 | if(!matches) {
269 | matches = intervalString.match(interval_regex);
270 | }
271 | if (!matches) {
272 | throw new Error('Invalid interval string, expecting a number followed by one of "y M w d h m s ms"');
273 | }
274 |
275 | var value = matches[1];
276 | var unit = matches[2];
277 | if (value%1!==0) {
278 | if(unit==='ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');}
279 | value = Math.round(kbn.intervals_in_seconds[unit]*value*1000);
280 | unit = 'ms';
281 |
282 | }
283 | switch(unit) {
284 | case 'ms':
285 | unit = 'milliseconds';
286 | break;
287 | case 's':
288 | unit = 'seconds';
289 | break;
290 | case 'm':
291 | unit = 'minutes';
292 | break;
293 | case 'h':
294 | unit = 'hours';
295 | break;
296 | case 'd':
297 | unit = 'days';
298 | break;
299 | case 'w':
300 | unit = 'weeks';
301 | break;
302 | case 'M':
303 | unit = 'months';
304 | break;
305 | case 'y':
306 | unit = 'years';
307 | break;
308 | default:
309 | console.log("Unknown interval ", intervalString);
310 | break;
311 | }
312 |
313 | return {
314 | "value": value,
315 | "unit": unit
316 | };
317 |
318 | };
319 |
320 | function convertToKairosTime(date, response_obj, start_stop_name) {
321 | var name;
322 | if (_.isString(date)) {
323 | if (date === 'now') {
324 | return;
325 | }
326 | else if (date.indexOf('now-') >= 0) {
327 |
328 | name = start_stop_name + "_relative";
329 |
330 | date = date.substring(4);
331 | var re_date = /(\d+)\s*(\D+)/;
332 | var result = re_date.exec(date);
333 | if (result) {
334 | var value = result[1];
335 | var unit = result[2];
336 | switch(unit) {
337 | case 'ms':
338 | unit = 'milliseconds';
339 | break;
340 | case 's':
341 | unit = 'seconds';
342 | break;
343 | case 'm':
344 | unit = 'minutes';
345 | break;
346 | case 'h':
347 | unit = 'hours';
348 | break;
349 | case 'd':
350 | unit = 'days';
351 | break;
352 | case 'w':
353 | unit = 'weeks';
354 | break;
355 | case 'M':
356 | unit = 'months';
357 | break;
358 | case 'y':
359 | unit = 'years';
360 | break;
361 | default:
362 | console.log("Unknown date ", date);
363 | break;
364 | }
365 | response_obj[name] = {
366 | "value": value,
367 | "unit": unit
368 | };
369 | return;
370 | }
371 | console.log("Unparseable date", date);
372 | return;
373 | }
374 | date = kbn.parseDate(date);
375 | }
376 |
377 | if(_.isDate(date)) {
378 | name = start_stop_name + "_absolute";
379 | response_obj[name] = date.getTime();
380 | return;
381 | }
382 |
383 | console.log("Date is neither string nor date");
384 | }
385 |
386 | function PeakFilter(dataIn, limit) {
387 | var datapoints = dataIn;
388 | var arrLength = datapoints.length;
389 | if (arrLength <= 3)
390 | return datapoints;
391 | var LastIndx = arrLength - 1;
392 |
393 | // Check first point
394 | var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]);
395 | var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]);
396 | if (prvDelta >= limit && nxtDelta < limit)
397 | datapoints[0][0] = datapoints[1][0];
398 |
399 | // Check last point
400 | prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]);
401 | nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]);
402 | if (prvDelta >= limit && nxtDelta < limit)
403 | datapoints[LastIndx][0] = datapoints[LastIndx - 1][0];
404 |
405 | for (var i = 1; i < arrLength - 1; i++){
406 | prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]);
407 | nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]);
408 | if (prvDelta >= limit && nxtDelta >= limit)
409 | datapoints[i][0] = (datapoints[i-1][0] + datapoints[i+1][0]) / 2;
410 | }
411 |
412 | return datapoints;
413 | }
414 |
415 | ////////////////////////////////////////////////////////////////////////
416 | return KairosDBDatasource;
417 | });
418 |
419 | });
420 |
--------------------------------------------------------------------------------
/datasources/kairosdb/kairosdb.TargetCtrl.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'angular',
3 | 'lodash'
4 | ],
5 | function (angular, _) {
6 | 'use strict';
7 |
8 | var module = angular.module('grafana.controllers');
9 | var MetricStruct = {};
10 | var metricList = null;
11 | var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
12 |
13 | module.controller('KairosDBTargetCtrl', function($scope) {
14 |
15 | $scope.init = function() {
16 | $scope.metric = {
17 | list: ["Loading..."],
18 | value: "Loading..."
19 | };
20 | $scope.panel.stack = false;
21 | if (!$scope.panel.downsampling) {
22 | $scope.panel.downsampling = 'avg';
23 | }
24 | if (!$scope.target.downsampling) {
25 | $scope.target.downsampling = $scope.panel.downsampling;
26 | $scope.target.sampling = $scope.panel.sampling;
27 | }
28 | $scope.targetLetters = targetLetters;
29 | $scope.updateMetricList();
30 | $scope.target.errors = validateTarget($scope.target);
31 | };
32 |
33 | $scope.targetBlur = function() {
34 | $scope.target.metric = $scope.metric.value;
35 | $scope.target.errors = validateTarget($scope.target);
36 | if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
37 | $scope.oldTarget = angular.copy($scope.target);
38 | $scope.get_data();
39 | }
40 | };
41 | $scope.panelBlur = function() {
42 | _.each($scope.panel.targets, function(target) {
43 | target.downsampling = $scope.panel.downsampling;
44 | target.sampling = $scope.panel.sampling;
45 | });
46 | $scope.get_data();
47 | };
48 |
49 | $scope.duplicate = function() {
50 | var clone = angular.copy($scope.target);
51 | $scope.panel.targets.push(clone);
52 | };
53 | $scope.moveMetricQuery = function(fromIndex, toIndex) {
54 | _.move($scope.panel.targets, fromIndex, toIndex);
55 | };
56 |
57 | //////////////////////////////
58 | // SUGGESTION QUERIES
59 | //////////////////////////////
60 |
61 | $scope.updateMetricList = function() {
62 | $scope.metricListLoading = true;
63 | metricList = [];
64 | $scope.datasource.performMetricSuggestQuery().then(function(series) {
65 | metricList = series;
66 | $scope.metric.list = series;
67 | if ($scope.target.metric)
68 | $scope.metric.value = $scope.target.metric;
69 | else
70 | $scope.metric.value = "";
71 | $scope.metricListLoading = false;
72 | return metricList;
73 | });
74 | };
75 |
76 | $scope.suggestTagKeys = function(query, callback) {
77 | $scope.updateTimeRange();
78 | callback($scope.datasource
79 | .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'key',''));
80 |
81 | };
82 |
83 | $scope.suggestTagValues = function(query, callback) {
84 | callback($scope.datasource
85 | .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'value',$scope.target.currentTagKey));
86 | };
87 |
88 | //////////////////////////////
89 | // FILTER by TAG
90 | //////////////////////////////
91 |
92 | $scope.addFilterTag = function() {
93 | if (!$scope.addFilterTagMode) {
94 | $scope.addFilterTagMode = true;
95 | $scope.validateFilterTag();
96 | return;
97 | }
98 |
99 | if (!$scope.target.tags) {
100 | $scope.target.tags = {};
101 | }
102 |
103 | $scope.validateFilterTag();
104 | if (!$scope.target.errors.tags) {
105 | if(!_.has($scope.target.tags,$scope.target.currentTagKey)) {
106 | $scope.target.tags[$scope.target.currentTagKey] = [];
107 | }
108 | $scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue);
109 | $scope.target.currentTagKey = '';
110 | $scope.target.currentTagValue = '';
111 | $scope.targetBlur();
112 | }
113 |
114 | $scope.addFilterTagMode = false;
115 | };
116 |
117 | $scope.removeFilterTag = function(key) {
118 | delete $scope.target.tags[key];
119 | if(_.size($scope.target.tags)===0) {
120 | $scope.target.tags = null;
121 | }
122 | $scope.targetBlur();
123 | };
124 |
125 | $scope.validateFilterTag = function() {
126 | $scope.target.errors.tags = null;
127 | if(!$scope.target.currentTagKey || !$scope.target.currentTagValue) {
128 | $scope.target.errors.tags = "You must specify a tag name and value.";
129 | }
130 | };
131 |
132 | //////////////////////////////
133 | // GROUP BY
134 | //////////////////////////////
135 |
136 | $scope.addGroupBy = function() {
137 | if (!$scope.addGroupByMode) {
138 | $scope.addGroupByMode = true;
139 | $scope.target.currentGroupByType = 'tag';
140 | $scope.isTagGroupBy = true;
141 | $scope.validateGroupBy();
142 | return;
143 | }
144 | $scope.validateGroupBy();
145 | // nb: if error is found, means that user clicked on cross : cancels input
146 | if (_.isEmpty($scope.target.errors.groupBy)) {
147 | if($scope.isTagGroupBy) {
148 | if (!$scope.target.groupByTags) {
149 | $scope.target.groupByTags = [];
150 | }
151 | console.log($scope.target.groupBy.tagKey);
152 | if (!_.contains($scope.target.groupByTags, $scope.target.groupBy.tagKey)) {
153 | $scope.target.groupByTags.push($scope.target.groupBy.tagKey);
154 | $scope.targetBlur();
155 | }
156 | $scope.target.groupBy.tagKey = '';
157 | }
158 | else {
159 | if (!$scope.target.nonTagGroupBys) {
160 | $scope.target.nonTagGroupBys = [];
161 | }
162 | var groupBy = {
163 | name: $scope.target.currentGroupByType
164 | };
165 | if($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;}
166 | else if($scope.isTimeGroupBy) {
167 | groupBy.range_size = $scope.target.groupBy.timeInterval;
168 | groupBy.group_count = $scope.target.groupBy.groupCount;
169 | }
170 | $scope.target.nonTagGroupBys.push(groupBy);
171 | }
172 | $scope.targetBlur();
173 | }
174 | $scope.isTagGroupBy = false;
175 | $scope.isValueGroupBy = false;
176 | $scope.isTimeGroupBy = false;
177 | $scope.addGroupByMode = false;
178 | };
179 |
180 | $scope.removeGroupByTag = function(index) {
181 | $scope.target.groupByTags.splice(index, 1);
182 | if(_.size($scope.target.groupByTags)===0) {
183 | $scope.target.groupByTags = null;
184 | }
185 | $scope.targetBlur();
186 | };
187 |
188 | $scope.removeNonTagGroupBy = function(index) {
189 | $scope.target.nonTagGroupBys.splice(index, 1);
190 | if(_.size($scope.target.nonTagGroupBys)===0) {
191 | $scope.target.nonTagGroupBys = null;
192 | }
193 | $scope.targetBlur();
194 | };
195 |
196 | $scope.changeGroupByInput = function() {
197 | $scope.isTagGroupBy = $scope.target.currentGroupByType==='tag';
198 | $scope.isValueGroupBy = $scope.target.currentGroupByType==='value';
199 | $scope.isTimeGroupBy = $scope.target.currentGroupByType==='time';
200 | $scope.validateGroupBy();
201 | };
202 |
203 | $scope.validateGroupBy = function() {
204 | delete $scope.target.errors.groupBy;
205 | var errors = {};
206 | $scope.isGroupByValid = true;
207 | if($scope.isTagGroupBy) {
208 | if(!$scope.target.groupBy.tagKey) {
209 | $scope.isGroupByValid = false;
210 | errors.tagKey = 'You must supply a tag name';
211 | }
212 | }
213 | if($scope.isValueGroupBy) {
214 | if(!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) {
215 | errors.valueRange = "Range must be an integer";
216 | $scope.isGroupByValid = false;
217 | }
218 | }
219 | if($scope.isTimeGroupBy) {
220 | try {
221 | $scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval);
222 | } catch(err) {
223 | errors.timeInterval = err.message;
224 | $scope.isGroupByValid = false;
225 | }
226 | if(!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) {
227 | errors.groupCount = "Group count must be an integer";
228 | $scope.isGroupByValid = false;
229 | }
230 | }
231 |
232 | if(!_.isEmpty(errors)) {
233 | $scope.target.errors.groupBy = errors;
234 | }
235 | };
236 |
237 | function isInt(n) {
238 | return parseInt(n) % 1 === 0;
239 | }
240 |
241 | //////////////////////////////
242 | // HORIZONTAL AGGREGATION
243 | //////////////////////////////
244 |
245 | $scope.addHorizontalAggregator = function() {
246 | if (!$scope.addHorizontalAggregatorMode) {
247 | $scope.addHorizontalAggregatorMode = true;
248 | $scope.target.currentHorizontalAggregatorName = 'avg';
249 | $scope.hasSamplingRate = true;
250 | $scope.validateHorizontalAggregator();
251 | return;
252 | }
253 |
254 | $scope.validateHorizontalAggregator();
255 | // nb: if error is found, means that user clicked on cross : cancels input
256 | if(_.isEmpty($scope.target.errors.horAggregator)) {
257 | if (!$scope.target.horizontalAggregators) {
258 | $scope.target.horizontalAggregators = [];
259 | }
260 | var aggregator = {
261 | name:$scope.target.currentHorizontalAggregatorName
262 | };
263 | if($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;}
264 | if($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;}
265 | if($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;}
266 | if($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;}
267 | $scope.target.horizontalAggregators.push(aggregator);
268 | $scope.targetBlur();
269 | }
270 |
271 | $scope.addHorizontalAggregatorMode = false;
272 | $scope.hasSamplingRate = false;
273 | $scope.hasUnit = false;
274 | $scope.hasFactor = false;
275 | $scope.hasPercentile = false;
276 |
277 | };
278 |
279 | $scope.removeHorizontalAggregator = function(index) {
280 | $scope.target.horizontalAggregators.splice(index, 1);
281 | if(_.size($scope.target.horizontalAggregators)===0) {
282 | $scope.target.horizontalAggregators = null;
283 | }
284 |
285 | $scope.targetBlur();
286 | };
287 |
288 | $scope.changeHorAggregationInput = function() {
289 | $scope.hasSamplingRate = _.contains(['avg','dev','max','min','sum','least_squares','count','percentile'],
290 | $scope.target.currentHorizontalAggregatorName);
291 | $scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName);
292 | $scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName);
293 | $scope.hasPercentile = 'percentile'===$scope.target.currentHorizontalAggregatorName;
294 | $scope.validateHorizontalAggregator();
295 | };
296 |
297 | $scope.validateHorizontalAggregator = function() {
298 | delete $scope.target.errors.horAggregator;
299 | var errors = {};
300 | $scope.isAggregatorValid = true;
301 | if($scope.hasSamplingRate) {
302 | try {
303 | $scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate);
304 | } catch(err) {
305 | errors.samplingRate = err.message;
306 | $scope.isAggregatorValid = false;
307 | }
308 | }
309 | if($scope.hasFactor) {
310 | if(!$scope.target.horAggregator.factor) {
311 | errors.factor = 'You must supply a numeric value for this aggregator';
312 | $scope.isAggregatorValid = false;
313 | }
314 | else if(parseInt($scope.target.horAggregator.factor)===0 && $scope.target.currentHorizontalAggregatorName==='div') {
315 | errors.factor = 'Cannot divide by 0';
316 | $scope.isAggregatorValid = false;
317 | }
318 | }
319 | if($scope.hasPercentile) {
320 | if(!$scope.target.horAggregator.percentile ||
321 | $scope.target.horAggregator.percentile<=0 ||
322 | $scope.target.horAggregator.percentile>1) {
323 | errors.percentile = 'Percentile must be between 0 and 1';
324 | $scope.isAggregatorValid = false;
325 | }
326 | }
327 |
328 | if(!_.isEmpty(errors)) {
329 | $scope.target.errors.horAggregator = errors;
330 | }
331 | };
332 |
333 | $scope.alert = function(message) {
334 | alert(message);
335 | };
336 |
337 | //////////////////////////////
338 | // VALIDATION
339 | //////////////////////////////
340 |
341 | function MetricListToObject(MetricList) {
342 | var result = {};
343 | var Metric;
344 | var MetricArray = [];
345 | var MetricCnt = 0;
346 | for (var i =0;i < MetricList.length; i++) {
347 | Metric = MetricList[i];
348 | MetricArray = Metric.split('.');
349 | if(!result.hasOwnProperty(MetricArray[0])) {
350 | result[MetricArray[0]] = {};
351 | }
352 | if(!result[MetricArray[0]].hasOwnProperty(MetricArray[1])) {
353 | result[MetricArray[0]][MetricArray[1]] = [];
354 | }
355 | result[MetricArray[0]][MetricArray[1]].push(MetricArray[2]);
356 | }
357 | return result;
358 | }
359 |
360 | function validateTarget(target) {
361 | var errs = {};
362 |
363 | if (!target.metric) {
364 | errs.metric = "You must supply a metric name.";
365 | }
366 |
367 | try {
368 | if (target.sampling) {
369 | $scope.datasource.convertToKairosInterval(target.sampling);
370 | }
371 | } catch(err) {
372 | errs.sampling = err.message;
373 | }
374 |
375 | return errs;
376 | }
377 |
378 | });
379 |
380 | });
--------------------------------------------------------------------------------
/datasources/kairosdb/kairosdb.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
50 |
51 |
52 | -
53 | {{targetLetters[$index]}}
54 |
55 | -
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
396 |
397 |
398 |
399 |
400 |
435 |
--------------------------------------------------------------------------------
/features/druid/README.md:
--------------------------------------------------------------------------------
1 | Grafana plugin for [Druid](http://druid.io/) real-time OLAP database.
2 |
3 | 
4 |
5 | ## Status
6 |
7 | This plugin is experimental. It's usable but still needs TLC. In particular, auto-completion for dimension names would really help. It supports timeseries, group by, and topN queries. For the filters, it supports a list of filters (AND) and negation (NOT) on a single expression. OR filters are not yet supported. To completely support all filters, the editor will need to let you build a tree.
8 |
9 | This plugin works with Grafana 1.9.x and has been tested against 1.9.1. Note that from 1.8.x to 1.9.x, the timestamp format for Grafana was changed from seconds to milliseconds.
10 |
11 | The code in Grafana master branch was recently reorganized so that all the files for each datasource live in a subdirectory under the [features](https://github.com/grafana/grafana/tree/master/src/app/features) directory. We've followed that convention for this plugin.
12 |
13 | ## Installation
14 |
15 | It must be installed in plugins/features/druid. You can do this by creating a symbolic link in the Grafana plugins directory so that the plugin/features directory maps to the features directory of this repo.
16 |
17 | Add this to your Grafana configuration file.
18 |
19 | ```
20 | datasources: {
21 | druid: {
22 | type: 'DruidDatasource',
23 | url: ''
24 | }
25 | },
26 |
27 | plugins: {
28 | panels: [],
29 | dependencies: ['features/druid/datasource'],
30 | }
31 | ```
32 |
33 | An example configuration and dashboard is [here](https://github.com/Quantiply/grafana-druid-wikipedia/).
--------------------------------------------------------------------------------
/features/druid/datasource.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014-2015 Quantiply Corporation. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | define([
17 | 'angular',
18 | 'lodash',
19 | 'kbn',
20 | 'moment',
21 | './queryCtrl',
22 | ],
23 | function (angular, _, kbn, moment) {
24 | 'use strict';
25 |
26 | var module = angular.module('grafana.services');
27 |
28 | module.factory('DruidDatasource', function($q, $http, templateSrv, $timeout, $log) {
29 |
30 | function replaceTemplateValues(obj, attrList) {
31 | var substitutedVals = attrList.map(function (attr) {
32 | return templateSrv.replace(obj[attr]);
33 | });
34 | return _.assign(_.clone(obj, true), _.zipObject(attrList, substitutedVals));
35 | }
36 |
37 | var GRANULARITIES = [
38 | ['minute', moment.duration(1, 'minute')],
39 | ['fifteen_minute', moment.duration(15, 'minute')],
40 | ['thirty_minute', moment.duration(30, 'minute')],
41 | ['hour', moment.duration(1, 'hour')],
42 | ['day', moment.duration(1, 'day')]
43 | ];
44 |
45 | var filterTemplateExpanders = {
46 | "selector": _.partialRight(replaceTemplateValues, ['value']),
47 | "regex": _.partialRight(replaceTemplateValues, ['pattern']),
48 | };
49 |
50 | function DruidDatasource(datasource) {
51 | this.type = 'druid';
52 | this.editorSrc = 'plugins/features/druid/partials/query.editor.html';
53 | this.url = datasource.url;
54 | this.name = datasource.name;
55 | this.supportMetrics = true;
56 | }
57 |
58 | //Get list of available datasources
59 | DruidDatasource.prototype.getDataSources = function() {
60 | return $http({method: 'GET', url: this.url + '/datasources'}).then(function (response) {
61 | return response.data;
62 | });
63 | }
64 |
65 | /* Returns a promise which returns
66 | {"dimensions":["page_url","ip_netspeed", ...],"metrics":["count", ...]}
67 | */
68 | DruidDatasource.prototype.getDimensionsAndMetrics = function (target, range) {
69 | var datasource = target.datasource;
70 | return $http({method: 'GET', url: this.url + '/datasources/' + datasource}).then(function (response) {
71 | return response.data;
72 | });
73 | }
74 |
75 | // Called once per panel (graph)
76 | DruidDatasource.prototype.query = function(options) {
77 | var dataSource = this;
78 | var from = dateToMoment(options.range.from);
79 | var to = dateToMoment(options.range.to);
80 |
81 | $log.debug(options);
82 |
83 | var promises = options.targets.map(function (target) {
84 | var maxDataPointsByResolution = options.maxDataPoints;
85 | var maxDataPointsByConfig = target.maxDataPoints? target.maxDataPoints : Number.MAX_VALUE;
86 | var maxDataPoints = Math.min(maxDataPointsByResolution, maxDataPointsByConfig);
87 | var granularity = target.shouldOverrideGranularity? target.customGranularity : computeGranularity(from, to, maxDataPoints);
88 | //Round up to start of an interval
89 | //Width of bar chars in Grafana is determined by size of the smallest interval
90 | var roundedFrom = roundUpStartTime(from, granularity);
91 | return dataSource._doQuery(roundedFrom, to, granularity, target);
92 | });
93 |
94 | return $q.all(promises).then(function(results) {
95 | return { data: _.flatten(results) };
96 | });
97 | };
98 |
99 | DruidDatasource.prototype._doQuery = function (from, to, granularity, target) {
100 | var datasource = target.datasource;
101 | var filters = target.filters;
102 | var aggregators = getAggregations(target.aggregators);
103 | var postAggregators = target.postAggregators;
104 | var groupBy = target.groupBy;
105 | var limitSpec = null;
106 | var metricNames = getMetricNames(aggregators, postAggregators);
107 | var intervals = getQueryIntervals(from, to);
108 | var promise = null;
109 |
110 | if (target.queryType === 'topN') {
111 | var threshold = target.limit;
112 | var metric = target.metric;
113 | var dimension = target.dimension;
114 | promise = this._topNQuery(datasource, intervals, granularity, filters, aggregators, postAggregators, threshold, metric, dimension)
115 | .then(function(response) {
116 | return convertTopNData(response.data, dimension, metric);
117 | });
118 | }
119 | else if (target.queryType === 'groupBy') {
120 | if (target.hasLimit) {
121 | limitSpec = getLimitSpec(target.limit, target.orderBy);
122 | }
123 | promise = this._groupByQuery(datasource, intervals, granularity, filters, aggregators, postAggregators, groupBy, limitSpec)
124 | .then(function(response) {
125 | return convertGroupByData(response.data, groupBy, metricNames);
126 | });
127 | }
128 | else {
129 | promise = this._timeSeriesQuery(datasource, intervals, granularity, filters, aggregators, postAggregators)
130 | .then(function(response) {
131 | return convertTimeSeriesData(response.data, metricNames);
132 | });
133 | }
134 | /*
135 | At this point the promise will return an list of time series of this form
136 | [
137 | {
138 | target: ,
139 | datapoints: [
140 | [, ],
141 | ...
142 | ]
143 | },
144 | ...
145 | ]
146 |
147 | Druid calculates metrics based on the intervals specified in the query but returns a timestamp rounded down.
148 | We need to adjust the first timestamp in each time series
149 | */
150 | return promise.then(function (metrics) {
151 | var fromMs = formatTimestamp(from);
152 | metrics.forEach(function (metric) {
153 | if (metric.datapoints[0][1] < fromMs) {
154 | metric.datapoints[0][1] = fromMs;
155 | }
156 | });
157 | return metrics;
158 | });
159 | };
160 |
161 | DruidDatasource.prototype._timeSeriesQuery = function (datasource, intervals, granularity, filters, aggregators, postAggregators) {
162 | var query = {
163 | "queryType": "timeseries",
164 | "dataSource": datasource,
165 | "granularity": granularity,
166 | "aggregations": aggregators,
167 | "postAggregations": postAggregators,
168 | "intervals": intervals
169 | };
170 |
171 | if (filters && filters.length > 0) {
172 | query.filter = buildFilterTree(filters);
173 | }
174 |
175 | return this._druidQuery(query);
176 | };
177 |
178 | DruidDatasource.prototype._topNQuery = function (datasource, intervals, granularity, filters, aggregators, postAggregators, threshold, metric, dimension) {
179 | var query = {
180 | "queryType": "topN",
181 | "dataSource": datasource,
182 | "granularity": granularity,
183 | "threshold": threshold,
184 | "dimension": dimension,
185 | "metric": metric,
186 | // "metric": {type: "inverted", metric: metric},
187 | "aggregations": aggregators,
188 | "postAggregations": postAggregators,
189 | "intervals": intervals
190 | };
191 |
192 | if (filters && filters.length > 0) {
193 | query.filter = buildFilterTree(filters);
194 | }
195 |
196 | return this._druidQuery(query);
197 | };
198 |
199 | DruidDatasource.prototype._groupByQuery = function (datasource, intervals, granularity, filters, aggregators, postAggregators, groupBy, limitSpec) {
200 | var query = {
201 | "queryType": "groupBy",
202 | "dataSource": datasource,
203 | "granularity": granularity,
204 | "dimensions": groupBy,
205 | "aggregations": aggregators,
206 | "postAggregations": postAggregators,
207 | "intervals": intervals,
208 | "limitSpec": limitSpec
209 | };
210 |
211 | if (filters && filters.length > 0) {
212 | query.filter = buildFilterTree(filters);
213 | }
214 |
215 | return this._druidQuery(query);
216 | };
217 |
218 | DruidDatasource.prototype._druidQuery = function (query) {
219 | var options = {
220 | method: 'POST',
221 | url: this.url,
222 | data: query
223 | };
224 | $log.debug(query);
225 | return $http(options);
226 | };
227 |
228 | function getAggregations(aggregations) {
229 | return _.map(aggregations, function (agg) {
230 | if (agg.type === 'doubleMax' || agg.type === 'longMax') {
231 | //This is called max in Duid 0.7, doubleMax/longMax in 0.8
232 | return _.assign({}, agg, {type: 'max'});
233 | }
234 | if (agg.type === 'doubleMin' || agg.type === 'longMin') {
235 | //This is called min in Duid 0.7, doubleMin/longMin in 0.8
236 | return _.assign({}, agg, {type: 'min'});
237 | }
238 | return agg;
239 | });
240 | }
241 |
242 | function getLimitSpec(limitNum, orderBy) {
243 | return {
244 | "type": "default",
245 | "limit": limitNum,
246 | "columns": orderBy.map(function (col) {
247 | return {"dimension": col, "direction": "DESCENDING"};
248 | })
249 | };
250 | }
251 |
252 | function buildFilterTree(filters) {
253 | //Do template variable replacement
254 | var replacedFilters = filters.map(function (filter) {
255 | return filterTemplateExpanders[filter.type](filter);
256 | })
257 | .map(function (filter) {
258 | var finalFilter = _.omit(filter, 'negate');
259 | if (filter.negate) {
260 | return { "type": "not", "field": finalFilter };
261 | }
262 | return finalFilter;
263 | });
264 | if (replacedFilters) {
265 | if (replacedFilters.length === 1) {
266 | return replacedFilters[0];
267 | }
268 | return {
269 | "type": "and",
270 | "fields": replacedFilters
271 | };
272 | }
273 | return null;
274 | }
275 |
276 | function getQueryIntervals(from, to) {
277 | return [from.toISOString() + '/' + to.toISOString()];
278 | }
279 |
280 | function getMetricNames(aggregators, postAggregators) {
281 | var displayAggs = _.filter(aggregators, function (agg) {
282 | return agg.type !== 'approxHistogramFold';
283 | });
284 | return _.union(_.pluck(displayAggs, 'name'), _.pluck(postAggregators, 'name'));
285 | }
286 |
287 | function formatTimestamp(ts) {
288 | return moment(ts).format('X')*1000
289 | }
290 |
291 | function convertTimeSeriesData(md, metrics) {
292 | return metrics.map(function (metric) {
293 | return {
294 | target: metric,
295 | datapoints: md.map(function (item) {
296 | return [
297 | item.result[metric],
298 | formatTimestamp(item.timestamp)
299 | ];
300 | })
301 | };
302 | });
303 | }
304 |
305 | function getGroupName(groupBy, metric) {
306 | return groupBy.map(function (dim) {
307 | return metric.event[dim];
308 | })
309 | .join("-");
310 | }
311 |
312 | function convertTopNData(md, dimension, metric) {
313 | /*
314 | Druid topN results look like this:
315 | [
316 | {
317 | "timestamp": "ts1",
318 | "result": [
319 | {"": d1, "": mv1},
320 | {"": d2, "": mv2}
321 | ]
322 | },
323 | {
324 | "timestamp": "ts2",
325 | "result": [
326 | {"": d1, "": mv3},
327 | {"": d2, "": mv4}
328 | ]
329 | },
330 | ...
331 | ]
332 | */
333 |
334 | /*
335 | First, we need make sure that the result for each
336 | timestamp contains entries for all distinct dimension values
337 | in the entire list of results.
338 |
339 | Otherwise, if we do a stacked bar chart, Grafana doesn't sum
340 | the metrics correctly.
341 | */
342 |
343 | //Get the list of all distinct dimension values for the entire result set
344 | var dVals = md.reduce(function (dValsSoFar, tsItem) {
345 | var dValsForTs = _.pluck(tsItem.result, dimension);
346 | return _.union(dValsSoFar, dValsForTs);
347 | }, {});
348 |
349 | //Add null for the metric for any missing dimension values per timestamp result
350 | md.forEach(function (tsItem) {
351 | var dValsPresent = _.pluck(tsItem.result, dimension);
352 | var dValsMissing = _.difference(dVals, dValsPresent);
353 | dValsMissing.forEach(function (dVal) {
354 | var nullPoint = {};
355 | nullPoint[dimension] = dVal;
356 | nullPoint[metric] = null;
357 | tsItem.result.push(nullPoint);
358 | });
359 | return tsItem;
360 | });
361 |
362 | //Re-index the results by dimension value instead of time interval
363 | var mergedData = md.map(function (item) {
364 | /*
365 | This first map() transforms this into a list of objects
366 | where the keys are dimension values
367 | and the values are [metricValue, unixTime] so that we get this:
368 | [
369 | {
370 | "d1": [mv1, ts1],
371 | "d2": [mv2, ts1]
372 | },
373 | {
374 | "d1": [mv3, ts2],
375 | "d2": [mv4, ts2]
376 | },
377 | ...
378 | ]
379 | */
380 | var timestamp = formatTimestamp(item.timestamp);
381 | var keys = _.pluck(item.result, dimension);
382 | var vals = _.pluck(item.result, metric).map(function (val) { return [val, timestamp]});
383 | return _.zipObject(keys, vals);
384 | })
385 | .reduce(function (prev, curr) {
386 | /*
387 | Reduce() collapses all of the mapped objects into a single
388 | object. The keys are dimension values
389 | and the values are arrays of all the values for the same key.
390 | The _.assign() function merges objects together and it's callback
391 | gets invoked for every key,value pair in the source (2nd argument).
392 | Since our initial value for reduce() is an empty object,
393 | the _.assign() callback will get called for every new val
394 | that we add to the final object.
395 | */
396 | return _.assign(prev, curr, function (pVal, cVal) {
397 | if (pVal) {
398 | pVal.push(cVal);
399 | return pVal;
400 | }
401 | return [cVal];
402 | });
403 | }, {});
404 |
405 | //Convert object keyed by dimension values into an array
406 | //of objects {target: , datapoints: }
407 | return _.map(mergedData, function (vals, key) {
408 | return {
409 | target: key,
410 | datapoints: vals
411 | };
412 | });
413 | }
414 |
415 | function convertGroupByData(md, groupBy, metrics) {
416 | var mergedData = md.map(function (item) {
417 | /*
418 | The first map() transforms the list Druid events into a list of objects
419 | with keys of the form ":" and values
420 | of the form [metricValue, unixTime]
421 | */
422 | var groupName = getGroupName(groupBy, item);
423 | var keys = metrics.map(function (metric) {
424 | return groupName + ":" + metric;
425 | });
426 | var vals = metrics.map(function (metric) {
427 | return [
428 | item.event[metric],
429 | formatTimestamp(item.timestamp)
430 | ];
431 | });
432 | return _.zipObject(keys, vals);
433 | })
434 | .reduce(function (prev, curr) {
435 | /*
436 | Reduce() collapses all of the mapped objects into a single
437 | object. The keys are still of the form ":"
438 | and the values are arrays of all the values for the same key.
439 | The _.assign() function merges objects together and it's callback
440 | gets invoked for every key,value pair in the source (2nd argument).
441 | Since our initial value for reduce() is an empty object,
442 | the _.assign() callback will get called for every new val
443 | that we add to the final object.
444 | */
445 | return _.assign(prev, curr, function (pVal, cVal) {
446 | if (pVal) {
447 | pVal.push(cVal);
448 | return pVal;
449 | }
450 | return [cVal];
451 | });
452 | }, {});
453 |
454 | return _.map(mergedData, function (vals, key) {
455 | /*
456 | Second map converts the aggregated object into an array
457 | */
458 | return {
459 | target: key,
460 | datapoints: vals
461 | };
462 | });
463 | }
464 |
465 | function dateToMoment(date) {
466 | if (date === 'now') {
467 | return moment();
468 | }
469 | return moment(kbn.parseDate(date));
470 | }
471 |
472 | function computeGranularity(from, to, maxDataPoints) {
473 | var intervalSecs = to.unix() - from.unix();
474 | /*
475 | Find the smallest granularity for which there
476 | will be fewer than maxDataPoints
477 | */
478 | var granularityEntry = _.find(GRANULARITIES, function(gEntry) {
479 | return Math.ceil(intervalSecs/gEntry[1].asSeconds()) <= maxDataPoints;
480 | });
481 |
482 | $log.debug("Calculated \"" + granularityEntry[0] + "\" granularity [" + Math.ceil(intervalSecs/granularityEntry[1].asSeconds()) + " pts]" + " for " + (intervalSecs/60).toFixed(0) + " minutes and max of " + maxDataPoints + " data points");
483 | return granularityEntry[0];
484 | }
485 |
486 | function roundUpStartTime(from, granularity) {
487 | var duration = _.find(GRANULARITIES, function (gEntry) {
488 | return gEntry[0] === granularity;
489 | })[1];
490 | var rounded = moment(Math.ceil((+from)/(+duration)) * (+duration));
491 | $log.debug("Rounding up start time from " + from.format() + " to " + rounded.format() + " for granularity [" + granularity + "]");
492 | return rounded;
493 | }
494 |
495 | return DruidDatasource;
496 | });
497 |
498 | });
499 |
--------------------------------------------------------------------------------
/features/druid/partials/query.editor.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
23 |
24 |
25 |
47 |
48 |
57 |
58 |
236 |
237 |
238 |
239 |
330 |
331 |
569 |
570 |
663 |
664 |
665 |
666 |
667 |
--------------------------------------------------------------------------------
/features/druid/queryCtrl.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014-2015 Quantiply Corporation. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | define([
17 | 'angular',
18 | 'lodash'
19 | ],
20 | function (angular, _) {
21 | 'use strict';
22 |
23 | var module = angular.module('grafana.controllers');
24 |
25 | module.controller('DruidTargetCtrl', function($scope, $q, $timeout, $log) {
26 |
27 | var
28 | validateMaxDataPoints = function (target, errs) {
29 | if (target.maxDataPoints) {
30 | var intMax = parseInt(target.maxDataPoints);
31 | if (isNaN(intMax) || intMax <= 0) {
32 | errs.maxDataPoints = "Must be a positive integer";
33 | return false;
34 | }
35 | target.maxDataPoints = intMax;
36 | }
37 | return true;
38 | },
39 | validateLimit = function (target, errs) {
40 | if (!target.limit) {
41 | errs.limit = "Must specify a limit";
42 | return false;
43 | }
44 | var intLimit = parseInt(target.limit);
45 | if (isNaN(intLimit)) {
46 | errs.limit = "Limit must be a integer";
47 | return false;
48 | }
49 | target.limit = intLimit;
50 | if (target.orderBy && !Array.isArray(target.orderBy)) {
51 | target.orderBy = target.orderBy.split(",");
52 | }
53 | if (!target.orderBy) {
54 | errs.orderBy = "Must list columns to order by.";
55 | return false;
56 | }
57 | return true;
58 | },
59 | validateGroupByQuery = function(target, errs) {
60 | if (target.groupBy && !Array.isArray(target.groupBy)) {
61 | target.groupBy = target.groupBy.split(",");
62 | }
63 | if (!target.groupBy) {
64 | errs.groupBy = "Must list dimensions to group by.";
65 | return false;
66 | }
67 | if (target.hasLimit) {
68 | if (!validateLimit(target, errs)) {
69 | return false;
70 | }
71 | }
72 | return true;
73 | },
74 | validateTopNQuery = function(target, errs) {
75 | if (!validateLimit(target, errs)) {
76 | return false;
77 | }
78 | if (!target.metric) {
79 | errs.metric = "Must specify a metric";
80 | return false;
81 | }
82 | if (!target.dimension) {
83 | errs.dimension = "Must specify a dimension";
84 | return false;
85 | }
86 | return true;
87 | },
88 | validateSelectorFilter = function(target) {
89 | if (!target.currentFilter.dimension) {
90 | return "Must provide dimension name for selector filter.";
91 | }
92 | if (!target.currentFilter.value) {
93 | //Empty string is how you match null or empty in Druid
94 | target.currentFilter.value = "";
95 | }
96 | return null;
97 | },
98 | validateRegexFilter = function(target) {
99 | if (!target.currentFilter.dimension) {
100 | return "Must provide dimension name for regex filter.";
101 | }
102 | if (!target.currentFilter.pattern) {
103 | return "Must provide pattern for regex filter.";
104 | }
105 | return null;
106 | },
107 | validateCountAggregator = function(target) {
108 | if (!target.currentAggregator.name) {
109 | return "Must provide an output name for count aggregator.";
110 | }
111 | return null;
112 | },
113 | validateSimpleAggregator = function(type, target) {
114 | if (!target.currentAggregator.name) {
115 | return "Must provide an output name for " + type + " aggregator.";
116 | }
117 | if (!target.currentAggregator.fieldName) {
118 | return "Must provide a metric name for " + type + " aggregator.";
119 | }
120 | //TODO - check that fieldName is a valid metric (exists and of correct type)
121 | return null;
122 | },
123 | validateApproxHistogramFoldAggregator = function(target) {
124 | var err = validateSimpleAggregator('approxHistogramFold', target);
125 | if (err) { return err; }
126 | //TODO - check that resolution and numBuckets are ints (if given)
127 | //TODO - check that lowerLimit and upperLimit are flots (if given)
128 | return null;
129 | },
130 | validateSimplePostAggregator = function(type, target) {
131 | if (!target.currentPostAggregator.name) {
132 | return "Must provide an output name for " + type + " post aggregator.";
133 | }
134 | if (!target.currentPostAggregator.fieldName) {
135 | return "Must provide an aggregator name for " + type + " post aggregator.";
136 | }
137 | //TODO - check that fieldName is a valid aggregation (exists and of correct type)
138 | return null;
139 | },
140 | validateQuantilePostAggregator = function (target) {
141 | var err = validateSimplePostAggregator('quantile', target);
142 | if (err) { return err; }
143 | if (!target.currentPostAggregator.probability) {
144 | return "Must provide a probability for the quantile post aggregator.";
145 | }
146 | return null;
147 | },
148 | validateArithmeticPostAggregator = function(target) {
149 | if (!target.currentPostAggregator.name) {
150 | return "Must provide an output name for arithmetic post aggregator.";
151 | }
152 | if (!target.currentPostAggregator.fn) {
153 | return "Must provide a function for arithmetic post aggregator.";
154 | }
155 | if (!isValidArithmeticPostAggregatorFn(target.currentPostAggregator.fn)) {
156 | return "Invalid arithmetic function";
157 | }
158 | if (!target.currentPostAggregator.fields) {
159 | return "Must provide a list of fields for arithmetic post aggregator.";
160 | }
161 | else {
162 | if (!Array.isArray(target.currentPostAggregator.fields)) {
163 | target.currentPostAggregator.fields = target.currentPostAggregator.fields
164 | .split(",")
165 | .map(function (f) { return f.trim(); })
166 | .map(function (f) { return {type: "fieldAccess", fieldName: f}; });
167 | }
168 | if (target.currentPostAggregator.fields.length < 2) {
169 | return "Must provide at least two fields for arithmetic post aggregator.";
170 | }
171 | }
172 | return null;
173 | },
174 | queryTypeValidators = {
175 | "timeseries": _.noop,
176 | "groupBy": validateGroupByQuery,
177 | "topN": validateTopNQuery
178 | },
179 | filterValidators = {
180 | "selector": validateSelectorFilter,
181 | "regex": validateRegexFilter
182 | },
183 | aggregatorValidators = {
184 | "count": validateCountAggregator,
185 | "longSum": _.partial(validateSimpleAggregator, 'longSum'),
186 | "doubleSum": _.partial(validateSimpleAggregator, 'doubleSum'),
187 | "doubleMax": _.partial(validateSimpleAggregator, 'doubleMax'),
188 | "doubleMin": _.partial(validateSimpleAggregator, 'doubleMin'),
189 | "longMax": _.partial(validateSimpleAggregator, 'longMax'),
190 | "longMin": _.partial(validateSimpleAggregator, 'longMin'),
191 | "approxHistogramFold": validateApproxHistogramFoldAggregator,
192 | "hyperUnique": _.partial(validateSimpleAggregator, 'hyperUnique')
193 | },
194 | postAggregatorValidators = {
195 | "arithmetic": validateArithmeticPostAggregator,
196 | "quantile": validateQuantilePostAggregator
197 | },
198 | arithmeticPostAggregatorFns = {'+': null, '-': null, '*': null, '/': null},
199 | defaultQueryType = "timeseries",
200 | defaultFilterType = "selector",
201 | defaultAggregatorType = "count",
202 | defaultPostAggregator = {type: 'arithmetic', 'fn': '+'},
203 | customGranularities = ['minute', 'fifteen_minute', 'thirty_minute', 'hour', 'day', 'all'],
204 | defaultCustomGranularity = 'minute',
205 | defaultLimit = 5;
206 |
207 | $scope.init = function() {
208 | $scope.target.errors = validateTarget($scope.target);
209 | $scope.queryTypes = _.keys(queryTypeValidators);
210 | $scope.filterTypes = _.keys(filterValidators);
211 | $scope.aggregatorTypes = _.keys(aggregatorValidators);
212 | $scope.postAggregatorTypes = _.keys(postAggregatorValidators);
213 | $scope.arithmeticPostAggregatorFns = _.keys(arithmeticPostAggregatorFns);
214 | $scope.customGranularities = customGranularities;
215 |
216 | if (!$scope.target.queryType) {
217 | $scope.target.queryType = defaultQueryType;
218 | }
219 |
220 | if (!$scope.target.currentFilter) {
221 | clearCurrentFilter();
222 | }
223 |
224 | if (!$scope.target.currentAggregator) {
225 | clearCurrentAggregator();
226 | }
227 |
228 | if (!$scope.target.currentPostAggregator) {
229 | clearCurrentPostAggregator();
230 | }
231 |
232 | if (!$scope.target.customGranularity) {
233 | $scope.target.customGranularity = defaultCustomGranularity;
234 | }
235 |
236 | if (!$scope.target.limit) {
237 | $scope.target.limit = defaultLimit;
238 | }
239 |
240 | $scope.$on('typeahead-updated', function() {
241 | $timeout($scope.targetBlur);
242 | });
243 | };
244 |
245 | /*
246 | rhoover: copied this function from OpenTSDB.
247 | I don't know what the comment below refers to
248 | */
249 | $scope.targetBlur = function() {
250 | $scope.target.errors = validateTarget($scope.target);
251 |
252 | // this does not work so good
253 | if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
254 | $scope.oldTarget = angular.copy($scope.target);
255 | $scope.get_data();
256 | }
257 | };
258 |
259 | $scope.duplicate = function() {
260 | var clone = angular.copy($scope.target);
261 | $scope.panel.targets.push(clone);
262 | };
263 |
264 | $scope.listDataSources = function(query, callback) {
265 | $log.debug("Datasource type-ahead query");
266 | var ioFn = _.bind($scope.datasource.getDataSources, $scope.datasource);
267 | return cachedAndCoalesced(ioFn, $scope, 'dataSourceList').then(function(sources) {
268 | callback(sources);
269 | });
270 | };
271 |
272 | $scope.getDimensions = function(query, callback) {
273 | $log.debug("Dimension type-ahead query");
274 | return $scope.getDimensionsAndMetrics(query).then(function (dimsAndMetrics) {
275 | callback(dimsAndMetrics.dimensions);
276 | });
277 | };
278 |
279 | $scope.getMetrics = function(query, callback) {
280 | $log.debug("Metric type-ahead query");
281 | return $scope.getDimensionsAndMetrics(query).then(function (dimsAndMetrics) {
282 | callback(dimsAndMetrics.metrics);
283 | });
284 | };
285 |
286 | $scope.getDimensionsAndMetrics = function(query) {
287 | var ioFn = _.bind($scope.datasource.getDimensionsAndMetrics, $scope.datasource, $scope.target, $scope.range);
288 | return cachedAndCoalesced(ioFn, $scope, 'dimensionsAndMetrics');
289 | };
290 |
291 | $scope.addFilter = function() {
292 | if (!$scope.addFilterMode) {
293 | //Enabling this mode will display the filter inputs
294 | $scope.addFilterMode = true;
295 | return;
296 | }
297 |
298 | if (!$scope.target.filters) {
299 | $scope.target.filters = [];
300 | }
301 |
302 | $scope.target.errors = validateTarget($scope.target);
303 | if (!$scope.target.errors.currentFilter) {
304 | //Add new filter to the list
305 | $scope.target.filters.push($scope.target.currentFilter);
306 | clearCurrentFilter();
307 | $scope.addFilterMode = false;
308 | }
309 |
310 | $scope.targetBlur();
311 | };
312 |
313 | $scope.removeFilter = function(index) {
314 | $scope.target.filters.splice(index, 1);
315 | $scope.targetBlur();
316 | };
317 |
318 | $scope.clearCurrentFilter = function() {
319 | clearCurrentFilter();
320 | $scope.addFilterMode = false;
321 | $scope.targetBlur();
322 | };
323 |
324 | $scope.addAggregator = function() {
325 | if (!$scope.addAggregatorMode) {
326 | $scope.addAggregatorMode = true;
327 | return;
328 | }
329 |
330 | if (!$scope.target.aggregators) {
331 | $scope.target.aggregators = [];
332 | }
333 |
334 | $scope.target.errors = validateTarget($scope.target);
335 | if (!$scope.target.errors.currentAggregator) {
336 | //Add new aggregator to the list
337 | $scope.target.aggregators.push($scope.target.currentAggregator);
338 | clearCurrentAggregator();
339 | $scope.addAggregatorMode = false;
340 | }
341 |
342 | $scope.targetBlur();
343 | };
344 |
345 | $scope.removeAggregator = function(index) {
346 | $scope.target.aggregators.splice(index, 1);
347 | $scope.targetBlur();
348 | };
349 |
350 | $scope.clearCurrentAggregator = function() {
351 | clearCurrentAggregator();
352 | $scope.addAggregatorMode = false;
353 | $scope.targetBlur();
354 | };
355 |
356 | $scope.addPostAggregator = function() {
357 | if (!$scope.addPostAggregatorMode) {
358 | $scope.addPostAggregatorMode = true;
359 | return;
360 | }
361 |
362 | if (!$scope.target.postAggregators) {
363 | $scope.target.postAggregators = [];
364 | }
365 |
366 | $scope.target.errors = validateTarget($scope.target);
367 | if (!$scope.target.errors.currentPostAggregator) {
368 | //Add new post aggregator to the list
369 | $scope.target.postAggregators.push($scope.target.currentPostAggregator);
370 | clearCurrentPostAggregator();
371 | $scope.addPostAggregatorMode = false;
372 | }
373 |
374 | $scope.targetBlur();
375 | };
376 |
377 | $scope.removePostAggregator = function(index) {
378 | $scope.target.postAggregators.splice(index, 1);
379 | $scope.targetBlur();
380 | };
381 |
382 | $scope.clearCurrentPostAggregator = function() {
383 | clearCurrentPostAggregator();
384 | $scope.addPostAggregatorMode = false;
385 | $scope.targetBlur();
386 | };
387 |
388 | function cachedAndCoalesced(ioFn, $scope, cacheName) {
389 | var promiseName = cacheName + "Promise";
390 | if (!$scope[cacheName]) {
391 | $log.debug(cacheName + ": no cached value to use");
392 | if (!$scope[promiseName]) {
393 | $log.debug(cacheName + ": making async call");
394 | $scope[promiseName] = ioFn()
395 | .then(function(result) {
396 | $scope[promiseName] = null;
397 | $scope[cacheName] = result;
398 | return $scope[cacheName];
399 | });
400 | }
401 | else {
402 | $log.debug(cacheName + ": async call already in progress...returning same promise");
403 | }
404 | return $scope[promiseName];
405 | }
406 | else {
407 | $log.debug(cacheName + ": using cached value");
408 | var deferred = $q.defer();
409 | deferred.resolve($scope[cacheName]);
410 | return deferred.promise;
411 | }
412 | };
413 |
414 | function isValidFilterType(type) {
415 | return _.has(filterValidators, type);
416 | }
417 |
418 | function isValidAggregatorType(type) {
419 | return _.has(aggregatorValidators, type);
420 | }
421 |
422 | function isValidPostAggregatorType(type) {
423 | return _.has(postAggregatorValidators, type);
424 | }
425 |
426 | function isValidQueryType(type) {
427 | return _.has(queryTypeValidators, type);
428 | }
429 |
430 | function isValidArithmeticPostAggregatorFn(fn) {
431 | return _.has(arithmeticPostAggregatorFns, fn);
432 | }
433 |
434 | function clearCurrentFilter() {
435 | $scope.target.currentFilter = {type: defaultFilterType};
436 | }
437 |
438 | function clearCurrentAggregator() {
439 | $scope.target.currentAggregator = {type: defaultAggregatorType};
440 | }
441 |
442 | function clearCurrentPostAggregator() {
443 | $scope.target.currentPostAggregator = _.clone(defaultPostAggregator);
444 | }
445 |
446 | function validateTarget(target) {
447 | var validatorOut, errs = {};
448 |
449 | if (!target.datasource) {
450 | errs.datasource = "You must supply a datasource name.";
451 | }
452 |
453 | if (!target.queryType) {
454 | errs.queryType = "You must supply a query type.";
455 | }
456 | else if (!isValidQueryType(target.queryType)) {
457 | errs.queryType = "Unknown query type: " + target.queryType + ".";
458 | }
459 | else {
460 | queryTypeValidators[target.queryType](target, errs);
461 | }
462 |
463 | if (target.shouldOverrideGranularity) {
464 | if (target.customGranularity) {
465 | if (!_.contains(customGranularities, target.customGranularity)) {
466 | errs.customGranularity = "Invalid granularity.";
467 | }
468 | }
469 | else {
470 | errs.customGranularity = "You must choose a granularity.";
471 | }
472 | }
473 | else {
474 | validateMaxDataPoints(target, errs);
475 | }
476 |
477 | if ($scope.addFilterMode) {
478 | if (!isValidFilterType(target.currentFilter.type)) {
479 | errs.currentFilter = "Invalid filter type: " + target.currentFilter.type + ".";
480 | }
481 | else {
482 | validatorOut = filterValidators[target.currentFilter.type](target);
483 | if (validatorOut) {
484 | errs.currentFilter = validatorOut;
485 | }
486 | }
487 | }
488 |
489 | if ($scope.addAggregatorMode) {
490 | if (!isValidAggregatorType(target.currentAggregator.type)) {
491 | errs.currentAggregator = "Invalid aggregator type: " + target.currentAggregator.type + ".";
492 | }
493 | else {
494 | validatorOut = aggregatorValidators[target.currentAggregator.type](target);
495 | if (validatorOut) {
496 | errs.currentAggregator = validatorOut;
497 | }
498 | }
499 | }
500 |
501 | if (!$scope.target.aggregators) {
502 | errs.aggregators = "You must supply at least one aggregator";
503 | }
504 |
505 | if ($scope.addPostAggregatorMode) {
506 | if (!isValidPostAggregatorType(target.currentPostAggregator.type)) {
507 | errs.currentPostAggregator = "Invalid post aggregator type: " + target.currentPostAggregator.type + ".";
508 | }
509 | else {
510 | validatorOut = postAggregatorValidators[target.currentPostAggregator.type](target);
511 | if (validatorOut) {
512 | errs.currentPostAggregator = validatorOut;
513 | }
514 | }
515 | }
516 |
517 | return errs;
518 | }
519 |
520 | });
521 |
522 | });
523 |
--------------------------------------------------------------------------------