From 1ce7ac32dbc5c096fccb81f2e3427d2d56100bc0 Mon Sep 17 00:00:00 2001 From: Stephan de Wit Date: Thu, 5 Sep 2024 12:29:40 +0200 Subject: [PATCH] filter: live log: switch from poll-based to stream-based --- .../Diagnostics/Api/FirewallController.php | 6 +- .../views/OPNsense/Diagnostics/fw_log.volt | 447 +++++++++++------- src/opnsense/scripts/filter/read_log.py | 4 +- .../conf/actions.d/actions_filter.conf | 2 +- 4 files changed, 278 insertions(+), 181 deletions(-) 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('