Kibana Timelion Plugin Enhancement
Why need to divide multiple valued seriesList?
Timelion is quite a great plugin for computing two series in kibana (version:4.5.4). The divide computing supports divisor as a number or a single series. However there are still some cases that we want to divide multiple series over other multiple series. So developers file a issue: Divide multi-valued seriesList by another multi-valued seriesList
Note, this implementation is in both kibana 4.5.4 and Kibana 5.3.4. It works well.
Fix patch
Inspired by the discussion in Divide multi-valued seriesList by another multi-valued seriesList , I have come up with another fix (ugly but workable) based on reduce. I have tested its basic functions, and you are welcome to file issues if you find any bugs.
For Kibana 4.5.4
The fix added two files to timelion.
(divideseries)$ git status
On branch divideseries
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: server/lib/reduce_series.js
new file: server/series_functions/divideseries.js
For Kibana 5.3.4
The fix added two files to timelion.
(divideseries)$ git status
On branch divideseries
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: src/core_plugins/timelion/server/lib/reduce_series.js
new file: src/core_plugins/timelion/server/series_functions/divideseries.js
reduce_series.js
var _ = require('lodash');
var Promise = require('bluebird');
/**
* Reduces multiple arrays into a single array using a function
* @param {Array} args - args[0] must always be a {type: 'seriesList'}
*
* - If only arg[0] exists, the seriesList will be reduced to a seriesList containing a single series
* - If multiple arguments are passed, each argument will be mapped onto each series in the seriesList.
* @params {Function} fn - Function used to combine points at same index in each array of each series in the seriesList.
* @return {seriesList}
*/
module.exports = function reduceMultiple(args, fn) {
return Promise.all(args).then(function (args) {
var seriesList = args.shift();
var argumentList = args.shift();
if (seriesList.type !== 'seriesList') {
throw new Error ('input must be a seriesList');
}
/*
if (_.isObject(argument) && argument.type === 'seriesList') {
if (argument.list.length !== 1) {
throw new Error ('argument must be a seriesList with a single series');
} else {
argument = argument.list[0];
}
}
*/
argumentList = argumentList.list;
/**
* get the group field
*/
function getGroupField (destObj) {
var label = destObj.label;
var split = label.split(' > ');
return split[split.length-2];
}
function getCorrespondingArg (destinationObject, argumentList) {
var destGroup = getGroupField(destinationObject);
for (var i = 0; i < argumentList.length; i++) {
if (destGroup === getGroupField(argumentList[i])) {
return argumentList[i];
}
}
return undefined;
}
function reduceSeries(series) {
return _.reduce(series, function (destinationObject, argument, i, p) {
var output = _.map(destinationObject.data, function (point, index) {
var value = point[1];
if (value == null) {
return [point[0], null];
}
if (_.isNumber(argument)) {
return [point[0], fn(value, argument, i, p)];
}
if (argument.data[index] == null || argument.data[index][1] == null) {
return [point[0], null];
}
return [point[0], fn(value, argument.data[index][1], i, p)];
});
// Output = single series
output = {
data: output
};
output = _.defaults(output, destinationObject);
return output;
});
}
var reduced;
if (argumentList != null) {
reduced = _.map(seriesList.list, function (series) {
var argument = getCorrespondingArg(series, argumentList);
if (!argument) return undefined;
return reduceSeries([series].concat(argument));
});
} else {
reduced = [reduceSeries(seriesList.list)];
}
seriesList.list = [];
for (var i = 0; i < reduced.length; i++) {
if (reduced[i] === undefined) continue;
seriesList.list.push(reduced[i]);
}
return seriesList;
}).catch(function (e) {
throw e;
});
};
divideseries.js
var reduceSeries = require('../lib/reduce_series.js');
var Chainable = require('../lib/classes/chainable');
module.exports = new Chainable('divideseries', {
args: [
{
name: 'inputSeries',
types: ['seriesList']
},
{
name: 'divisor',
types: ['seriesList', 'number'],
help: 'Series to divide by. It should have the same split field as the the divider. If a seriesList in divider cannot find the same group field in divisor list, then this seriesList will be ignored.'
}
],
help: 'Series to divide by. It should have the same split field as the the divider. If a seriesList in divider cannot find the same group field in divisor list, then this seriesList will be ignored.',
fn: function divideSeriesFn(args) {
return reduceSeries (args, function (a, b) {
return a / b;
});
}
});
Demo
I tested with the following results:
Query
.es('query1', split='cdn:2').divideseries(.es('query2', split='cdn:2'))
Result