Reporting / traffic: add "top talkers" tab which shows the current (and max) bandwith usage per ip address sorted by total (in+out).

some people seem to miss the numbers, since the top host graph uses the same information over time, it wouldn't hurt to add a tab containting the raw numbers for the selected interfaces.
The current version doesn't limit the number of results, we might need to add a limit at some point in time, depending on how many results iftop samples.

general remarks:

o total amount of traffic is not being displayed, although we could count the "cumulative" from iftop, the numbers would always be flawed (since sampled with an interval)
o no reverse lookups, maybe for a future version, let's first see if this helps.
o hosts not seen for 120 seconds will automatically be removed from the list (fixed ttl)
This commit is contained in:
Ad Schellevis 2020-12-25 15:14:13 +01:00
parent b4104cf0a3
commit 4186f2bbe5
3 changed files with 132 additions and 46 deletions

View File

@ -71,9 +71,11 @@ class TrafficController extends ApiControllerBase
if (count($iflist) > 0) {
$data = (new Backend())->configdpRun('interface show top', [implode(",", $iflist)]);
$data = json_decode($data, true);
foreach ($data as $if => $content) {
if (isset($ifmap[$if])) {
$response[$ifmap[$if]] = $content;
if (is_array($data)) {
foreach ($data as $if => $content) {
if (isset($ifmap[$if])) {
$response[$ifmap[$if]] = $content;
}
}
}
}

View File

@ -227,6 +227,59 @@ POSSIBILITY OF SUCH DAMAGE.
return new Chart(ctx, config);
}
/**
* iftop (top talkers) update
*/
function updateTopTable(data) {
let target = $("#rxTopTable > tbody");
let update_stamp = Math.trunc(Date.now() / 1000.0);
Object.keys(data).forEach(function(intf) {
let intf_label = $("#interfaces > option[value="+intf+"]").data('content');
['in', 'out'].forEach(function(dir) {
for (var i=0; i < data[intf][dir].length ; i++) {
let item = data[intf][dir][i];
let tr = target.find("tr[data-address='"+item.address+"']");
if (tr.length === 0) {
tr = $("<tr data-bps_in='0' data-bps_out='0' data-bps_max_in='0' data-bps_max_out='0'/>");
tr.attr("data-address", item.address);
tr.append($("<td/>").html(intf_label));
tr.append($("<td/>").text(item.address));
tr.append($("<td class='bps_in'/>").text("0b"));
tr.append($("<td class='bps_out'/>").text("0b"));
tr.append($("<td class='bps_max_in'/>").text("0b"));
tr.append($("<td class='bps_max_out'/>").text("0b"));
target.append(tr);
}
tr.attr('data-bps_'+dir, item.rate_bits);
tr.attr('data-last_seen', update_stamp);
if (parseInt(tr.data('bps_max_'+dir)) < item.rate_bits) {
tr.attr('data-bps_max_'+dir, item.rate_bits);
tr.find('td.bps_max_'+dir).text(item.rate);
}
tr.find('td.bps_'+dir).text(item.rate);
}
});
});
let ttl = 120; // keep visible for ttl seconds
target.find('tr').each(function(){
if (parseInt($(this).data('last_seen')) < (update_stamp - ttl)) {
$(this).remove();
} else if (parseInt($(this).data('last_seen')) != update_stamp) {
// reset measurements not in this set
$(this).attr('data-bps_in', 0);
$(this).attr('data-bps_out', 0);
$(this).find('td.bps_in').text("0b");
$(this).find('td.bps_out').text("0b");
}
});
// sort by current top consumer
target.find('tr').sort(function(a, b) {
let a_total = +$(a).data('bps_in') + $(a).data('bps_out');
let b_total = +$(b).data('bps_in') + $(b).data('bps_out');
return b_total - a_total;
}).appendTo(target);
}
/**
* startup, fetch initial interface stats and create graphs
*/
@ -334,6 +387,7 @@ POSSIBILITY OF SUCH DAMAGE.
if (status == 'success') {
$( document ).trigger( "updateTrafficTopCharts", [ data ] );
top_traffic_poller();
updateTopTable(data);
} else {
setTimeout(top_traffic_poller, 2000);
}
@ -346,7 +400,6 @@ POSSIBILITY OF SUCH DAMAGE.
window.localStorage.setItem("api.diagnostics.traffic.interface", $(this).val());
}
});
});
@ -357,46 +410,67 @@ POSSIBILITY OF SUCH DAMAGE.
}
</style>
<div class="content-box">
<div class="content-box-main">
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" id="graph_tab" href="#graph">{{ lang._('Graph') }}</a></li>
<li><a data-toggle="tab" id="gtid_tab" href="#toptalkers">{{ lang._('Top talkers') }}</a></li>
<div class="pull-right">
<select class="selectpicker" id="interfaces" multiple=multiple>
</select>
&nbsp;
</div>
</ul>
<div class="tab-content content-box">
<div id="graph" class="tab-pane fade in active">
<div class="table-responsive">
<div class="row">
<div class="col-sm-12">
<div class="pull-right">
<select class="selectpicker" id="interfaces" multiple=multiple>
</select>
&nbsp;
</div>
</div>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="rxChart" data-src_field="bytes received"></canvas>
</div>
</div>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="txChart" data-src_field="bytes transmitted"></canvas>
</div>
</div>
<div class="row">
<div class="col-sm-12">
</div>
<div class="row">
<div class="col-xs-12">
<hr/>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="rxChart" data-src_field="bytes received"></canvas>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="rxTopChart" data-src_field="in"></canvas>
</div>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="txChart" data-src_field="bytes transmitted"></canvas>
</div>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="txTopChart" data-src_field="out"></canvas>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="rxTopChart" data-src_field="in"></canvas>
</div>
</div>
<div class="col-xs-12 col-lg-6">
<div class="chart-container">
<canvas id="txTopChart" data-src_field="out"></canvas>
</div>
</div>
</div>
</div>
</div>
<div id="toptalkers" class="tab-pane fade in">
<div class="col-xs-12 col-lg-6">
<table class="table table-condensed" id="rxTopTable">
<thead>
<tr>
<th></th>
<th>{{ lang._('Address') }}</th>
<th>{{ lang._('In (bps)') }}</th>
<th>{{ lang._('Out (bps)') }}</th>
<th>{{ lang._('In max(bps)') }}</th>
<th>{{ lang._('Out max(bps)') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -61,6 +61,18 @@ def local_addresses():
result.append(ip)
return result
def convert_bformat(value):
value = value.lower()
if value.endswith('kb'):
return decimal.Decimal(value[:-2]) * 1000
elif value.endswith('mb'):
return decimal.Decimal(value[:-2]) * 1000000
elif value.endswith('gb'):
return decimal.Decimal(value[:-2]) * 1000000000
elif value.endswith('b') and value[:-1].isdigit():
return decimal.Decimal(value[:-1])
return 0
if __name__ == '__main__':
result = dict()
parser = argparse.ArgumentParser()
@ -86,16 +98,14 @@ if __name__ == '__main__':
parts = line.split()
if parts[0].find('.') == -1 and parts[0].find(':') == -1:
parts.pop(0)
rate_bits = 0
if parts[2].endswith('Kb'):
rate_bits = decimal.Decimal(parts[2][:-2]) * 1000
elif parts[2].endswith('Mb'):
rate_bits = decimal.Decimal(parts[2][:-2]) * 1000000
elif parts[2].endswith('Gb'):
rate_bits = decimal.Decimal(parts[2][:-2]) * 1000000000
elif parts[2].endswith('b') and parts[2][:-1].isdigit():
rate_bits = decimal.Decimal(parts[2][:-1])
item = {'address': parts[0], 'rate': parts[2], 'rate_bits': rate_bits, 'tags': []}
item = {
'address': parts[0],
'rate': parts[2],
'rate_bits': int(convert_bformat(parts[2])),
'cumulative': parts[5],
'cumulative_bytes': int(convert_bformat(parts[5])),
'tags': []
}
# attach tags (type of address)
try:
ip = IPAddress(parts[0])