From eb7b0c6c89bddf4c45b8fff7b4d39cbb5c98328c Mon Sep 17 00:00:00 2001 From: Ryan Bak Date: Mon, 18 Jul 2016 14:18:58 -0600 Subject: [PATCH] Move monasca datasource to new repo The previous repo was https://github.com/twc-openstack/grafana-plugins Change-Id: I07ce789fb7455a8969fa9c6e8bcc78a09de1c0b3 --- LICENSE | 201 ++++++++++++++ README.md | 5 + datasource.js | 529 ++++++++++++++++++++++++++++++++++++ directives.js | 17 ++ img/openstack_logo.png | Bin 0 -> 8199 bytes module.js | 20 ++ partials/config.html | 55 ++++ partials/query.editor.html | 99 +++++++ partials/query.options.html | 52 ++++ plugin.json | 37 +++ query_ctrl.js | 171 ++++++++++++ 11 files changed, 1186 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 datasource.js create mode 100644 directives.js create mode 100644 img/openstack_logo.png create mode 100644 module.js create mode 100644 partials/config.html create mode 100644 partials/query.editor.html create mode 100644 partials/query.options.html create mode 100644 plugin.json create mode 100644 query_ctrl.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..278d527 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {2016} {Raintank Inc.} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..441eab5 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +## Monasca Datasource - A datasource for use with the OpenStack Monasca api. + +For more information on Monasca see the [Monasca documentation](https://wiki.openstack.org/wiki/Monasca) + +When combined with Grafana Keystone authentication this datasource supports using login credentials to authenticate queries. \ No newline at end of file diff --git a/datasource.js b/datasource.js new file mode 100644 index 0000000..33f1940 --- /dev/null +++ b/datasource.js @@ -0,0 +1,529 @@ +define([ + 'angular', + 'lodash', + 'moment', + 'app/plugins/sdk', + 'app/core/utils/datemath', + 'app/core/utils/kbn', + './query_ctrl', +], +function (angular, _, moment, sdk, dateMath, kbn) { + 'use strict'; + + var self; + + function MonascaDatasource(instanceSettings, $q, backendSrv, templateSrv) { + this.url = instanceSettings.url; + this.name = instanceSettings.name; + + if (instanceSettings.jsonData) { + this.token = instanceSettings.jsonData.token; + this.keystoneAuth = instanceSettings.jsonData.keystoneAuth; + } else { + this.token = null; + this.keystoneAuth = null; + } + + this.q = $q; + this.backendSrv = backendSrv; + this.templateSrv = templateSrv; + + self = this; + } + + MonascaDatasource.prototype.query = function(options) { + var datasource = this; + var from = this.translateTime(options.range.from); + var to = this.translateTime(options.range.to); + + var targets_list = []; + for (var i = 0; i < options.targets.length; i++) { + var target = options.targets[i]; + // Missing target.period indicates a new unfilled query + if (target.error || target.hide || !target.period) { + continue; + } + var query = this.buildDataQuery(options.targets[i], from, to); + query = self.templateSrv.replace(query, options.scopedVars); + var query_list + if (options.group){ + query_list = this.expandTemplatedQueries(query); + } + else { + query_list = this.expandQueries(query); + } + targets_list.push(query_list); + } + + var targets_promise = self.q.all(targets_list).then(function(results) { + return _.flatten(results); + }); + + var promises = self.q.resolve(targets_promise).then(function(targets) { + return targets.map(function (target) { + target = datasource.convertPeriod(target); + return datasource._limitedMonascaRequest(target, {}).then(datasource.convertDataPoints).catch(function(err) {throw err}); + }); + }).catch(function(err) {throw err}); + + return self.q.resolve(promises).then(function(promises) { + return self.q.all(promises).then(function(results) { + var sorted_results = results.map(function (results) { + return results.sort(function (a, b) { + return a.target.localeCompare(b.target); + }); + }); + return { data: _.flatten(sorted_results).filter(function(result) { return !_.isEmpty(result)}) }; + }); + }); + }; + + MonascaDatasource.prototype.namesQuery = function() { + return this._limitedMonascaRequest('/v2.0/metrics/names', {}).catch(function(err) {throw err}); + }; + + MonascaDatasource.prototype.convertNamesList = function(data) { + var metrics = []; + data = data.data.elements; + for (var i = 0; i < data.length; i++) { + metrics.push(data[i].name); + } + return metrics; + }; + + MonascaDatasource.prototype.metricsQuery = function(params) { + return this._limitedMonascaRequest('/v2.0/metrics', params).catch(function(err) {throw err}); + }; + + MonascaDatasource.prototype.buildDimensionList = function(data) { + var keys = []; + var values = {}; + data = data.data.elements; + for (var i = 0; i < data.length; i++) { + var dim_set = data[i].dimensions; + for (var key in dim_set) { + if (keys.indexOf(key) == -1) { + keys.push(key); + values[key] = []; + } + var value = dim_set[key]; + if (values[key].indexOf(value) == -1) { + values[key].push(value); + } + } + } + return {'keys' : keys, 'values' : values}; + }; + + MonascaDatasource.prototype.buildMetricList = function(data) { + data = data.data.elements; + return data; + }; + + MonascaDatasource.prototype.buildDataQuery = function(options, from, to) { + var params = {}; + params.name = options.metric; + if (options.group) { + params.group_by = '*'; + } + else { + params.merge_metrics = 'true'; + } + params.start_time = from; + if (to) { + params.end_time = to; + } + if (options.dimensions) { + var dimensions = ''; + for (var i = 0; i < options.dimensions.length; i++) { + var key = options.dimensions[i].key; + var value = options.dimensions[i].value; + if (options.group && value == '$all') { + continue; + } + if (dimensions) { + dimensions += ','; + } + dimensions += key; + dimensions += ':'; + dimensions += value; + } + params.dimensions = dimensions; + } + if (options.alias) { + params.alias = options.alias; + } + var path; + if (options.aggregator != 'none') { + params.statistics = options.aggregator; + params.period = options.period; + path = '/v2.0/metrics/statistics'; + } + else { + path = '/v2.0/metrics/measurements'; + } + var first = true; + Object.keys(params).forEach(function (key) { + if (first) { + path += '?'; + first = false; + } + else { + path += '&'; + } + path += key; + path += '='; + path += params[key]; + }); + return path; + }; + + MonascaDatasource.prototype.expandQueries = function(query) { + var datasource = this; + return this.expandAllQueries(query).then(function(partial_query_list) { + var query_list = []; + for (var i = 0; i < partial_query_list.length; i++) { + query_list = query_list.concat(datasource.expandTemplatedQueries(partial_query_list[i])); + } + query_list = datasource.autoAlias(query_list); + return query_list; + }); + }; + + MonascaDatasource.prototype.expandTemplatedQueries = function(query) { + var templated_vars = query.match(/{[^}]*}/g); + if (!templated_vars) { + return [query]; + } + + var expandedQueries = []; + var to_replace = templated_vars[0]; + var var_options = to_replace.substring(1, to_replace.length - 1); + var_options = var_options.split(','); + for (var i = 0; i < var_options.length; i++) { + var new_query = query.replace(new RegExp(to_replace, 'g'), var_options[i]); + expandedQueries = expandedQueries.concat(this.expandTemplatedQueries(new_query)); + } + return expandedQueries; + }; + + MonascaDatasource.prototype.expandAllQueries = function(query) { + if (query.indexOf("$all") > -1) { + var metric_name = query.match(/name=([^&]*)/)[1]; + var start_time = query.match(/start_time=([^&]*)/)[1]; + + // Find all matching subqueries + var dimregex = /(?:dimensions=|,)([^,]*):\$all/g; + var matches, neededDimensions = []; + while (matches = dimregex.exec(query)) { + neededDimensions.push(matches[1]); + } + + var metricQueryParams = {'name' : metric_name, 'start_time': start_time}; + var queriesPromise = this.metricsQuery(metricQueryParams).then(function(data) { + var expandedQueries = []; + var metrics = data.data.elements; + var matchingMetrics = {}; // object ensures uniqueness of dimension sets + for (var i = 0; i < metrics.length; i++) { + var dimensions = metrics[i].dimensions; + var set = {}; + var skip = false; + for (var j = 0; j < neededDimensions.length; j++) { + var key = neededDimensions[j]; + if (!(key in dimensions)) { + skip = true; + break; + } + set[key] = dimensions[key]; + } + if (!skip) { + matchingMetrics[JSON.stringify(set)] = set; + } + } + Object.keys(matchingMetrics).forEach(function (set) { + var new_query = query; + var match = matchingMetrics[set]; + Object.keys(match).forEach(function (key) { + var to_replace = key+":\\$all"; + var replacement = key+":"+match[key]; + new_query = new_query.replace(new RegExp(to_replace, 'g'), replacement); + }); + expandedQueries.push(new_query); + }); + return expandedQueries; + }); + + return queriesPromise; + } + else { + return self.q.resolve([query]); + } + }; + + // Alias based on dimensions in query + // Used when querying with merge flag, where no dimension info is returned. + MonascaDatasource.prototype.autoAlias = function(query_list) { + function keysSortedByLengthDesc(obj) { + var keys = []; + for (var key in obj) { + keys.push(key) + } + function byLength(a, b) {return b.length - a.length} + return keys.sort(byLength) + }; + + for (var i = 0; i < query_list.length; i++) { + var query = query_list[i]; + var alias = query.match(/alias=[^&@]*@([^&]*)/); + var dimensions = query.match(/dimensions=([^&]*)/); + if (alias && dimensions[1]) { + var dimensions_list = dimensions[1].split(','); + var dimensions_dict = {}; + for (var j = 0; j < dimensions_list.length; j++) { + var dim = dimensions_list[j].split(':'); + dimensions_dict[dim[0]] = dim[1]; + } + var keys = keysSortedByLengthDesc(dimensions_dict); + for (var k in keys) { + query = query.replace(new RegExp("@"+keys[k], 'g'), dimensions_dict[keys[k]]); + } + query_list[i] = query; + } + } + return query_list; + }; + + MonascaDatasource.prototype.convertDataPoints = function(data) { + function keysSortedByLengthDesc(obj) { + var keys = []; + for (var key in obj) { + keys.push(key) + } + function byLength(a, b) {return b.length - a.length} + return keys.sort(byLength) + }; + + var url = data.config.url; + var results = []; + + for (var i = 0; i < data.data.elements.length; i++) + { + var element = data.data.elements[i]; + + var target = element.name; + var alias = data.config.url.match(/alias=([^&]*)/); + // Alias based on returned dimensions + // Used when querying with group_by flag where dimensions are not specified in initial query + if (alias) { + alias = alias[1]; + var keys = keysSortedByLengthDesc(element.dimensions); + for (var k in keys) + { + alias = alias.replace(new RegExp("@"+keys[k], 'g'), element.dimensions[keys[k]]) + } + target = alias + } + + var raw_datapoints; + var aggregator; + if ('measurements' in element) { + raw_datapoints = element.measurements; + aggregator = 'value'; + } + else { + raw_datapoints = element.statistics; + aggregator = url.match(/statistics=[^&]*/); + aggregator = aggregator[0].substring('statistics='.length); + } + var datapoints = []; + var timeCol = element.columns.indexOf('timestamp'); + var dataCol = element.columns.indexOf(aggregator); + for (var j = 0; j < raw_datapoints.length; j++) { + var datapoint = raw_datapoints[j]; + var time = new Date(datapoint[timeCol]); + var point = datapoint[dataCol]; + datapoints.push([point, time.getTime()]); + } + var convertedData = { 'target': target, 'datapoints': datapoints }; + results.push(convertedData) + } + return results; + }; + + // For use with specified or api enforced limits. + // Pages through data until all data is retrieved. + MonascaDatasource.prototype._limitedMonascaRequest = function(path, params) { + var datasource = this; + var deferred = self.q.defer(); + var data = null; + var element_list = []; + + function aggregateResults() { + var elements = {}; + for (var i = 0; i < element_list.length; i++) { + var element = element_list[i]; + if (element.id in elements){ + if (element.measurements){ + elements[element.id].measurements = elements[element.id].measurements.concat(element.measurements); + } + if (element.statistics){ + elements[element.id].measurements = elements[element.id].statistics.concat(element.statistics); + } + } + else{ + elements[element.id] = element; + } + } + data.data.elements = Object.keys(elements).map(function(key) { + return elements[key]; + }); + } + + // Handle incosistent element.id from merging here. Remove when this bug is fixed. + function flattenResults() { + var elements = []; + for (var i = 0; i < element_list.length; i++) { + var element = element_list[i]; + if (element.measurements){ + elements.push(element.measurements); + } + if (element.statistics){ + elements.push(element.statistics); + } + } + if (data.data.elements[0].measurements){ + data.data.elements[0].measurements = _.flatten(elements, true) + } + if (data.data.elements[0].statistics){ + data.data.elements[0].statistics = _.flatten(elements, true) + } + } + + function requestAll(multi_page){ + datasource._monascaRequest(path, params) + .then(function(d) { + data = d + element_list = element_list.concat(d.data.elements); + if(d.data.links) { + for (var i = 0; i < d.data.links.length; i++) { + if (d.data.links[i].rel == 'next'){ + var next = decodeURIComponent(d.data.links[i].href) + var offset = next.match(/offset=([^&]*)/); + params.offset = offset[1]; + requestAll(true); + return; + } + } + } + // Handle incosistent element.id from merging here. Remove when this bug is fixed. + var query = d.data.links[0].href + if (multi_page){ + if (query.indexOf('merge_metrics') > -1) { + flattenResults(); + } + else { + aggregateResults(); + } + } + deferred.resolve(data); + }).catch(function(err) {deferred.reject(err)}); + } + + requestAll(false); + return deferred.promise; + }; + + MonascaDatasource.prototype._monascaRequest = function(path, params) { + var headers = { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.token + }; + + var options = { + method: 'GET', + url: this.url + path, + params: params, + headers: headers, + withCredentials: true, + }; + + return this.backendSrv.datasourceRequest(options).catch(function(err) { + if (err.status !== 0 || err.status >= 300) { + var monasca_response + if (err.data) { + if (err.data.message){ + monasca_response = err.data.message; + } else{ + var err_name = Object.keys(err.data)[0] + monasca_response = err.data[err_name].message + } + } + if (monasca_response) { + throw { message: 'Monasca Error Response: ' + monasca_response }; + } else { + throw { message: 'Monasca Error Status: ' + err.status }; + } + } + }); + }; + + MonascaDatasource.prototype.metricFindQuery = function(query) { + return this.metricsQuery({}).then(function(data) { + var values = []; + data = data.data.elements; + for (var i = 0; i < data.length; i++) { + var dim_set = data[i].dimensions; + if (query in dim_set) { + var value = dim_set[query]; + if (values.indexOf(value) == -1) { + values.push(value); + } + } + } + return _.map(values, function(value) { + return {text: value}; + }); + }); + }; + + MonascaDatasource.prototype.listTemplates = function() { + var template_list = []; + for (var i = 0; i < self.templateSrv.variables.length; i++) { + template_list.push('$'+self.templateSrv.variables[i].name); + } + return template_list; + }; + + MonascaDatasource.prototype.testDatasource = function() { + return this.namesQuery().then(function () { + return { status: 'success', message: 'Data source is working', title: 'Success' }; + }); + }; + + MonascaDatasource.prototype.translateTime = function(date) { + if (date === 'now') { + return null; + } + return moment.utc(dateMath.parse(date).valueOf()).toISOString(); + }; + + MonascaDatasource.prototype.convertPeriod = function(target) { + var regex = target.match(/period=[^&]*/); + if (regex) { + var period = regex[0].substring('period='.length); + var matches = period.match(kbn.interval_regex); + if (matches) { + period = kbn.interval_to_seconds(period); + target = target.replace(regex, 'period='+period); + } + } + return target; + }; + + MonascaDatasource.prototype.isInt = function(str) { + var n = ~~Number(str); + return String(n) === str && n >= 0; + }; + + return MonascaDatasource; +}); diff --git a/directives.js b/directives.js new file mode 100644 index 0000000..6203f6b --- /dev/null +++ b/directives.js @@ -0,0 +1,17 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('metricQueryEditorMonasca', function() { + return {controller: 'MonascaQueryCtrl', templateUrl: 'app/plugins/datasource/monasca/partials/query.editor.html'}; + }); + + module.directive('metricQueryOptionsMonasca', function() { + return {templateUrl: 'app/plugins/datasource/monasca/partials/query.options.html'}; + }); + +}); diff --git a/img/openstack_logo.png b/img/openstack_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..468bc4ca196b909b4376327a55cc6f196a56f824 GIT binary patch literal 8199 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 literal 0 HcmV?d00001 diff --git a/module.js b/module.js new file mode 100644 index 0000000..1499d5d --- /dev/null +++ b/module.js @@ -0,0 +1,20 @@ +define([ + './datasource', + './query_ctrl' +], +function(MonascaDatasource, MonascaQueryCtrl) { + 'use strict'; + + var MonascaConfigCtrl = function() {}; + MonascaConfigCtrl.templateUrl = "partials/config.html"; + + var MonascaQueryOptionsCtrl = function() {}; + MonascaQueryOptionsCtrl.templateUrl = "partials/query.options.html"; + + return { + 'Datasource': MonascaDatasource, + 'QueryCtrl': MonascaQueryCtrl, + 'ConfigCtrl': MonascaConfigCtrl, + 'QueryOptionsCtrl': MonascaQueryOptionsCtrl + }; +}); \ No newline at end of file diff --git a/partials/config.html b/partials/config.html new file mode 100644 index 0000000..0f16a45 --- /dev/null +++ b/partials/config.html @@ -0,0 +1,55 @@ +
+

Http settings

+
+
+ Url + + +

Specify a complete HTTP url (for example http://your_server:8080)

+ + Your access method is Direct, this means the url + needs to be accessable from the browser. + + + Your access method is currently Proxy, this means the url + needs to be accessable from the grafana backend. + +
+
+
+ +
+
+ Access +
+ + + Direct = url is used directly from browser
+ Proxy = Grafana backend will proxy the request +
+
+
+
+
+ +
+

Monasca details

+
+
+ Token + + + A token is required to autenticate if Keystone auth is not set. + +
+
+
+
+ Auth + + +
+
+
diff --git a/partials/query.editor.html b/partials/query.editor.html new file mode 100644 index 0000000..833b9d7 --- /dev/null +++ b/partials/query.editor.html @@ -0,0 +1,99 @@ + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + + + +
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+ + +
+
+
+
+
+
+
diff --git a/partials/query.options.html b/partials/query.options.html new file mode 100644 index 0000000..837de24 --- /dev/null +++ b/partials/query.options.html @@ -0,0 +1,52 @@ +
+ +
+ +
+
+
Dimensions Templating
+
    +
  • Template variables can be used as normal
  • +
  • $all : Use as a dimension value to automatically group the query by all valid values for the dimension
  • +
  • When 'group_by' is selected, $all will be ignored, and the query will group the data on all unspecified dimensions
  • +
+
+ +
+
Aliasing
+
    +
  • Template variables can be used as normal
  • +
  • @dimension_name : Use to alias using the given dimension_name
  • +
+
+ +
+
Period
+
    +
  • Required when function is not none
  • +
  • Defaults to seconds if s,m,h,d is not specified
  • +
  • Works with grafana interval templates
  • +
+
+
diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..c1ce044 --- /dev/null +++ b/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "Monasca", + "id": "monasca-grafana-datasource", + "type": "datasource", + + "staticRoot": ".", + + "partials": { + "config": "app/plugins/datasource/monasca/partials/config.html" + }, + + "metrics": true, + "annotations": false, + + "info": { + "description": "datasource for the Monasca Api", + "author": { + "name": "OpenStack", + "url": "https://wiki.openstack.org/wiki/Monasca" + }, + "logos": { + "small": "img/openstack_logo.png", + "large": "img/openstack_logo.png" + }, + "links": [ + {"name": "GitHub", "url": "https://github.com/openstack/monasca-grafana-datasource"}, + {"name": "License", "url": "https://github.com/openstack/monasca-grafana-datasource/LICENSE"} + ], + "version": "1.0.0", + "updated": "2016-07-26" + }, + + "dependencies": { + "grafanaVersion": "3.x.x", + "plugins": [ ] + } +} diff --git a/query_ctrl.js b/query_ctrl.js new file mode 100644 index 0000000..b70f2e7 --- /dev/null +++ b/query_ctrl.js @@ -0,0 +1,171 @@ +define([ + 'angular', + 'lodash', + 'app/plugins/sdk' +], +function (angular, _, sdk) { + 'use strict'; + + var MonascaQueryCtrl = (function(_super) { + + var self; + var metricList = null; + var dimensionList = { 'keys' : [], 'values' : {} }; + var currentDimension = null; + + function MonascaQueryCtrl($scope, $injector, templateSrv, $q, uiSegmentSrv) { + _super.call(this, $scope, $injector); + this.q = $q; + this.uiSegmentSrv = uiSegmentSrv; + this.templateSrv = templateSrv; + + if (!this.target.aggregator) { + this.target.aggregator = 'avg'; + } + if (!this.target.period) { + this.target.period = '300'; + } + if (!this.target.dimensions) { + this.target.dimensions = []; + } + + this.validateTarget(); + if (this.target.metric) { + this.resetDimensionList(); + } + + self = this; + } + + MonascaQueryCtrl.prototype = Object.create(_super.prototype); + MonascaQueryCtrl.prototype.constructor = MonascaQueryCtrl; + + MonascaQueryCtrl.templateUrl = 'partials/query.editor.html'; + + MonascaQueryCtrl.prototype.targetBlur = function() { + this.validateTarget(); + if (!_.isEqual(this.oldTarget, this.target) && _.isEmpty(this.target.error)) { + this.oldTarget = angular.copy(this.target); + this.refresh(); + } + }; + + MonascaQueryCtrl.prototype.validateTarget = function() { + this.target.error = ""; + if (!this.target.metric) { + this.target.error = "No metric specified"; + } + if (this.target.aggregator != 'none' && !this.target.period) { + this.target.error = "You must supply a period when using an aggregator"; + } + for (var i = 0; i < this.target.dimensions.length; i++) { + if (!this.target.dimensions[i].key) { + this.target.error = "One or more dimensions is missing a key"; + break; + } + if (!this.target.dimensions[i].value){ + this.target.error = "One or more dimensions is missing a value"; + break; + } + } + if (this.target.error) { + console.log(this.target.error); + } + }; + + ////////////////////////////// + // METRIC + ////////////////////////////// + + MonascaQueryCtrl.prototype.suggestMetrics = function(query, callback) { + if (!metricList) { + self.datasource.namesQuery() + .then(self.datasource.convertNamesList) + .then(function(metrics) { + metricList = metrics; + callback(metrics); + }); + } + else { + return metricList; + } + }; + + MonascaQueryCtrl.prototype.onMetricChange = function() { + this.resetDimensionList(); + this.targetBlur(); + }; + + ////////////////////////////// + // ALIAS + ////////////////////////////// + + MonascaQueryCtrl.prototype.suggestAlias = function(query, callback) { + var upToLastTag = query.substr(0, query.lastIndexOf('@')); + var suggestions = self.datasource.listTemplates(); + var dimensions = self.suggestDimensionKeys(query, callback); + for (var i = 0; i < dimensions.length; i++) { + suggestions.push(upToLastTag+"@"+dimensions[i]); + } + return suggestions; + }; + + ////////////////////////////// + // DIMENSIONS + ////////////////////////////// + + MonascaQueryCtrl.prototype.resetDimensionList = function() { + dimensionList = { 'keys' : [], 'values' : {} }; + if (this.target.metric) { + this.datasource.metricsQuery({'name' : this.target.metric}) + .then(this.datasource.buildDimensionList) + .then(function(dimensions) { + dimensionList = dimensions; + }); + } + }; + + MonascaQueryCtrl.prototype.suggestDimensionKeys = function(query, callback) { + if (dimensionList.keys.length === 0 && self.target.metric) { + self.datasource.metricsQuery({'name' : self.target.metric}) + .then(self.datasource.buildDimensionList) + .then(function(dimensions) { + dimensionList = dimensions; + callback(dimensions.keys); + }); + } + else { + return dimensionList.keys; + } + }; + + MonascaQueryCtrl.prototype.suggestDimensionValues = function(query, callback) { + var values = ['$all']; + values = values.concat(self.datasource.listTemplates()); + if (currentDimension.key && currentDimension.key in dimensionList.values) { + values = values.concat(dimensionList.values[currentDimension.key]); + } + return values; + }; + + MonascaQueryCtrl.prototype.editDimension = function(index) { + currentDimension = this.target.dimensions[index]; + }; + + MonascaQueryCtrl.prototype.addDimension = function() { + this.target.dimensions.push({}); + }; + + MonascaQueryCtrl.prototype.removeDimension = function(index) { + this.target.dimensions.splice(index, 1); + this.targetBlur(); + }; + + ////////////////////////////// + + return MonascaQueryCtrl; + + })(sdk.QueryCtrl); + + return MonascaQueryCtrl; +});