5 'require tools.prng as random';
7 function subst(str
, val
) {
8 return str
.replace(/%(H|pn|pi|dt|di|ds)/g, function(m
, p1
) {
10 case 'H': return val
.host
|| '';
11 case 'pn': return val
.plugin
|| '';
12 case 'pi': return val
.pinst
|| '';
13 case 'dt': return val
.dtype
|| '';
14 case 'di': return val
.dinst
|| '';
15 case 'ds': return val
.dsrc
|| '';
20 var i18n
= L
.Class
.singleton({
21 title: function(host
, plugin
, pinst
, dtype
, dinst
, user_title
) {
22 var title
= user_title
|| 'p=%s/pi=%s/dt=%s/di=%s'.format(
38 label: function(host
, plugin
, pinst
, dtype
, dinst
, user_label
) {
39 var label
= user_label
|| 'dt=%s/%di=%s'.format(
53 ds: function(host
, source
) {
54 var label
= source
.title
|| 'dt=%s/di=%s/ds=%s'.format(
55 source
.type
|| '(nil)',
56 source
.instance
|| '(nil)',
63 dinst
: source
.instance
,
65 }).replace(/:/g
, '\\:');
69 var colors
= L
.Class
.singleton({
70 fromString: function(s
) {
71 if (typeof(s
) != 'string' || !s
.match(/^[0-9a-fA-F]{6}$/))
75 parseInt(s
.substring(0, 2), 16),
76 parseInt(s
.substring(2, 4), 16),
77 parseInt(s
.substring(4, 6), 16)
81 asString: function(c
) {
82 if (!Array
.isArray(c
) || c
.length
!= 3)
85 return '%02x%02x%02x'.format(c
[0], c
[1], c
[2]);
88 defined: function(i
) {
100 return this.asString(t
[i
% t
.length
]);
104 var r
= random
.get(255),
113 var b
= min
+ Math
.floor(random
.get() * (max
- min
));
118 faded: function(fg
, bg
, alpha
) {
119 fg
= this.fromString(fg
) || (this.asString(fg
) ? fg
: null);
120 bg
= this.fromString(bg
) || (this.asString(bg
) ? bg
: [255, 255, 255]);
121 alpha
= !isNaN(alpha
) ? +alpha
: 0.25;
127 (alpha
* fg
[0]) + ((1.0 - alpha
) * bg
[0]),
128 (alpha
* fg
[1]) + ((1.0 - alpha
) * bg
[1]),
129 (alpha
* fg
[2]) + ((1.0 - alpha
) * bg
[2])
137 return baseclass
.extend({
138 __init__: function() {
144 L
.resolveDefault(fs
.list('/www' + L
.resource('statistics/rrdtool/definitions')), []),
145 fs
.trimmed('/proc/sys/kernel/hostname'),
146 uci
.load('luci_statistics')
147 ]).then(L
.bind(function(data
) {
148 var definitions
= data
[0],
151 this.opts
.host
= uci
.get('luci_statistics', 'collectd', 'Hostname') || hostname
;
152 this.opts
.timespan
= uci
.get('luci_statistics', 'rrdtool', 'default_timespan') || 900;
153 this.opts
.width
= uci
.get('luci_statistics', 'rrdtool', 'image_width') || 400;
154 this.opts
.height
= uci
.get('luci_statistics', 'rrdtool', 'image_height') || 100;
155 this.opts
.rrdpath
= (uci
.get('luci_statistics', 'collectd_rrdtool', 'DataDir') || '/tmp/rrd').replace(/\/$/, '');
156 this.opts
.rrasingle
= (uci
.get('luci_statistics', 'collectd_rrdtool', 'RRASingle') == '1');
157 this.opts
.rramax
= (uci
.get('luci_statistics', 'collectd_rrdtool', 'RRAMax') == '1');
161 var tasks
= [ this.scan() ];
163 for (var i
= 0; i
< definitions
.length
; i
++) {
164 var m
= definitions
[i
].name
.match(/^(.+)\.js$/);
166 if (definitions
[i
].type
!= 'file' || m
== null)
169 tasks
.push(L
.require('statistics.rrdtool.definitions.' + m
[1]).then(L
.bind(function(name
, def
) {
170 graphdefs
[name
] = def
;
174 return Promise
.all(tasks
);
179 var dir
= this.opts
.rrdpath
;
181 return L
.resolveDefault(fs
.list(dir
), []).then(function(entries
) {
184 for (var i
= 0; i
< entries
.length
; i
++) {
185 if (entries
[i
].type
!= 'directory')
188 tasks
.push(L
.resolveDefault(fs
.list(dir
+ '/' + entries
[i
].name
), []).then(L
.bind(function(entries
) {
191 for (var j
= 0; j
< entries
.length
; j
++) {
192 if (entries
[j
].type
!= 'directory')
195 tasks
.push(L
.resolveDefault(fs
.list(dir
+ '/' + this.name
+ '/' + entries
[j
].name
), []).then(L
.bind(function(entries
) {
196 return Object
.assign(this, {
197 entries
: entries
.filter(function(e
) {
198 return e
.type
== 'file' && e
.name
.match(/\.rrd$/);
204 return Promise
.all(tasks
).then(L
.bind(function(entries
) {
205 return Object
.assign(this, {
212 return Promise
.all(tasks
);
217 return this.ls().then(L
.bind(function(entries
) {
220 for (var i
= 0; i
< entries
.length
; i
++) {
221 var hostInstance
= entries
[i
].name
;
223 rrdtree
[hostInstance
] = rrdtree
[hostInstance
] || {};
225 for (var j
= 0; j
< entries
[i
].entries
.length
; j
++) {
226 var m
= entries
[i
].entries
[j
].name
.match(/^([^-]+)(?:-(.+))?$/);
231 var pluginName
= m
[1],
232 pluginInstance
= m
[2] || '';
234 rrdtree
[hostInstance
][pluginName
] = rrdtree
[hostInstance
][pluginName
] || {};
235 rrdtree
[hostInstance
][pluginName
][pluginInstance
] = rrdtree
[hostInstance
][pluginName
][pluginInstance
] || {};
237 for (var k
= 0; k
< entries
[i
].entries
[j
].entries
.length
; k
++) {
238 var m
= entries
[i
].entries
[j
].entries
[k
].name
.match(/^([^-]+)(?:-(.+))?\.rrd$/);
244 dataInstance
= m
[2] || '';
246 rrdtree
[hostInstance
][pluginName
][pluginInstance
][dataType
] = rrdtree
[hostInstance
][pluginName
][pluginInstance
][dataType
] || [];
247 rrdtree
[hostInstance
][pluginName
][pluginInstance
][dataType
].push(dataInstance
);
254 hostInstances: function() {
255 return Object
.keys(rrdtree
).sort();
258 pluginNames: function(hostInstance
) {
259 return Object
.keys(rrdtree
[hostInstance
] || {}).sort();
262 pluginInstances: function(hostInstance
, pluginName
) {
263 return Object
.keys((rrdtree
[hostInstance
] || {})[pluginName
] || {}).sort(function(a
, b
) {
264 var x
= a
.match(/^(\d+)\b/),
265 y
= b
.match(/^(\d+)\b/);
269 else if (x
&& y
&& x
[0] != y
[0])
270 return +x
[0] - +y
[0];
276 dataTypes: function(hostInstance
, pluginName
, pluginInstance
) {
277 return Object
.keys(((rrdtree
[hostInstance
] || {})[pluginName
] || {})[pluginInstance
] || {}).sort();
280 dataInstances: function(hostInstance
, pluginName
, pluginInstance
, dataType
) {
281 return ((((rrdtree
[hostInstance
] || {})[pluginName
] || {})[pluginInstance
] || {})[dataType
] || []).sort();
284 pluginTitle: function(pluginName
) {
285 var def
= graphdefs
[pluginName
];
286 return (def
? def
.title
: null) || pluginName
;
289 hasDefinition: function(pluginName
) {
290 return (graphdefs
[pluginName
] != null);
293 hasInstanceDetails: function(hostInstance
, pluginName
, pluginInstance
) {
294 var def
= graphdefs
[pluginName
];
296 if (!def
|| typeof(def
.rrdargs
) != 'function')
299 var optlist
= this._forcelol(def
.rrdargs(this, hostInstance
, pluginName
, pluginInstance
, null, false));
301 for (var i
= 0; i
< optlist
.length
; i
++)
302 if (optlist
[i
].detail
)
308 _mkpath: function(host
, plugin
, plugin_instance
, dtype
, data_instance
) {
309 var path
= host
+ '/' + plugin
;
311 if (plugin_instance
!= null && plugin_instance
!= '')
312 path
+= '-' + plugin_instance
;
316 if (data_instance
!= null && data_instance
!= '')
317 path
+= '-' + data_instance
;
322 mkrrdpath: function(/* ... */) {
323 return '%s/%s.rrd'.format(
325 this._mkpath
.apply(this, arguments
)
326 ).replace(/[\\:]/g, '\\$&');
329 _forcelol: function(list
) {
330 return L
.isObject(list
[0]) ? list
: [ list
];
333 _rrdtool: function(def
, rrd
, timespan
, width
, height
, cache
) {
335 'graph', '-', '-a', 'PNG',
336 '-s', 'NOW-%s'.format(timespan
|| this.opts
.timespan
),
338 '-w', width
|| this.opts
.width
,
339 '-h', height
|| this.opts
.height
342 for (var i
= 0; i
< def
.length
; i
++) {
343 var opt
= String(def
[i
]);
346 opt
= opt
.replace(/\{file\}/g, rrd
);
351 if (L
.isObject(cache
)) {
352 var key
= sfh(cmdline
.join('\0'));
354 if (!cache
.hasOwnProperty(key
))
355 cache
[key
] = fs
.exec_direct('/usr/bin/rrdtool', cmdline
, 'blob', true);
360 return fs
.exec_direct('/usr/bin/rrdtool', cmdline
, 'blob', true);
363 _generic: function(opts
, host
, plugin
, plugin_instance
, dtype
, index
) {
373 /* use the plugin+instance+type as seed for the prng to ensure the
374 same pseudo-random color sequence for each render */
375 random
.seed(sfh([plugin
, plugin_instance
|| '', dtype
|| ''].join('.')));
377 function __def(source
) {
378 var inst
= source
.sname
,
380 ds
= source
.ds
|| 'value';
383 'DEF:%s_avg_raw=%s:%s:AVERAGE'.format(inst
, rrd
, ds
),
384 'CDEF:%s_avg=%s_avg_raw,%s'.format(inst
, inst
, source
.transform_rpn
)
387 if (!gopts
.rrasingle
)
389 'DEF:%s_min_raw=%s:%s:MIN'.format(inst
, rrd
, ds
),
390 'CDEF:%s_min=%s_min_raw,%s'.format(inst
, inst
, source
.transform_rpn
),
391 'DEF:%s_max_raw=%s:%s:MAX'.format(inst
, rrd
, ds
),
392 'CDEF:%s_max=%s_max_raw,%s'.format(inst
, inst
, source
.transform_rpn
)
396 'CDEF:%s_nnl=%s_avg,UN,0,%s_avg,IF'.format(inst
, inst
, inst
)
400 function __cdef(source
) {
404 prev
= _stack_neg
[_stack_neg
.length
- 1];
406 prev
= _stack_pos
[_stack_pos
.length
- 1];
408 /* is first source in stack or overlay source: source_stk = source_nnl */
409 if (prev
== null || source
.overlay
) {
410 /* create cdef statement for cumulative stack (no NaNs) and also
411 for display (preserving NaN where no points should be displayed) */
412 if (gopts
.rrasingle
|| !gopts
.rramax
)
414 'CDEF:%s_stk=%s_nnl'.format(source
.sname
, source
.sname
),
415 'CDEF:%s_plot=%s_avg'.format(source
.sname
, source
.sname
)
419 'CDEF:%s_stk=%s_nnl'.format(source
.sname
, source
.sname
),
420 'CDEF:%s_plot=%s_max'.format(source
.sname
, source
.sname
)
423 /* is subsequent source without overlay: source_stk = source_nnl + previous_stk */
425 /* create cdef statement */
426 if (gopts
.rrasingle
|| !gopts
.rramax
)
428 'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source
.sname
, source
.sname
, prev
),
429 'CDEF:%s_plot=%s_avg,%s_stk,+'.format(source
.sname
, source
.sname
, prev
)
433 'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source
.sname
, source
.sname
, prev
),
434 'CDEF:%s_plot=%s_max,%s_stk,+'.format(source
.sname
, source
.sname
, prev
)
438 /* create multiply by minus one cdef if flip is enabled */
440 _args
.push('CDEF:%s_neg=%s_plot,-1,*'.format(source
.sname
, source
.sname
));
442 /* push to negative stack if overlay is disabled */
444 _stack_neg
.push(source
.sname
);
447 /* no flipping, push to positive stack if overlay is disabled */
448 else if (!source
.overlay
) {
449 /* push to positive stack */
450 _stack_pos
.push(source
.sname
);
453 /* calculate total amount of data if requested */
456 'CDEF:%s_avg_sample=%s_avg,UN,0,%s_avg,IF,sample_len,*'.format(source
.sname
, source
.sname
, source
.sname
),
457 'CDEF:%s_avg_sum=PREV,UN,0,PREV,IF,%s_avg_sample,+'.format(source
.sname
, source
.sname
, source
.sname
)
461 /* local helper: create cdefs required for calculating total values */
462 function __cdef_totals() {
465 'CDEF:mytime=%s_avg,TIME,TIME,IF'.format(_sources
[0].sname
),
466 'CDEF:sample_len_raw=mytime,PREV(mytime),-',
467 'CDEF:sample_len=sample_len_raw,UN,0,sample_len_raw,IF'
471 /* local helper: create line and area statements */
472 function __line(source
) {
473 var line_color
, area_color
, legend
, variable
;
475 /* find colors: try source, then opts.colors; fall back to random color */
476 if (typeof(source
.color
) == 'string') {
477 line_color
= source
.color
;
478 area_color
= colors
.fromString(line_color
);
480 else if (typeof(opts
.colors
[source
.name
.replace(/\W/g, '_')]) == 'string') {
481 line_color
= opts
.colors
[source
.name
.replace(/\W/g, '_')];
482 area_color
= colors
.fromString(line_color
);
485 area_color
= colors
.random();
486 line_color
= colors
.asString(area_color
);
489 /* derive area background color from line color */
490 area_color
= colors
.asString(colors
.faded(area_color
));
492 /* choose source_plot or source_neg variable depending on flip state */
493 variable
= source
.flip
? 'neg' : 'plot';
496 legend
= '%%-%us'.format(_longest_name
).format(source
.title
);
498 /* create area is not disabled */
500 _args
.push('AREA:%s_%s#%s'.format(source
.sname
, variable
, area_color
));
502 /* create line statement */
503 _args
.push('LINE%d:%s_%s#%s:%s'.format(
504 source
.width
|| (source
.noarea
? 2 : 1),
505 source
.sname
, variable
, line_color
, legend
509 /* local helper: create gprint statements */
510 function __gprint(source
) {
511 var numfmt
= opts
.number_format
|| '%6.1lf',
512 totfmt
= opts
.totals_format
|| '%5.1lf%s';
514 /* don't include MIN if rrasingle is enabled */
515 if (!gopts
.rrasingle
)
516 _args
.push('GPRINT:%s_min:MIN:\tMin\\: %s'.format(source
.sname
, numfmt
));
518 /* don't include AVERAGE if noavg option is set */
520 _args
.push('GPRINT:%s_avg:AVERAGE:\tAvg\\: %s'.format(source
.sname
, numfmt
));
522 /* don't include MAX if rrasingle is enabled */
523 if (!gopts
.rrasingle
)
524 _args
.push('GPRINT:%s_max:MAX:\tMax\\: %s'.format(source
.sname
, numfmt
));
526 /* include total count if requested else include LAST */
528 _args
.push('GPRINT:%s_avg_sum:LAST:(ca. %s Total)\\l'.format(source
.sname
, totfmt
));
530 _args
.push('GPRINT:%s_avg:LAST:\tLast\\: %s\\l'.format(source
.sname
, numfmt
));
534 * find all data sources
537 /* find data types */
538 var data_types
= dtype
? [ dtype
] : (opts
.data
.types
|| []);
540 if (!(dtype
|| opts
.data
.types
)) {
541 if (L
.isObject(opts
.data
.instances
))
542 data_types
.push
.apply(data_types
, Object
.keys(opts
.data
.instances
));
543 else if (L
.isObject(opts
.data
.sources
))
544 data_types
.push
.apply(data_types
, Object
.keys(opts
.data
.sources
));
548 /* iterate over data types */
549 for (var i
= 0; i
< data_types
.length
; i
++) {
553 if (!opts
.per_instance
) {
554 if (L
.isObject(opts
.data
.instances
) && Array
.isArray(opts
.data
.instances
[data_types
[i
]]))
555 data_instances
= opts
.data
.instances
[data_types
[i
]];
557 data_instances
= this.dataInstances(host
, plugin
, plugin_instance
, data_types
[i
]);
560 if (!Array
.isArray(data_instances
) || data_instances
.length
== 0)
561 data_instances
= [ '' ];
563 /* iterate over data instances */
564 for (var j
= 0; j
< data_instances
.length
; j
++) {
565 /* construct combined data type / instance name */
566 var dname
= data_types
[i
];
568 if (data_instances
[j
].length
)
569 dname
+= '_' + data_instances
[j
];
572 var data_sources
= [ 'value' ];
574 if (L
.isObject(opts
.data
.sources
)) {
575 if (Array
.isArray(opts
.data
.sources
[dname
]))
576 data_sources
= opts
.data
.sources
[dname
];
577 else if (Array
.isArray(opts
.data
.sources
[data_types
[i
]]))
578 data_sources
= opts
.data
.sources
[data_types
[i
]];
581 /* iterate over data sources */
582 for (var k
= 0; k
< data_sources
.length
; k
++) {
583 var dsname
= data_types
[i
] + '_' + data_instances
[j
].replace(/\W/g, '_') + '_' + data_sources
[k
],
584 altname
= data_types
[i
] + '__' + data_sources
[k
];
586 /* find datasource options */
589 if (L
.isObject(opts
.data
.options
)) {
590 if (L
.isObject(opts
.data
.options
[dsname
]))
591 dopts
= opts
.data
.options
[dsname
];
592 else if (L
.isObject(opts
.data
.options
[altname
]))
593 dopts
= opts
.data
.options
[altname
];
594 else if (L
.isObject(opts
.data
.options
[dname
]))
595 dopts
= opts
.data
.options
[dname
];
596 else if (L
.isObject(opts
.data
.options
[data_types
[i
]]))
597 dopts
= opts
.data
.options
[data_types
[i
]];
602 rrd
: dopts
.rrd
|| this.mkrrdpath(host
, plugin
, plugin_instance
, data_types
[i
], data_instances
[j
]),
603 color
: dopts
.color
|| colors
.asString(colors
.random()),
604 flip
: dopts
.flip
|| false,
605 total
: dopts
.total
|| false,
606 overlay
: dopts
.overlay
|| false,
607 transform_rpn
: dopts
.transform_rpn
|| '0,+',
608 noarea
: dopts
.noarea
|| false,
609 noavg
: dopts
.noavg
|| false,
610 title
: dopts
.title
|| null,
611 weight
: dopts
.weight
|| (dopts
.negweight
? -+data_instances
[j
] : null) || (dopts
.posweight
? +data_instances
[j
] : null) || null,
614 instance
: data_instances
[j
],
615 index
: _sources
.length
+ 1,
616 sname
: String(_sources
.length
+ 1) + data_types
[i
]
619 _sources
.push(source
);
621 /* generate datasource title */
622 source
.title
= i18n
.ds(host
, source
);
624 /* find longest name */
625 _longest_name
= Math
.max(_longest_name
, source
.title
.length
);
638 /* if per_instance is enabled then find all instances from the first datasource in diagram */
639 /* if per_instance is disabled then use an empty pseudo instance and use model provided values */
640 var instances
= [ '' ];
642 if (opts
.per_instance
)
643 instances
= this.dataInstances(host
, plugin
, plugin_instance
, _sources
[0].type
);
645 /* iterate over instances */
646 for (var i
= 0; i
< instances
.length
; i
++) {
647 /* store title and vlabel */
649 '-t', i18n
.title(host
, plugin
, plugin_instance
, _sources
[0].type
, instances
[i
], opts
.title
),
650 '-v', i18n
.label(host
, plugin
, plugin_instance
, _sources
[0].type
, instances
[i
], opts
.vlabel
)
654 _args
.push('-u', String(opts
.y_max
));
657 _args
.push('-l', String(opts
.y_min
));
659 if (opts
.units_exponent
)
660 _args
.push('-X', String(opts
.units_exponent
));
662 if (opts
.alt_autoscale
)
665 if (opts
.alt_autoscale_max
)
668 /* store additional rrd options */
669 if (Array
.isArray(opts
.rrdopts
))
670 for (var j
= 0; j
< opts
.rrdopts
.length
; j
++)
671 _args
.push(String(opts
.rrdopts
[j
]));
674 _sources
.sort(function(a
, b
) {
675 var x
= a
.weight
|| a
.index
|| 0,
676 y
= b
.weight
|| b
.index
|| 0;
681 /* define colors in order */
683 for (var j
= 0; j
< _sources
.length
; j
++)
684 _sources
[j
].color
= colors
.defined(j
);
686 /* create DEF statements for each instance */
687 for (var j
= 0; j
< _sources
.length
; j
++) {
688 /* fixup properties for per instance mode... */
689 if (opts
.per_instance
) {
690 _sources
[j
].instance
= instances
[i
];
691 _sources
[j
].rrd
= this.mkrrdpath(host
, plugin
, plugin_instance
, _sources
[j
].type
, instances
[i
]);
697 /* create CDEF required for calculating totals */
700 /* create CDEF statements for each instance in reversed order */
701 for (var j
= _sources
.length
- 1; j
>= 0; j
--)
704 /* create LINE1, AREA and GPRINT statements for each instance */
705 for (var j
= 0; j
< _sources
.length
; j
++) {
707 __gprint(_sources
[j
]);
710 /* push arg stack to definition list */
722 render: function(plugin
, plugin_instance
, is_index
, hostname
, timespan
, width
, height
, cache
) {
725 /* check for a whole graph handler */
726 var def
= graphdefs
[plugin
];
728 if (def
&& typeof(def
.rrdargs
) == 'function') {
729 /* temporary image matrix */
732 /* get diagram definitions */
733 var optlist
= this._forcelol(def
.rrdargs(this, hostname
, plugin
, plugin_instance
, null, is_index
));
734 for (var i
= 0; i
< optlist
.length
; i
++) {
735 var opt
= optlist
[i
];
736 if (!is_index
|| !opt
.detail
) {
739 /* get diagram definition instances */
740 var diagrams
= this._generic(opt
, hostname
, plugin
, plugin_instance
, null, i
);
742 /* render all diagrams */
743 for (var j
= 0; j
< diagrams
.length
; j
++) {
745 _images
[i
][j
] = this._rrdtool(diagrams
[j
], null, timespan
, width
, height
, cache
);
750 /* remember images - XXX: fixme (will cause probs with asymmetric data) */
751 for (var y
= 0; y
< _images
[0].length
; y
++)
752 for (var x
= 0; x
< _images
.length
; x
++)
753 pngs
.push(_images
[x
][y
]);
756 return Promise
.all(pngs
);