luci/applications/luci-app-statistics/htdocs/luci-static/resources/statistics/rrdtool.js

759 lines
21 KiB
JavaScript
Raw Normal View History

'use strict';
'require baseclass';
'require fs';
'require uci';
'require tools.prng as random';
function subst(str, val) {
return str.replace(/%(H|pn|pi|dt|di|ds)/g, function(m, p1) {
switch (p1) {
case 'H': return val.host || '';
case 'pn': return val.plugin || '';
case 'pi': return val.pinst || '';
case 'dt': return val.dtype || '';
case 'di': return val.dinst || '';
case 'ds': return val.dsrc || '';
}
});
}
var i18n = L.Class.singleton({
title: function(host, plugin, pinst, dtype, dinst, user_title) {
var title = user_title || 'p=%s/pi=%s/dt=%s/di=%s'.format(
plugin,
pinst || '(nil)',
dtype || '(nil)',
dinst || '(nil)'
);
return subst(title, {
host: host,
plugin: plugin,
pinst: pinst,
dtype: dtype,
dinst: dinst
});
},
label: function(host, plugin, pinst, dtype, dinst, user_label) {
var label = user_label || 'dt=%s/%di=%s'.format(
dtype || '(nil)',
dinst || '(nil)'
);
return subst(label, {
host: host,
plugin: plugin,
pinst: pinst,
dtype: dtype,
dinst: dinst
});
},
ds: function(host, source) {
var label = source.title || 'dt=%s/di=%s/ds=%s'.format(
source.type || '(nil)',
source.instance || '(nil)',
source.ds || '(nil)'
);
return subst(label, {
host: host,
dtype: source.type,
dinst: source.instance,
dsrc: source.ds
}).replace(/:/g, '\\:');
}
});
var colors = L.Class.singleton({
fromString: function(s) {
if (typeof(s) != 'string' || !s.match(/^[0-9a-fA-F]{6}$/))
return null;
return [
parseInt(s.substring(0, 2), 16),
parseInt(s.substring(2, 4), 16),
parseInt(s.substring(4, 6), 16)
];
},
asString: function(c) {
if (!Array.isArray(c) || c.length != 3)
return null;
return '%02x%02x%02x'.format(c[0], c[1], c[2]);
},
defined: function(i) {
var t = [
[230, 25, 75],
[245, 130, 48],
[255, 225, 25],
[60, 180, 75],
[70, 240, 240],
[0, 130, 200],
[0, 0, 128],
[170, 110, 40]
];
return this.asString(t[i % t.length]);
},
random: function() {
var r = random.get(255),
g = random.get(255),
min = 0, max = 255;
if (r + g < 255)
min = 255 - r - g;
else
max = 511 - r - g;
var b = min + Math.floor(random.get() * (max - min));
return [ r, g, b ];
},
faded: function(fg, bg, alpha) {
fg = this.fromString(fg) || (this.asString(fg) ? fg : null);
bg = this.fromString(bg) || (this.asString(bg) ? bg : [255, 255, 255]);
alpha = !isNaN(alpha) ? +alpha : 0.25;
if (!fg)
return null;
return [
(alpha * fg[0]) + ((1.0 - alpha) * bg[0]),
(alpha * fg[1]) + ((1.0 - alpha) * bg[1]),
(alpha * fg[2]) + ((1.0 - alpha) * bg[2])
];
}
});
var rrdtree = {},
graphdefs = {};
return baseclass.extend({
__init__: function() {
this.opts = {};
},
load: function() {
return Promise.all([
L.resolveDefault(fs.list('/www' + L.resource('statistics/rrdtool/definitions')), []),
fs.trimmed('/proc/sys/kernel/hostname'),
uci.load('luci_statistics')
]).then(L.bind(function(data) {
var definitions = data[0],
hostname = data[1];
this.opts.host = uci.get('luci_statistics', 'collectd', 'Hostname') || hostname;
this.opts.timespan = uci.get('luci_statistics', 'rrdtool', 'default_timespan') || 3600;
this.opts.width = uci.get('luci_statistics', 'rrdtool', 'image_width') || 600;
this.opts.height = uci.get('luci_statistics', 'rrdtool', 'image_height') || 150;
this.opts.rrdpath = (uci.get('luci_statistics', 'collectd_rrdtool', 'DataDir') || '/tmp/rrd').replace(/\/$/, '');
this.opts.rrasingle = (uci.get('luci_statistics', 'collectd_rrdtool', 'RRASingle') == '1');
this.opts.rramax = (uci.get('luci_statistics', 'collectd_rrdtool', 'RRAMax') == '1');
graphdefs = {};
var tasks = [ this.scan() ];
for (var i = 0; i < definitions.length; i++) {
var m = definitions[i].name.match(/^(.+)\.js$/);
if (definitions[i].type != 'file' || m == null)
continue;
tasks.push(L.require('statistics.rrdtool.definitions.' + m[1]).then(L.bind(function(name, def) {
graphdefs[name] = def;
}, this, m[1])));
}
return Promise.all(tasks);
}, this));
},
ls: function() {
var dir = this.opts.rrdpath;
return L.resolveDefault(fs.list(dir), []).then(function(entries) {
var tasks = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].type != 'directory')
continue;
tasks.push(L.resolveDefault(fs.list(dir + '/' + entries[i].name), []).then(L.bind(function(entries) {
var tasks = [];
for (var j = 0; j < entries.length; j++) {
if (entries[j].type != 'directory')
continue;
tasks.push(L.resolveDefault(fs.list(dir + '/' + this.name + '/' + entries[j].name), []).then(L.bind(function(entries) {
return Object.assign(this, {
entries: entries.filter(function(e) {
return e.type == 'file' && e.name.match(/\.rrd$/);
})
});
}, entries[j])));
}
return Promise.all(tasks).then(L.bind(function(entries) {
return Object.assign(this, {
entries: entries
});
}, this));
}, entries[i])));
}
return Promise.all(tasks);
});
},
scan: function() {
return this.ls().then(L.bind(function(entries) {
rrdtree = {};
for (var i = 0; i < entries.length; i++) {
var hostInstance = entries[i].name;
rrdtree[hostInstance] = rrdtree[hostInstance] || {};
for (var j = 0; j < entries[i].entries.length; j++) {
var m = entries[i].entries[j].name.match(/^([^-]+)(?:-(.+))?$/);
if (!m)
continue;
var pluginName = m[1],
pluginInstance = m[2] || '';
rrdtree[hostInstance][pluginName] = rrdtree[hostInstance][pluginName] || {};
rrdtree[hostInstance][pluginName][pluginInstance] = rrdtree[hostInstance][pluginName][pluginInstance] || {};
for (var k = 0; k < entries[i].entries[j].entries.length; k++) {
var m = entries[i].entries[j].entries[k].name.match(/^([^-]+)(?:-(.+))?\.rrd$/);
if (!m)
continue;
var dataType = m[1],
dataInstance = m[2] || '';
rrdtree[hostInstance][pluginName][pluginInstance][dataType] = rrdtree[hostInstance][pluginName][pluginInstance][dataType] || [];
rrdtree[hostInstance][pluginName][pluginInstance][dataType].push(dataInstance);
}
}
}
}, this));
},
hostInstances: function() {
return Object.keys(rrdtree).sort();
},
pluginNames: function(hostInstance) {
return Object.keys(rrdtree[hostInstance] || {}).sort();
},
pluginInstances: function(hostInstance, pluginName) {
return Object.keys((rrdtree[hostInstance] || {})[pluginName] || {}).sort(function(a, b) {
var x = a.match(/^(\d+)\b/),
y = b.match(/^(\d+)\b/);
if (!x != !y)
return !x - !y;
else if (x && y && x[0] != y[0])
return +x[0] - +y[0];
else
return a > b;
});
},
dataTypes: function(hostInstance, pluginName, pluginInstance) {
return Object.keys(((rrdtree[hostInstance] || {})[pluginName] || {})[pluginInstance] || {}).sort();
},
dataInstances: function(hostInstance, pluginName, pluginInstance, dataType) {
return ((((rrdtree[hostInstance] || {})[pluginName] || {})[pluginInstance] || {})[dataType] || []).sort();
},
pluginTitle: function(pluginName) {
var def = graphdefs[pluginName];
return (def ? def.title : null) || pluginName;
},
hasDefinition: function(pluginName) {
return (graphdefs[pluginName] != null);
},
hasInstanceDetails: function(hostInstance, pluginName, pluginInstance) {
var def = graphdefs[pluginName];
if (!def || typeof(def.rrdargs) != 'function')
return false;
var optlist = this._forcelol(def.rrdargs(this, hostInstance, pluginName, pluginInstance, null, false));
for (var i = 0; i < optlist.length; i++)
if (optlist[i].detail)
return true;
return false;
},
_mkpath: function(host, plugin, plugin_instance, dtype, data_instance) {
var path = host + '/' + plugin;
if (plugin_instance != null && plugin_instance != '')
path += '-' + plugin_instance;
path += '/' + dtype;
if (data_instance != null && data_instance != '')
path += '-' + data_instance;
return path;
},
mkrrdpath: function(/* ... */) {
return '%s/%s.rrd'.format(
this.opts.rrdpath,
this._mkpath.apply(this, arguments)
).replace(/[\\:]/g, '\\$&');
},
_forcelol: function(list) {
return L.isObject(list[0]) ? list : [ list ];
},
_rrdtool: function(def, rrd, timespan, width, height, cache) {
var cmdline = [
'graph', '-', '-a', 'PNG',
'-s', 'NOW-%s'.format(timespan || this.opts.timespan),
'-e', 'NOW-15',
'-w', width || this.opts.width,
'-h', height || this.opts.height
];
for (var i = 0; i < def.length; i++) {
var opt = String(def[i]);
if (rrd)
opt = opt.replace(/\{file\}/g, rrd);
cmdline.push(opt);
}
if (L.isObject(cache)) {
var key = sfh(cmdline.join('\0'));
if (!cache.hasOwnProperty(key))
cache[key] = fs.exec_direct('/usr/bin/rrdtool', cmdline, 'blob', true);
return cache[key];
}
return fs.exec_direct('/usr/bin/rrdtool', cmdline, 'blob', true);
},
_generic: function(opts, host, plugin, plugin_instance, dtype, index) {
var defs = [],
gopts = this.opts,
_args = [],
_sources = [],
_stack_neg = [],
_stack_pos = [],
_longest_name = 0,
_has_totals = false;
/* use the plugin+instance+type as seed for the prng to ensure the
same pseudo-random color sequence for each render */
random.seed(sfh([plugin, plugin_instance || '', dtype || ''].join('.')));
function __def(source) {
var inst = source.sname,
rrd = source.rrd,
ds = source.ds || 'value';
_args.push(
'DEF:%s_avg_raw=%s:%s:AVERAGE'.format(inst, rrd, ds),
'CDEF:%s_avg=%s_avg_raw,%s'.format(inst, inst, source.transform_rpn)
);
if (!gopts.rrasingle)
_args.push(
'DEF:%s_min_raw=%s:%s:MIN'.format(inst, rrd, ds),
'CDEF:%s_min=%s_min_raw,%s'.format(inst, inst, source.transform_rpn),
'DEF:%s_max_raw=%s:%s:MAX'.format(inst, rrd, ds),
'CDEF:%s_max=%s_max_raw,%s'.format(inst, inst, source.transform_rpn)
);
_args.push(
'CDEF:%s_nnl=%s_avg,UN,0,%s_avg,IF'.format(inst, inst, inst)
);
}
function __cdef(source) {
var prev;
if (source.flip)
prev = _stack_neg[_stack_neg.length - 1];
else
prev = _stack_pos[_stack_pos.length - 1];
/* is first source in stack or overlay source: source_stk = source_nnl */
if (prev == null || source.overlay) {
/* create cdef statement for cumulative stack (no NaNs) and also
for display (preserving NaN where no points should be displayed) */
if (gopts.rrasingle || !gopts.rramax)
_args.push(
'CDEF:%s_stk=%s_nnl'.format(source.sname, source.sname),
'CDEF:%s_plot=%s_avg'.format(source.sname, source.sname)
);
else
_args.push(
'CDEF:%s_stk=%s_nnl'.format(source.sname, source.sname),
'CDEF:%s_plot=%s_max'.format(source.sname, source.sname)
);
}
/* is subsequent source without overlay: source_stk = source_nnl + previous_stk */
else {
/* create cdef statement */
if (gopts.rrasingle || !gopts.rramax)
_args.push(
'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source.sname, source.sname, prev),
'CDEF:%s_plot=%s_avg,%s_stk,+'.format(source.sname, source.sname, prev)
);
else
_args.push(
'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source.sname, source.sname, prev),
'CDEF:%s_plot=%s_max,%s_stk,+'.format(source.sname, source.sname, prev)
);
}
/* create multiply by minus one cdef if flip is enabled */
if (source.flip) {
_args.push('CDEF:%s_neg=%s_plot,-1,*'.format(source.sname, source.sname));
/* push to negative stack if overlay is disabled */
if (!source.overlay)
_stack_neg.push(source.sname);
}
/* no flipping, push to positive stack if overlay is disabled */
else if (!source.overlay) {
/* push to positive stack */
_stack_pos.push(source.sname);
}
/* calculate total amount of data if requested */
if (source.total)
_args.push(
'CDEF:%s_avg_sample=%s_avg,UN,0,%s_avg,IF,sample_len,*'.format(source.sname, source.sname, source.sname),
'CDEF:%s_avg_sum=PREV,UN,0,PREV,IF,%s_avg_sample,+'.format(source.sname, source.sname, source.sname)
);
}
/* local helper: create cdefs required for calculating total values */
function __cdef_totals() {
if (_has_totals)
_args.push(
'CDEF:mytime=%s_avg,TIME,TIME,IF'.format(_sources[0].sname),
'CDEF:sample_len_raw=mytime,PREV(mytime),-',
'CDEF:sample_len=sample_len_raw,UN,0,sample_len_raw,IF'
);
}
/* local helper: create line and area statements */
function __line(source) {
var line_color, area_color, legend, variable;
/* find colors: try source, then opts.colors; fall back to random color */
if (typeof(source.color) == 'string') {
line_color = source.color;
area_color = colors.fromString(line_color);
}
else if (typeof(opts.colors[source.name.replace(/\W/g, '_')]) == 'string') {
line_color = opts.colors[source.name.replace(/\W/g, '_')];
area_color = colors.fromString(line_color);
}
else {
area_color = colors.random();
line_color = colors.asString(area_color);
}
/* derive area background color from line color */
area_color = colors.asString(colors.faded(area_color));
/* choose source_plot or source_neg variable depending on flip state */
variable = source.flip ? 'neg' : 'plot';
/* create legend */
legend = '%%-%us'.format(_longest_name).format(source.title);
/* create area is not disabled */
if (!source.noarea)
_args.push('AREA:%s_%s#%s'.format(source.sname, variable, area_color));
/* create line statement */
_args.push('LINE%d:%s_%s#%s:%s'.format(
source.width || (source.noarea ? 2 : 1),
source.sname, variable, line_color, legend
));
}
/* local helper: create gprint statements */
function __gprint(source) {
var numfmt = opts.number_format || '%6.1lf',
totfmt = opts.totals_format || '%5.1lf%s';
/* don't include MIN if rrasingle is enabled */
if (!gopts.rrasingle)
_args.push('GPRINT:%s_min:MIN:\tMin\\: %s'.format(source.sname, numfmt));
/* don't include AVERAGE if noavg option is set */
if (!source.noavg)
_args.push('GPRINT:%s_avg:AVERAGE:\tAvg\\: %s'.format(source.sname, numfmt));
/* don't include MAX if rrasingle is enabled */
if (!gopts.rrasingle)
_args.push('GPRINT:%s_max:MAX:\tMax\\: %s'.format(source.sname, numfmt));
/* include total count if requested else include LAST */
if (source.total)
_args.push('GPRINT:%s_avg_sum:LAST:(ca. %s Total)\\l'.format(source.sname, totfmt));
else
_args.push('GPRINT:%s_avg:LAST:\tLast\\: %s\\l'.format(source.sname, numfmt));
}
/*
* find all data sources
*/
/* find data types */
var data_types = dtype ? [ dtype ] : (opts.data.types || []);
if (!(dtype || opts.data.types)) {
if (L.isObject(opts.data.instances))
data_types.push.apply(data_types, Object.keys(opts.data.instances));
else if (L.isObject(opts.data.sources))
data_types.push.apply(data_types, Object.keys(opts.data.sources));
}
/* iterate over data types */
for (var i = 0; i < data_types.length; i++) {
/* find instances */
var data_instances;
if (!opts.per_instance) {
if (L.isObject(opts.data.instances) && Array.isArray(opts.data.instances[data_types[i]]))
data_instances = opts.data.instances[data_types[i]];
else
data_instances = this.dataInstances(host, plugin, plugin_instance, data_types[i]);
}
if (!Array.isArray(data_instances) || data_instances.length == 0)
data_instances = [ '' ];
/* iterate over data instances */
for (var j = 0; j < data_instances.length; j++) {
/* construct combined data type / instance name */
var dname = data_types[i];
if (data_instances[j].length)
dname += '_' + data_instances[j];
/* find sources */
var data_sources = [ 'value' ];
if (L.isObject(opts.data.sources)) {
if (Array.isArray(opts.data.sources[dname]))
data_sources = opts.data.sources[dname];
else if (Array.isArray(opts.data.sources[data_types[i]]))
data_sources = opts.data.sources[data_types[i]];
}
/* iterate over data sources */
for (var k = 0; k < data_sources.length; k++) {
var dsname = data_types[i] + '_' + data_instances[j].replace(/\W/g, '_') + '_' + data_sources[k],
altname = data_types[i] + '__' + data_sources[k];
/* find datasource options */
var dopts = {};
if (L.isObject(opts.data.options)) {
if (L.isObject(opts.data.options[dsname]))
dopts = opts.data.options[dsname];
else if (L.isObject(opts.data.options[altname]))
dopts = opts.data.options[altname];
else if (L.isObject(opts.data.options[dname]))
dopts = opts.data.options[dname];
else if (L.isObject(opts.data.options[data_types[i]]))
dopts = opts.data.options[data_types[i]];
}
/* store values */
var source = {
rrd: dopts.rrd || this.mkrrdpath(host, plugin, plugin_instance, data_types[i], data_instances[j]),
color: dopts.color || colors.asString(colors.random()),
flip: dopts.flip || false,
total: dopts.total || false,
overlay: dopts.overlay || false,
transform_rpn: dopts.transform_rpn || '0,+',
noarea: dopts.noarea || false,
noavg: dopts.noavg || false,
title: dopts.title || null,
weight: dopts.weight || (dopts.negweight ? -+data_instances[j] : null) || (dopts.posweight ? +data_instances[j] : null) || null,
ds: data_sources[k],
type: data_types[i],
instance: data_instances[j],
index: _sources.length + 1,
sname: String(_sources.length + 1) + data_types[i]
};
_sources.push(source);
/* generate datasource title */
source.title = i18n.ds(host, source);
/* find longest name */
_longest_name = Math.max(_longest_name, source.title.length);
/* has totals? */
if (source.total)
_has_totals = true;
}
}
}
/*
* construct diagrams
*/
/* if per_instance is enabled then find all instances from the first datasource in diagram */
/* if per_instance is disabled then use an empty pseudo instance and use model provided values */
var instances = [ '' ];
if (opts.per_instance)
instances = this.dataInstances(host, plugin, plugin_instance, _sources[0].type);
/* iterate over instances */
for (var i = 0; i < instances.length; i++) {
/* store title and vlabel */
_args.push(
'-t', i18n.title(host, plugin, plugin_instance, _sources[0].type, instances[i], opts.title),
'-v', i18n.label(host, plugin, plugin_instance, _sources[0].type, instances[i], opts.vlabel)
);
if (opts.y_max)
_args.push('-u', String(opts.y_max));
if (opts.y_min)
_args.push('-l', String(opts.y_min));
if (opts.units_exponent)
_args.push('-X', String(opts.units_exponent));
if (opts.alt_autoscale)
_args.push('-A');
if (opts.alt_autoscale_max)
_args.push('-M');
/* store additional rrd options */
if (Array.isArray(opts.rrdopts))
for (var j = 0; j < opts.rrdopts.length; j++)
_args.push(String(opts.rrdopts[j]));
/* sort sources */
_sources.sort(function(a, b) {
var x = a.weight || a.index || 0,
y = b.weight || b.index || 0;
return +x - +y;
});
/* define colors in order */
if (opts.ordercolor)
for (var j = 0; j < _sources.length; j++)
_sources[j].color = colors.defined(j);
/* create DEF statements for each instance */
for (var j = 0; j < _sources.length; j++) {
/* fixup properties for per instance mode... */
if (opts.per_instance) {
_sources[j].instance = instances[i];
_sources[j].rrd = this.mkrrdpath(host, plugin, plugin_instance, _sources[j].type, instances[i]);
}
__def(_sources[j]);
}
/* create CDEF required for calculating totals */
__cdef_totals();
/* create CDEF statements for each instance in reversed order */
for (var j = _sources.length - 1; j >= 0; j--)
__cdef(_sources[j]);
/* create LINE1, AREA and GPRINT statements for each instance */
for (var j = 0; j < _sources.length; j++) {
__line(_sources[j]);
__gprint(_sources[j]);
}
/* push arg stack to definition list */
defs.push(_args);
/* reset stacks */
_args = [];
_stack_pos = [];
_stack_neg = [];
}
return defs;
},
render: function(plugin, plugin_instance, is_index, hostname, timespan, width, height, cache) {
var pngs = [];
/* check for a whole graph handler */
var def = graphdefs[plugin];
if (def && typeof(def.rrdargs) == 'function') {
/* temporary image matrix */
var _images = [];
/* get diagram definitions */
var optlist = this._forcelol(def.rrdargs(this, hostname, plugin, plugin_instance, null, is_index));
for (var i = 0; i < optlist.length; i++) {
var opt = optlist[i];
if (!is_index || !opt.detail) {
_images[i] = [];
/* get diagram definition instances */
var diagrams = this._generic(opt, hostname, plugin, plugin_instance, null, i);
/* render all diagrams */
for (var j = 0; j < diagrams.length; j++) {
/* exec */
_images[i][j] = this._rrdtool(diagrams[j], null, timespan, width, height, cache);
}
}
}
/* remember images - XXX: fixme (will cause probs with asymmetric data) */
for (var y = 0; y < _images[0].length; y++)
for (var x = 0; x < _images.length; x++)
pngs.push(_images[x][y]);
}
return Promise.all(pngs);
}
});