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 0000000..468bc4c Binary files /dev/null and b/img/openstack_logo.png differ 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
+ +
+ +
+
Aliasing
+ +
+ +
+
Period
+ +
+
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; +});