define([ 'angular', 'lodash', 'app/core/utils/datemath', 'app/core/utils/kbn', './queryCtrl', './directives', ], function (angular, _, dateMath, kbn) { 'use strict'; var module = angular.module('grafana.services'); module.factory('KairosDBDatasource', function($q, backendSrv, templateSrv) { function KairosDBDatasource(datasource) { this.type = datasource.type; this.url = datasource.url; this.name = datasource.name; this.supportMetrics = true; } // Called once per panel (graph) KairosDBDatasource.prototype.query = function(options) { var start = options.rangeRaw.from; var end = options.rangeRaw.to; var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options))); var plotParams = _.compact(_.map(options.targets, function(target) { var alias = target.alias; if (typeof target.alias === 'undefined' || target.alias === "") { alias = target.metric; } if (!target.hide) { return { alias: alias, exouter: target.exOuter }; } else { return null; } })); var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams); // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { var d = $q.defer(); d.resolve({ data: [] }); return d.promise; } return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias, handleQueryError); }; KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { var reqBody = { metrics: queries, cache_time: 0 }; convertToKairosTime(start, reqBody, 'start'); convertToKairosTime(end, reqBody, 'end'); var options = { method: 'POST', url: this.url + '/api/v1/datapoints/query', data: reqBody }; return backendSrv.datasourceRequest(options); }; /** * Gets the list of metrics * @returns {*|Promise} */ KairosDBDatasource.prototype._performMetricSuggestQuery = function(metric) { var options = { url: this.url + '/api/v1/metricnames', method: 'GET' }; return backendSrv.datasourceRequest(options).then(function(response) { if (!response.data) { return $q.when([]); } var metrics = []; _.each(response.data.results, function(r) { if (r.indexOf(metric) >= 0) { metrics.push(r); } }); return metrics; }); }; KairosDBDatasource.prototype._performMetricKeyLookup = function(metric) { if(!metric) { return $q.when([]); } var options = { method: 'POST', url: this.url + '/api/v1/datapoints/query/tags', data: { metrics: [{ name: metric }], cache_time: 0, start_absolute: 0 } }; return backendSrv.datasourceRequest(options).then(function(result) { if (!result.data) { return $q.when([]); } var tagks = []; _.each(result.data.queries[0].results[0].tags, function(tagv, tagk) { if(tagks.indexOf(tagk) === -1) { tagks.push(tagk); } }); return tagks; }); }; KairosDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) { if(!metric || !key) { return $q.when([]); } var options = { method: 'POST', url: this.url + '/api/v1/datapoints/query/tags', data: { metrics: [{ name: metric }], cache_time: 0, start_absolute: 0 } }; return backendSrv.datasourceRequest(options).then(function(result) { if (!result.data) { return $q.when([]); } return result.data.queries[0].results[0].tags[key]; }); }; KairosDBDatasource.prototype.performTagSuggestQuery = function(metric) { var options = { url: this.url + '/api/v1/datapoints/query/tags', method: 'POST', data: { metrics: [{ name: metric }], cache_time: 0, start_absolute: 0 } }; return backendSrv.datasourceRequest(options).then(function(response) { if (!response.data) { return []; } else { return response.data.queries[0].results[0]; } }); }; KairosDBDatasource.prototype.metricFindQuery = function(query) { if (!query) { return $q.when([]); } var interpolated; try { interpolated = templateSrv.replace(query); } catch (err) { return $q.reject(err); } var responseTransform = function(result) { return _.map(result, function(value) { return {text: value}; }); }; var metrics_regex = /metrics\((.*)\)/; var tag_names_regex = /tag_names\((.*)\)/; var tag_values_regex = /tag_values\((.*),\s?(.*?)\)/; var metrics_query = interpolated.match(metrics_regex); if (metrics_query) { return this._performMetricSuggestQuery(metrics_query[1]).then(responseTransform); } var tag_names_query = interpolated.match(tag_names_regex); if (tag_names_query) { return this._performMetricKeyLookup(tag_names_query[1]).then(responseTransform); } var tag_values_query = interpolated.match(tag_values_regex); if (tag_values_query) { return this._performMetricKeyValueLookup(tag_values_query[1], tag_values_query[2]).then(responseTransform); } return $q.when([]); }; ///////////////////////////////////////////////////////////////////////// /// Formatting methods //////////////////////////////////////////////////////////////////////// /** * Requires a verion of KairosDB with every CORS defects fixed * @param results * @returns {*} */ function handleQueryError(results) { if (results.data.errors && !_.isEmpty(results.data.errors)) { var errors = { message: results.data.errors[0] }; return $q.reject(errors); } else { return $q.reject(results); } } function handleKairosDBQueryResponse(plotParams, results) { var output = []; var index = 0; _.each(results.data.queries, function(series) { _.each(series.results, function(result) { var target = plotParams[index].alias; var details = " ( "; _.each(result.group_by, function(element) { if (element.name === "tag") { _.each(element.group, function(value, key) { details += key + "=" + value + " "; }); } else if (element.name === "value") { details += 'value_group=' + element.group.group_number + " "; } else if (element.name === "time") { details += 'time_group=' + element.group.group_number + " "; } }); details += ") "; if (details !== " ( ) ") { target += details; } var datapoints = []; for (var i = 0; i < result.values.length; i++) { var t = Math.floor(result.values[i][0]); var v = result.values[i][1]; datapoints[i] = [v, t]; } if (plotParams[index].exouter) { datapoints = new PeakFilter(datapoints, 10); } output.push({ target: target, datapoints: datapoints }); }); index++; }); return { data: _.flatten(output) }; } function convertTargetToQuery(options, target) { if (!target.metric || target.hide) { return null; } var query = { name: templateSrv.replace(target.metric) }; query.aggregators = []; if (target.downsampling !== '(NONE)') { if (target.downsampling === undefined) { target.downsampling = 'avg'; target.sampling = '10s'; } query.aggregators.push({ name: target.downsampling, align_sampling: true, align_start_time: true, sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval) }); } if (target.horizontalAggregators) { _.each(target.horizontalAggregators, function(chosenAggregator) { var returnedAggregator = { name:chosenAggregator.name }; if (chosenAggregator.sampling_rate) { returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); returnedAggregator.align_sampling = true; returnedAggregator.align_start_time =true; } if (chosenAggregator.unit) { returnedAggregator.unit = chosenAggregator.unit + 's'; } if (chosenAggregator.factor && chosenAggregator.name === 'div') { returnedAggregator.divisor = chosenAggregator.factor; } else if (chosenAggregator.factor && chosenAggregator.name === 'scale') { returnedAggregator.factor = chosenAggregator.factor; } if (chosenAggregator.percentile) { returnedAggregator.percentile = chosenAggregator.percentile; } query.aggregators.push(returnedAggregator); }); } if (_.isEmpty(query.aggregators)) { delete query.aggregators; } if (target.tags) { query.tags = angular.copy(target.tags); _.forOwn(query.tags, function(value, key) { query.tags[key] = _.map(value, function(tag) { return templateSrv.replace(tag); }); }); } if (target.groupByTags || target.nonTagGroupBys) { query.group_by = []; if (target.groupByTags) { query.group_by.push({ name: "tag", tags: _.map(angular.copy(target.groupByTags), function(tag) { return templateSrv.replace(tag); }) }); } if (target.nonTagGroupBys) { _.each(target.nonTagGroupBys, function(rawGroupBy) { var formattedGroupBy = angular.copy(rawGroupBy); if (formattedGroupBy.name === 'time') { formattedGroupBy.range_size = KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); } query.group_by.push(formattedGroupBy); }); } } return query; } /////////////////////////////////////////////////////////////////////// /// Time conversion functions specifics to KairosDB ////////////////////////////////////////////////////////////////////// KairosDBDatasource.prototype.convertToKairosInterval = function(intervalString) { intervalString = templateSrv.replace(intervalString); var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/; var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/; var matches = intervalString.match(interval_regex_ms); if (!matches) { matches = intervalString.match(interval_regex); } if (!matches) { throw new Error('Invalid interval string, expecting a number followed by one of "y M w d h m s ms"'); } var value = matches[1]; var unit = matches[2]; if (value%1 !== 0) { if (unit === 'ms') { throw new Error('Invalid interval value, cannot be smaller than the millisecond'); } value = Math.round(kbn.intervals_in_seconds[unit] * value * 1000); unit = 'ms'; } return { value: value, unit: convertToKairosDBTimeUnit(unit) }; }; function convertToKairosTime(date, response_obj, start_stop_name) { var name; if (_.isString(date)) { if (date === 'now') { return; } else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) { date = date.substring(4); name = start_stop_name + "_relative"; var re_date = /(\d+)\s*(\D+)/; var result = re_date.exec(date); if (result) { var value = result[1]; var unit = result[2]; response_obj[name] = { value: value, unit: convertToKairosDBTimeUnit(unit) }; return; } console.log("Unparseable date", date); return; } date = dateMath.parse(date, start_stop_name === 'end'); } name = start_stop_name + "_absolute"; response_obj[name] = date.valueOf(); } function convertToKairosDBTimeUnit(unit) { switch (unit) { case 'ms': return 'milliseconds'; case 's': return 'seconds'; case 'm': return 'minutes'; case 'h': return 'hours'; case 'd': return 'days'; case 'w': return 'weeks'; case 'M': return 'months'; case 'y': return 'years'; default: console.log("Unknown unit ", unit); return ''; } } function PeakFilter(dataIn, limit) { var datapoints = dataIn; var arrLength = datapoints.length; if (arrLength <= 3) { return datapoints; } var LastIndx = arrLength - 1; // Check first point var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]); var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]); if (prvDelta >= limit && nxtDelta < limit) { datapoints[0][0] = datapoints[1][0]; } // Check last point prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]); nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]); if (prvDelta >= limit && nxtDelta < limit) { datapoints[LastIndx][0] = datapoints[LastIndx - 1][0]; } for (var i = 1; i < arrLength - 1; i++) { prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); if (prvDelta >= limit && nxtDelta >= limit) { datapoints[i][0] = (datapoints[i - 1][0] + datapoints[i + 1][0]) / 2; } } return datapoints; } return KairosDBDatasource; }); });