2020-02-13 19:45:26 +00:00
|
|
|
'use strict';
|
2020-04-03 08:00:06 +00:00
|
|
|
'require baseclass';
|
2020-02-13 19:45:26 +00:00
|
|
|
'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 = {};
|
|
|
|
|
2020-04-03 08:00:06 +00:00
|
|
|
return baseclass.extend({
|
2020-02-13 19:45:26 +00:00
|
|
|
__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;
|
2021-11-10 20:27:43 +00:00
|
|
|
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;
|
2020-02-13 19:45:26 +00:00
|
|
|
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) {
|
2020-02-22 22:32:44 +00:00
|
|
|
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;
|
|
|
|
});
|
2020-02-13 19:45:26 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
2020-02-22 22:32:44 +00:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2020-02-13 19:45:26 +00:00
|
|
|
_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 ];
|
|
|
|
},
|
|
|
|
|
2020-02-22 22:32:44 +00:00
|
|
|
_rrdtool: function(def, rrd, timespan, width, height, cache) {
|
2020-02-13 19:45:26 +00:00
|
|
|
var cmdline = [
|
|
|
|
'graph', '-', '-a', 'PNG',
|
|
|
|
'-s', 'NOW-%s'.format(timespan || this.opts.timespan),
|
2020-12-18 15:41:18 +00:00
|
|
|
'-e', 'NOW-15',
|
2020-02-13 19:45:26 +00:00
|
|
|
'-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);
|
|
|
|
}
|
|
|
|
|
2020-02-22 22:32:44 +00:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
2020-02-13 19:45:26 +00:00
|
|
|
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;
|
|
|
|
|
2020-02-21 22:01:22 +00:00
|
|
|
/* 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('.')));
|
|
|
|
|
2020-02-13 19:45:26 +00:00
|
|
|
function __def(source) {
|
|
|
|
var inst = source.sname,
|
2020-08-17 06:29:01 +00:00
|
|
|
rrd = source.rrd,
|
2020-02-13 19:45:26 +00:00
|
|
|
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
|
2020-02-20 12:48:00 +00:00
|
|
|
for display (preserving NaN where no points should be displayed) */
|
2020-02-13 19:45:26 +00:00
|
|
|
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));
|
|
|
|
|
2020-09-08 05:21:55 +00:00
|
|
|
/* don't include AVERAGE if noavg option is set */
|
2020-08-18 10:50:46 +00:00
|
|
|
if (!source.noavg)
|
|
|
|
_args.push('GPRINT:%s_avg:AVERAGE:\tAvg\\: %s'.format(source.sname, numfmt));
|
2020-02-13 19:45:26 +00:00
|
|
|
|
|
|
|
/* 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,
|
2020-08-18 10:50:46 +00:00
|
|
|
noavg: dopts.noavg || false,
|
2020-02-13 19:45:26 +00:00
|
|
|
title: dopts.title || null,
|
|
|
|
weight: dopts.weight || (dopts.negweight ? -+data_instances[j] : null) || (dopts.posweight ? +data_instances[j] : null) || null,
|
2020-02-19 13:28:28 +00:00
|
|
|
ds: data_sources[k],
|
2020-02-13 19:45:26 +00:00
|
|
|
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;
|
|
|
|
|
2020-02-20 12:48:00 +00:00
|
|
|
return +x - +y;
|
2020-02-13 19:45:26 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
/* 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;
|
|
|
|
},
|
|
|
|
|
2020-02-22 22:32:44 +00:00
|
|
|
render: function(plugin, plugin_instance, is_index, hostname, timespan, width, height, cache) {
|
2020-02-13 19:45:26 +00:00
|
|
|
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 */
|
2020-02-22 22:32:44 +00:00
|
|
|
_images[i][j] = this._rrdtool(diagrams[j], null, timespan, width, height, cache);
|
2020-02-13 19:45:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 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);
|
|
|
|
}
|
|
|
|
});
|