luci/applications/luci-app-statistics/htdocs/luci-static/resources/statistics/rrdtool.js
Hannu Nyman 5b3fa3cdc7 luci-app-statistics: adjust graph size fallback to defaults
Adjust the fallback timespan and graph width to the current config
defaults. (The default width in /etc/config/luci_statistics has been
600 already since commit 7ab8b51bd in March 2010.)

Expose the graph height in the config file and
increase it from 100 to 150 to keep the original aspect ratio.

Signed-off-by: Hannu Nyman <hannu.nyman@iki.fi>
2021-11-10 22:27:43 +02:00

758 lines
21 KiB
JavaScript

'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);
}
});