diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
index d7ae7d6657..3e53281961 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
@@ -59,16 +59,16 @@ class FirewallController extends ApiControllerBase
}
}
- public function streamLogAction()
+ public function streamLogAction($lines = 5)
{
return $this->configdStream(
'filter stream log',
- [],
+ [$lines],
[
'Content-Type: text/event-stream',
'Cache-Control: no-cache'
],
- 60
+ /* this stream implements a keepalive, the default poll_timeout of 2 should suffice */
);
}
diff --git a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_log.volt b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_log.volt
index c95e68bdff..4e5fa051d6 100644
--- a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_log.volt
+++ b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_log.volt
@@ -39,19 +39,20 @@
};
var interface_descriptions = {};
let hostnameMap = {};
+ let stream = null;
/**
* reverse lookup address fields (replace address part for hostname if found)
*/
function reverse_lookup() {
let to_fetch = [];
- let unfinshed_lookup = false;
+ let unfinished_lookup = false;
$(".address").each(function(){
let address = $(this).data('address');
if (!hostnameMap.hasOwnProperty(address) && !to_fetch.includes(address)) {
// limit dns fetches to 50 per cycle
if (to_fetch.length >= 50) {
- unfinshed_lookup = true;
+ unfinished_lookup = true;
return;
}
to_fetch.push(address);
@@ -77,7 +78,7 @@
hostnameMap[address] = data[address];
}
});
- if (unfinshed_lookup) {
+ if (unfinished_lookup) {
// schedule next fetch
reverse_lookup();
}
@@ -223,12 +224,100 @@
return t_fetched;
}
-
- function fetch_log() {
- let record_spec = [];
+ function fetch_initial_data(digest='')
+ {
// Overfetch for limited display scope to increase the chance of being able to find matches.
- // As we fetch once per second, we would be limited to 25 records/sec of log data when 25 is selected.
let max_rows = Math.max(1000, parseInt($("#limit").val()));
+ ajaxGet('/api/diagnostics/firewall/log/', {'digest': digest, 'limit': max_rows}, function(data, status) {
+ if (status == 'error') {
+ return;
+ } else if (data !== undefined && data.length > 0) {
+ let rows = parse_records(data);
+ if (rows !== false && rows.length > 0) {
+ insert_log_rows(rows);
+ }
+ }
+ });
+ }
+
+ function debounce_queue_limited(fn, delay, limit)
+ {
+ let timeout;
+ let eventQueue = [];
+
+ const processQueue = () => {
+ if (eventQueue.length > 0) {
+ fn(eventQueue);
+ eventQueue = [];
+ }
+ };
+
+ return function(event) {
+ if (!event) {
+ fn(false);
+ return;
+ }
+ eventQueue.push(event);
+
+ if (eventQueue.length >= limit) {
+ clearTimeout(timeout);
+ processQueue();
+ }
+
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ processQueue();
+ }, delay);
+ };
+ }
+
+ function open_stream()
+ {
+ if (stream === null) {
+ stream = new EventSource('/api/diagnostics/firewall/streamLog/0');
+ // Events may come in very quickly, therefore debounce to update the UI in batches
+ stream.onmessage = debounce_queue_limited(function (events) {
+ if (!events) {
+ close_stream();
+ return;
+ }
+
+ let records = [];
+ events.forEach(event => {
+ records.push(JSON.parse(event.data));
+ })
+ let rows = parse_records(records);
+ if (!rows || rows.length == 0) {
+ return;
+ }
+
+ insert_log_rows(rows);
+ }, 200, 50);
+ stream.onerror = function(event) {
+ close_stream(true);
+ };
+ }
+ }
+
+ function close_stream(error = false)
+ {
+ if (stream !== null) {
+ stream.close();
+ stream = null;
+
+ if (error) {
+ // XXX inform user or retry?
+ $("#auto_refresh").prop('checked', false);
+ }
+ }
+ }
+
+ function parse_records(records) {
+ if (records === undefined || records.length == 0) {
+ return false;
+ }
+
+ let record_spec = [];
// read heading, contains field specs
$("#grid-log > thead > tr > th ").each(function () {
record_spec.push({
@@ -237,173 +326,172 @@
'class': $(this).attr('class')
});
});
- // read last digest (record hash) from top data row
- var last_digest = $("#grid-log > tbody > tr:first > td:first").text();
- // fetch new log lines and add on top of grid-log
- ajaxGet('/api/diagnostics/firewall/log/', {'digest': last_digest, 'limit': max_rows}, function(data, status) {
- if (status == 'error') {
- // stop poller on failure
- $("#auto_refresh").prop('checked', false);
- } else if (last_digest != '' && $("#grid-log > tbody > tr").length == 1){
- // $("#limit").change(); called, this request should be discarted (data grid reset)
- return;
- } else if (data !== undefined && data.length > 0) {
- let record;
- let trs = [];
- while ((record = data.pop()) != null) {
- if (record['__digest__'] != last_digest) {
- var log_tr = $("
");
- if (record.interface !== undefined && interface_descriptions[record.interface] !== undefined) {
- record['interface_name'] = interface_descriptions[record.interface];
- } else {
- record['interface_name'] = record.interface;
+
+ let record;
+ let trs = [];
+ while ((record = records.pop()) != null) {
+ var log_tr = $("
");
+ if (record.interface !== undefined && interface_descriptions[record.interface] !== undefined) {
+ record['interface_name'] = interface_descriptions[record.interface];
+ } else {
+ record['interface_name'] = record.interface;
+ }
+ log_tr.data('details', record);
+ log_tr.hide();
+ $.each(record_spec, function(idx, field){
+ var log_td = $('| ').addClass(field['class']);
+ var column_name = field['column-id'];
+ var content = null;
+ switch (field['type']) {
+ case 'icon':
+ var icon = field_type_icons[record[column_name]];
+ if (icon != undefined) {
+ log_td.html(''+record[column_name]+'');
}
- log_tr.data('details', record);
- log_tr.hide();
- $.each(record_spec, function(idx, field){
- var log_td = $(' | ').addClass(field['class']);
- var column_name = field['column-id'];
- var content = null;
- switch (field['type']) {
- case 'icon':
- var icon = field_type_icons[record[column_name]];
- if (icon != undefined) {
- log_td.html(''+record[column_name]+'');
- }
- break;
- case 'address':
- log_td.text(record[column_name]);
- log_td.addClass('address');
- log_td.data('address', record[column_name]);
- if (record[column_name+'port'] !== undefined) {
- if (record['ipversion'] == 6) {
- log_td.text('['+log_td.text()+']:'+record[column_name+'port']);
- } else {
- log_td.text(log_td.text()+':'+record[column_name+'port']);
- }
- }
- break;
- case 'info':
- log_td.html(' |