diff --git a/www/nginx/Makefile b/www/nginx/Makefile index 1fa6c45f0..290dec87f 100644 --- a/www/nginx/Makefile +++ b/www/nginx/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= nginx -PLUGIN_VERSION= 1.24 +PLUGIN_VERSION= 1.25 PLUGIN_COMMENT= Nginx HTTP server and reverse proxy PLUGIN_DEPENDS= nginx PLUGIN_MAINTAINER= franz.fabian.94@gmail.com diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index 3afedea29..165ae90fc 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -10,6 +10,10 @@ WWW: https://nginx.org/ Plugin Changelog ================ +1.25 + +* Reworked logging frontent to support filtering and historic view (contributed by Manuel Faux) + 1.24 * Change all Listen Port directives to Listen Address and migrate the Port data to Addresses diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php index 765c1be82..c340ba5fb 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -40,18 +41,26 @@ class LogsController extends ApiControllerBase * "/" -> list of access logs * "/uuid" -> conent of access log * @param null|string $uuid log uuid of the HTTP server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function accessesAction($uuid = null) + public function accessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /accesses delivers a list of servers with access logs return $this->list_vhosts(); - } else { + } + elseif (!isset($fileno)) { + return $this->list_logfiles('access', $uuid); + } + else { // emulate REST call for a specific log /accesses/uuid - $this->call_configd('access', $uuid); + $this->get_logs('access', $uuid, $fileno, $page, $perPage, $query); } } @@ -68,18 +77,26 @@ class LogsController extends ApiControllerBase * "/" -> list of error logs * "/uuid" -> conent of error log * @param null|string $uuid uuid of the HTTP server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function errorsAction($uuid = null) + public function errorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /errors delivers a list of servers with error logs return $this->list_vhosts(); - } else { + } + elseif (!isset($fileno)) { + return $this->list_logfiles('error', $uuid); + } + else { // emulate REST call for a specific log /errors/uuid - $this->call_configd('error', $uuid); + $this->get_logs('error', $uuid, $fileno, $page, $perPage, $query); } } @@ -87,18 +104,26 @@ class LogsController extends ApiControllerBase * "/" -> list of access logs * "/uuid" -> conent of access log * @param null|string $uuid log uuid of the stream server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function streamAccessesAction($uuid = null) + public function streamaccessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /stream_accesses delivers a list of servers with access logs return $this->list_streams(); - } else { + } + elseif (!isset($fileno)) { + return $this->list_stream_logfiles('streamaccess', $uuid); + } + else { // emulate REST call for a specific log /stream_accesses/uuid - $this->call_configd_stream('streamaccess', $uuid); + $this->get_stream_logs('streamaccess', $uuid, $fileno, $page, $perPage, $query); } } @@ -106,49 +131,112 @@ class LogsController extends ApiControllerBase * "/" -> list of access logs * "/uuid" -> conent of error log * @param null $uuid uuid of the stream server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function streamErrorsAction($uuid = null) + public function streamerrorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /stream_errors delivers a list of servers with error logs return $this->list_streams(); - } else { + } + elseif (!isset($fileno)) { + return $this->list_stream_logfiles('streamerror', $uuid); + } + else { // emulate REST call for a specific log /stream_errors/uuid - $this->call_configd_stream('streamerror', $uuid); + $this->get_stream_logs('streamerror', $uuid, $fileno, $page, $perPage, $query); } } /** + * Retrieve log content for HTTP server. + * * @param $type string access or error for the used log type * @param $uuid string uuid of the server + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return |null * @throws \Exception ? */ - private function call_configd($type, $uuid) + private function get_logs($type, $uuid, $fileno, $page, $perPage, $query) { if (!($this->vhost_exists($uuid) || $uuid == 'global')) { - $this->response->setStatusCode(404, "Not Found"); + return $this->response->setStatusCode(404, "Not Found"); } - return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid); + $page = intval($page); + $perPage = intval($perPage); + $query = base64_encode(urldecode($query)); + + return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query"); } + /** + * Retrieve available log files for specific HTTP server uuid. + * * @param $type string access or error for the used log type * @param $uuid string uuid of the server * @return |null * @throws \Exception ? */ - private function call_configd_stream($type, $uuid) + private function list_logfiles($type, $uuid) { - if (!$this->stream_exists($uuid)) { - $this->response->setStatusCode(404, "Not Found"); + if (!($this->vhost_exists($uuid) || $uuid == 'global')) { + return $this->response->setStatusCode(404, "Not Found"); } - return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid); + return $this->sendConfigdToClient("nginx listlogs $type $uuid"); + } + + /** + * Retrieve log content for stream server. + * + * @param $type string access or error for the used log type + * @param $uuid string uuid of the server + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply + * @return |null + * @throws \Exception ? + */ + private function get_stream_logs($type, $uuid, $fileno, $page, $perPage, $query) + { + if (!$this->stream_exists($uuid)) { + return $this->response->setStatusCode(404, "Not Found"); + } + + $page = intval($page); + $perPage = intval($perPage); + $query = base64_encode(urldecode($query)); + + return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query"); + } + + /** + * Retrieve available log files for specific stream server uuid. + * + * @param $type string access or error for the used log type + * @param $uuid string uuid of the server + * @return |null + * @throws \Exception ? + */ + private function list_stream_logfiles($type, $uuid) + { + if (!$this->stream_exists($uuid)) { + return $this->response->setStatusCode(404, "Not Found"); + } + + return $this->sendConfigdToClient("nginx listlogs $type $uuid"); } /** diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index e80537625..410e04989 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -71,14 +71,6 @@ class IndexController extends \OPNsense\Base\IndexController $this->view->pick('OPNsense/Nginx/index'); } - /** - * show the nginx logs page /ui/nginx/index/logs - */ - public function logsAction() - { - $this->view->pick('OPNsense/Nginx/logs'); - } - /** * show the nginx TLS handshakes page /ui/nginx/index/tls_handshakes */ diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php new file mode 100644 index 000000000..fec65f18d --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php @@ -0,0 +1,87 @@ +view->log = 'global'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/accesses + */ + public function accessesAction() + { + $this->view->log = 'accesses'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/errors + */ + public function errorsAction() + { + $this->view->log = 'errors'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/stream_accesses + */ + public function streamaccessesAction() + { + $this->view->log = 'stream_accesses'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/stream_errors + */ + public function streamerrorsAction() + { + $this->view->log = 'stream_errors'; + $this->view->pick('OPNsense/Nginx/logs'); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php index 5f6ed187c..f2d94de63 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,32 +30,11 @@ namespace OPNsense\Nginx; -class AccessLogParser +class AccessLogParser extends LogParserBase { - private $file_name; - private $result; - private const LogLineRegex = '/(\S+) - (\S+) \[([\d\sa-z\:\-\/\+]+)\] "([^"]+?)" (\d+) (\d+) "([^"]*?)" "([^"]*?)" "([^"]*?)"/i'; - function __construct($file_name) - { - $this->file_name = $file_name; - $this->result = array(); - $this->parse_file(); - } - - private function parse_file() - { - $handle = @fopen($this->file_name, 'r'); - if ($handle) { - while (($buffer = fgets($handle)) !== false) { - $this->result[] = $this->parse_line($buffer); - } - fclose($handle); - } - } - - private function parse_line($line) + protected function parse_line($line) { $container = new AccessLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -70,9 +50,4 @@ class AccessLogParser } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php index fd272a4d0..99336e2b6 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,21 +30,11 @@ namespace OPNsense\Nginx; -class ErrorLogParser +class ErrorLogParser extends LogParserBase { - private $file_name; - private $lines; - private $result; - private const LogLineRegex = '/(\S+) (\S+) \[([\d\sa-z\:\-\/\+\#]+)\] ([\S:]+): (.+)/i'; - function __construct($file_name) - { - $this->file_name = $file_name; - $this->lines = file($this->file_name); - $this->result = array_map([$this, 'parse_line'], $this->lines); - } - private function parse_line($line) + protected function parse_line($line) { $container = new ErrorLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -55,9 +46,4 @@ class ErrorLogParser } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php new file mode 100644 index 000000000..9c38adcfa --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php @@ -0,0 +1,123 @@ +file_name = $file_name; + $this->page = $page; + $this->per_page = $per_page; + $this->query = $query; + $this->page_count = 0; + $this->total_lines = 0; + $this->query_lines = 0; + $this->result = array(); + + $this->parse_file(); + } + + /** + * Forward read complete gz compressed logfile into memory, reverse file, + * count lines, save lines which match filter and count matching lines. + */ + private function parse_file() + { + $lines = gzfile($this->file_name); + if ($lines !== false && count($lines) > 0) { + $lines = array_reverse($lines); + + $filtering = false; + // Did we receive a non-zero filtering query? + foreach ($this->query as $key => $val) { + if (strlen($val) > 0) { + $filtering = true; + } + } + + $cnt = 0; + foreach ($lines as $line) { + $pass = true; + + $parsed_line = ''; + // Perform filtering if needed + if ($filtering) { + $parsed_line = $this->parse_line($line); + + foreach ($this->query as $key => $val) { + $val = (string)$val; + if (!empty($val) && strpos($parsed_line->{$key}, (string)$val) === false) { + $pass = false; + } + } + } + + if ($pass) { + if ($this->per_page <= 0 || floor($cnt / $this->per_page) == $this->page) { + // Only parse line if not already parsed due to filtering + if (!$filtering) { + $parsed_line = $this->parse_line($line); + } + + $this->result[] = $parsed_line; + } + $cnt++; + } + } + + $this->page_count = $this->per_page > 0 ? ceil($cnt / $this->per_page) : 1; + $this->total_lines = count($lines); + $this->query_lines = $cnt; + } + + unset($lines); + } + + abstract protected function parse_line($line); + + public function get_result() + { + return $this->result; + } +} + diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php index 37726d704..f6974d6e0 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,33 +30,11 @@ namespace OPNsense\Nginx; -class StreamAccessLogParser +class StreamAccessLogParser extends LogParserBase { - private $file_name; - private $result; - private const LogLineRegex = '/(\S+) \[([\d\sa-z\:\-\/\+]+)\] (\S+?) (\d+) (\d+) (\d+) (\d+(?:\.\d+)?)/i'; - - function __construct($file_name) - { - $this->file_name = $file_name; - $this->result = array(); - $this->parse_file(); - } - - private function parse_file() - { - $handle = @fopen($this->file_name, 'r'); - if ($handle) { - while (($buffer = fgets($handle)) !== false) { - $this->result[] = $this->parse_line($buffer); - } - fclose($handle); - } - } - - private function parse_line($line) + protected function parse_line($line) { $container = new StreamAccessLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -68,9 +47,4 @@ class StreamAccessLogParser } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml index d9f582220..bbe625bd7 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml @@ -5,7 +5,11 @@ - + + + + + diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt index 125fc5d63..b920f706b 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt @@ -25,7 +25,9 @@ # POSSIBILITY OF SUCH DAMAGE. #} -
+ + +
diff --git a/www/nginx/src/opnsense/scripts/nginx/list_logs.php b/www/nginx/src/opnsense/scripts/nginx/list_logs.php new file mode 100755 index 000000000..c3b94e9dd --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/list_logs.php @@ -0,0 +1,106 @@ +#!/usr/local/bin/php + strlen($filename)) ? substr($file, strlen($filename) + 1, -3) : -1; + $result[$number] = array( + 'filename' => substr($file, strlen($log_prefix)), + 'date' => ($number >= 0) ? date('d/M/Y', filemtime($file) - 3600) : 'current', + 'number' => $number + ); + } + + ksort($result, SORT_NUMERIC); + $result = array_values($result); + + return $result; +} + +if ($_SERVER['argc'] < 3) { + die('{"error": "Incorrect amount of parameters given"}'); +} + +// first parameter: error|access +$mode = $_SERVER['argv'][1]; +// second parameter: uuid of server +$server = $_SERVER['argv'][2]; +$nginx = new Nginx(); + +$result = []; +// special case: the global error log +if ($server == 'global') { + $result = list_logfiles('error.log'); +} +else { + switch ($mode) { + case 'error': + case 'access': + if ($data = $nginx->getNodeByReference('http_server.' . $server)) { + $server_names = (string)$data->servername; + if (empty($server_names)) { + die('{"error": "The server entry has no server name"}'); + } + + $log_file_name = basename($server_names) . '.' . $mode . $log_suffix; + $result = list_logfiles($log_file_name); + } + else { + die('{"error": "UUID not found"}'); + } + break; + case 'streamerror': + case 'streamaccess': + if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { + $mode = str_replace('stream', '', $mode); + $log_file_name = 'stream_' . $server . '.' . $mode . $log_suffix; + $result = list_logfiles($log_file_name); + } else { + die('{"error": "UUID not found"}'); + } + break; + default: + die('{"error": "action (' . $mode . ') not found"}'); + } +} + +echo json_encode($result); diff --git a/www/nginx/src/opnsense/scripts/nginx/read_log.php b/www/nginx/src/opnsense/scripts/nginx/read_log.php index 488c018ef..1ee5e4030 100755 --- a/www/nginx/src/opnsense/scripts/nginx/read_log.php +++ b/www/nginx/src/opnsense/scripts/nginx/read_log.php @@ -2,7 +2,8 @@ 0) ? max(intval($_SERVER['argv'][3]), -1) : -1; +// third parameter: current page +$page = max(intval($_SERVER['argv'][4]), 0); +// fourth parameter: lines per page +$per_page = max(intval($_SERVER['argv'][5]), 0); +// fifth parameter: filter query +$query = json_decode(base64_decode($_SERVER['argv'][6]), true); $nginx = new Nginx(); +if (!is_array($query)) { + $query = array(); +} + +if ($file_no >= 0) { + $log_suffix .= ".$file_no.gz"; +} + +$result = []; // special case: the global error log if ($server == 'global') { - $logparser = new ErrorLogParser($log_prefix . 'error.log'); - echo json_encode(empty($logparser->get_result()) ? - array('error' => 'no lines found') : - $logparser->get_result()); - exit(0); + $logparser = new ErrorLogParser($log_prefix . 'error' . $log_suffix, $page, $per_page, $query); +} +else { + switch ($mode) { + case 'error': + case 'access': + if ($data = $nginx->getNodeByReference('http_server.' . $server)) { + $server_names = (string)$data->servername; + if (empty($server_names)) { + die('{"error": "The server entry has no server name"}'); + } + $log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix; + // this entry has no log file, ignore it + if (!file_exists($log_file_name)) { + break; + } + $logparser = null; + + if ($mode == 'error') { + $logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query); + } elseif ($mode == 'access') { + $logparser = new AccessLogParser($log_file_name, $page, $per_page, $query); + } + } + else { + die('{"error": "UUID not found"}'); + } + break; + case 'streamerror': + case 'streamaccess': + if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { + $mode = str_replace('stream', '', $mode); + $log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix; + // this entry has no log file, ignore it + if (!file_exists($log_file_name)) { + die('{"error": "file not found"}'); + } + $logparser = null; + + if ($mode == 'error') { + $logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query); + } elseif ($mode == 'access') { + $logparser = new StreamAccessLogParser($log_file_name, $page, $per_page, $query); + } + } else { + die('{"error": "UUID not found"}'); + } + break; + default: + die('{"error": "action (' . $mode . ') not found"}'); + } } -switch ($mode) { - case 'error': - case 'access': - if ($data = $nginx->getNodeByReference('http_server.' . $server)) { - $server_names = (string)$data->servername; - if (empty($server_names)) { - die('{"error": "The server entry has no server name"}'); - } - $lines = []; - $log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix; - // this entry has no log file, ignore it - if (!file_exists($log_file_name)) { - break; - } - $logparser = null; - if ($mode == 'error') { - $logparser = new ErrorLogParser($log_file_name); - } elseif ($mode == 'access') { - $logparser = new AccessLogParser($log_file_name); - } - // we cannot parse the file - something went wrong - if ($logparser == null) { - break; - } - $lines = array_merge($lines, $logparser->get_result()); - if (empty($lines)) { - $lines['error'] = 'no lines found'; - } - echo json_encode($lines); - } else { - die('{"error": "UUID not found"}'); - } - break; - case 'streamerror': - case 'streamaccess': - if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { - $lines = []; - $mode = str_replace('stream', '', $mode); - $log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix; - // this entry has no log file, ignore it - if (!file_exists($log_file_name)) { - die('{"error": "file not found"}'); - } - $logparser = null; - - if ($mode == 'error') { - $logparser = new ErrorLogParser($log_file_name); - } elseif ($mode == 'access') { - $logparser = new StreamAccessLogParser($log_file_name); - } - // we cannot parse the file - something went wrong - if ($logparser == null) { - break; - } - $lines = array_merge($lines, $logparser->get_result()); - if (empty($lines)) { - $lines['error'] = 'no lines found'; - } - echo json_encode($lines); - } else { - die('{"error": "UUID not found"}'); - } - break; - default: - die('{"error": "action (' . $mode . ') not found"}'); +// we cannot parse the file - something went wrong +if ($logparser === null) { + $result['error'] = 'cannot retrieve requested logs'; } +else { + $result['lines'] = $logparser->get_result(); + $result['pages'] = $logparser->page_count; + $result['total'] = $logparser->total_lines; + $result['found'] = $logparser->query_lines; + $result['returned'] = count($result['lines']); + $result['query'] = json_encode($query); +} + +echo json_encode($result); diff --git a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf index 1cb7927e5..ed34d4971 100644 --- a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf +++ b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf @@ -28,9 +28,15 @@ type:script_output [log] command:/usr/local/opnsense/scripts/nginx/read_log.php +parameters: %s %s %s %s %s %s +type:script_output +message:querying nginx %s log for %s rotate %s (page %s of %s with filter %s) + +[listlogs] +command:/usr/local/opnsense/scripts/nginx/list_logs.php parameters: %s %s type:script_output -message:restarting nginx +message:listing nginx %s log for %s [tls_handshakes] command:cat /var/log/nginx/handshakes.json diff --git a/www/nginx/src/opnsense/www/css/nginx/logs.css b/www/nginx/src/opnsense/www/css/nginx/logs.css new file mode 100644 index 000000000..c8bf446a0 --- /dev/null +++ b/www/nginx/src/opnsense/www/css/nginx/logs.css @@ -0,0 +1,43 @@ +/* +* Copyright (C) 2020 Manuel Faux +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +* POSSIBILITY OF SUCH DAMAGE. +*/ + +thead.sticky-top th { + position: sticky; + top: 80px; + background-color: white; +} + +thead.sticky-top tr:first-child th { + top: 50px; +} + +tfoot.sticky-bottom th { + position: sticky; + bottom: 50px; + background-color: white; + font-weight: normal; + font-family: inherit; +} diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js index 8bdd4e6c7..a52439ce8 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js @@ -1 +1 @@ -!function(e){var t={};function n(l){if(t[l])return t[l].exports;var i=t[l]={i:l,l:!1,exports:{}};return e[l].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(l,i,function(t){return e[t]}.bind(null,i));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=26)}([function(e,t,n){var l=n(2),i=n(4),o=/[&<>"']/g,r=RegExp(o.source);e.exports=function(e){return(e=i(e))&&r.test(e)?e.replace(o,l):e}},function(e,t,n){var l=n(6).Symbol;e.exports=l},function(e,t,n){var l=n(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=l},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var l=n(5);e.exports=function(e){return null==e?"":l(e)}},function(e,t,n){var l=n(1),i=n(9),o=n(10),r=n(11),_=1/0,a=l?l.prototype:void 0,s=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(o(t))return i(t,e)+"";if(r(t))return s?s.call(t):"";var n=t+"";return"0"==n&&1/t==-_?"-0":n}},function(e,t,n){var l=n(7),i="object"==typeof self&&self&&self.Object===Object&&self,o=l||i||Function("return this")();e.exports=o},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,l=null==e?0:e.length,i=Array(l);++n\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_sent"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_received"))?"":__t)+'\n'+(null==(__t=model.escape("session_time"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj){let count=0;__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?(count=5,__p+="\n \n \n \n \n \n "):"accesses"===log_type?(count=9,__p+="\n \n \n \n \n \n \n \n \n \n "):(count=6,__p+="\n \n \n \n \n \n \n "),__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?__p+='\n \n \n \n \n \n ':"accesses"===log_type?__p+='\n \n \n \n \n \n \n \n \n \n ':__p+='\n \n \n \n \n \n \n ',__p+='\n \n \n \n \n \n \n \n \n \n \n \n
DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest LineTimeRemote IPStatusBytes SentBytes ReceivedSession Time
\n \n \n \n Entries per page:\n \n \n \n
\n'}return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
\n No data available...\n
\n';return __p}},,,,function(e,t,n){"use strict";n.r(t);const l=new Backbone.Collection([{name:"HTTP Access Logs",logType:"accesses"},{name:"HTTP Error Logs",logType:"errors"},{name:"Stream Access Logs",logType:"stream_accesses"},{name:"Stream Error Logs",logType:"stream_errors"}]);var i=Backbone.Model.extend({});var o=Backbone.Collection.extend({model:i,url:function(){return"/api/nginx/logs/"+this.logType},initialize:function(e){this.logType=e.logType}}),r=n(16),_=n.n(r);var a=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick"},initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),this.$el.html(""),this.$el.append(_()({model:this.collection,name:this.model.attributes.name}))},mainMenuClick:function(){this.collection.models[0]&&this.handleElementClick(this.collection.models[0].id)},menuEntryClick:function(e){this.handleElementClick(e.target.dataset.modelUuid)},handleElementClick:function(e){this.logview.get_log(this.model.get("logType"),e)}}),s=n(17),c=n.n(s);var u=Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(c()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var d=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),this.collection.forEach(e=>this.render_one(e)),this.render_single_tabs()},render_one:function(e){const t=new o({uuid:e.get("url"),logType:e.get("logType")}),n=new a({collection:t,model:e,logview:this.logview});this.$el.append(n.$el),t.fetch()},render_single_tabs:function(){const e=new u({logview:this.logview,log_name:"global",visible_name:"Global Error Log",log_type:"errors"});e.render(),this.$el.append(e.$el)}}),p=n(18),m=n.n(p),h=n(19),f=n.n(h),b=n(20),g=n.n(b),v=n(21),y=n.n(v);var x=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:x,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(){this.logType="none",this.uuid="none"},parse:function(e){return"error"in e?[]:e},filter_collection:function(e){const t=e.keys();return this.filter(function(n){if(!n)return!1;for(let l=0;lthis.render_one(e,t))}},render_one:function(e,t){const n=new T({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t){this.collection.uuid=t,this.collection.logType=e,this.type=e,this.$el.html(""),this.filter_model.clear(),this.update()},update:function(){this.collection.fetch()},clear_and_render:function(){this.current_filtered_collection=null,this.render()},update_filter:function(e){const t=e.target;this.filter_model.set(t.name,$(t).val())},page_back:function(){this.current_page>0&&(this.current_page--,this.render())},page_forward:function(){(this.current_page+1)*this.page_entry_count"']/g,o=RegExp(r.source);e.exports=function(e){return(e=l(e))&&o.test(e)?e.replace(r,i):e}},function(e,t,n){var i=n(6).Symbol;e.exports=i},function(e,t,n){var i=n(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=i},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var i=n(5);e.exports=function(e){return null==e?"":i(e)}},function(e,t,n){var i=n(1),l=n(9),r=n(10),o=n(11),a=1/0,s=i?i.prototype:void 0,_=s?s.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(r(t))return l(t,e)+"";if(o(t))return _?_.call(t):"";var n=t+"";return"0"==n&&1/t==-a?"-0":n}},function(e,t,n){var i=n(7),l="object"==typeof self&&self&&self.Object===Object&&self,r=i||l||Function("return this")();e.exports=r},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,i=null==e?0:e.length,l=Array(i);++n\r\n \r\n\r\n'+(null==(__t=_.escape(name))?"":__t)+'\r\n\r\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_sent"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_received"))?"":__t)+'\n'+(null==(__t=model.escape("session_time"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj){let count=0;__p+='\r\n \r\n \r\n ',"errors"===log_type||"stream_errors"===log_type?(count=5,__p+="\r\n \r\n \r\n \r\n \r\n \r\n "):"accesses"===log_type?(count=9,__p+="\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n "):(count=6,__p+="\r\n \r\n \r\n \r\n \r\n \r\n \r\n "),__p+='\r\n \r\n \r\n ',"errors"===log_type||"stream_errors"===log_type?__p+='\r\n \r\n \r\n \r\n \r\n \r\n ':"accesses"===log_type?__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ':__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n ',__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest LineTimeRemote IPStatusBytes SentBytes ReceivedSession Time
\r\n \r\n \r\n \r\n \r\n \r\n \r\n Page 1/0\r\n \r\n \r\n \r\n \r\n \r\n Found lines: 0/0
\r\n'}return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
\n No data available...\n
\n';return __p}},,,,function(e,t,n){"use strict";n.r(t);var i=Backbone.Model.extend({});var l=Backbone.Collection.extend({model:i,url:function(){return`/api/nginx/logs/${this.logType}`},initialize:function(e){this.logType=e.logType}}),r=Backbone.Model.extend({});var o=Backbone.Collection.extend({model:r,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(e){this.logType=e.logType,this.uuid=e.uuid}}),a=n(16),s=n.n(a);var _=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick"},initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logType=e.logType,this.logview=e.logview},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),"global"==this.model.get("id")&&(this.$el.addClass("active"),this.logview.get_log("errors","global",0)),this.$el.html(""),this.$el.append(s()({model:this.collection,id:this.model.get("id"),name:this.model.has("server_name")?this.model.get("server_name"):"Port "+this.model.get("port")}))},mainMenuClick:function(){this.collection.models[0]&&(this.handleElementClick(this.model.get("id"),this.collection.models[0].get("number")),$(`#tab_${this.model.get("id")} li`).removeClass("active"),$(`#subtab_item_${this.model.get("id")}_${this.collection.models[0].get("number")}`).parent().addClass("active"))},menuEntryClick:function(e){this.handleElementClick(e.target.dataset.modelUuid,e.target.dataset.modelFileno)},handleElementClick:function(e,t){this.logview.get_log(this.logType,e,t)}}),c=n(17),u=n.n(c);Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(u()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var p=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logview=e.logview,this.logType=e.logType},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),"global"==this.logType?this.render_global_error_tab():this.collection.forEach(e=>this.render_one_server(e))},render_one_server:function(e){const t=new o({uuid:e.get("id"),logType:this.logType}),n=new _({collection:t,model:e,logType:this.logType,logview:this.logview});this.$el.append(n.$el),t.fetch()},render_global_error_tab:function(){const e=new o({uuid:"global",logType:"errors"}),t=new _({collection:e,model:new Backbone.Model({server_name:"Global Error Log",id:"global"}),logType:"errors",logview:this.logview});this.$el.append(t.$el),e.fetch()}}),d=n(18),h=n.n(d),m=n(19),g=n.n(m),b=n(20),f=n.n(b),y=n(21),v=n.n(y);var x=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:x,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`},initialize:function(){this.logType="none",this.uuid="none",this.fileNo=-1,this.page=0,this.pageSize=0,this.filter_model=new Backbone.Model},parse:function(e){return"error"in e?[]:(this.page_count=e.pages,this.total_entries=e.total,this.displayed_entries=e.found,e.lines)},create_filter:function(){return encodeURIComponent(JSON.stringify(this.filter_model))}}),k=n(22),j=n.n(k);const T=Backbone.View.extend({tagName:"tr",initialize:function(e){this.type=e.type},render:function(){this.$el.html(this.get_template()({model:this.model}))},get_template:function(){return"accesses"===this.type?h.a:"stream_accesses"===this.type?g.a:f.a}});const C=new(Backbone.View.extend({tagName:"div",className:"content-box tab-content",events:{"keyup .filter input":"update_filter","click #paging_first":"page_first","click #paging_back":"page_back","click #refresh":"update","click #paging_forward":"page_forward","click #paging_last":"page_last","change #entrycount":"change_entry_count"},page_entry_count:100,filter_delay:-1,initialize:function(){this.collection=new w,this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.listenTo(this.collection.filter_model,"change",this.render),this.type=""},render:function(){let e=this.$("tbody");e.length<1?0!==this.collection.length?(this.$el.html(v()({log_type:this.type,model:this.collection.filter_model})),e=this.$("tbody")):this.$el.html(j.a):e.html(""),0!==this.collection.length&&null==this.current_filtered_collection&&this.collection.forEach(t=>this.render_one(e,t)),this.$("#entrycountdisplay").html(this.page_entry_count),this.$("#currentpage").html(this.current_page+1),this.$("#pagecount").html(this.collection.page_count),this.$("#totalcount").html(this.collection.total_entries),this.$("#resultcount").html(this.collection.displayed_entries),this.current_page>=this.collection.page_count-1?(this.$("#paging_last").addClass("disabled"),this.$("#paging_forward").addClass("disabled")):(this.$("#paging_last").removeClass("disabled"),this.$("#paging_forward").removeClass("disabled")),this.current_page<=0?(this.$("#paging_back").addClass("disabled"),this.$("#paging_first").addClass("disabled")):(this.$("#paging_back").removeClass("disabled"),this.$("#paging_first").removeClass("disabled"))},render_one:function(e,t){const n=new T({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t,n){this.collection.uuid=t,this.collection.logType=e,this.collection.fileNo=n,this.type=e,this.current_page=0,this.$el.html(""),this.collection.filter_model.clear(),this.update()},update:function(){this.collection.page=this.current_page,this.collection.pageSize=this.page_entry_count,this.collection.fetch()},update_filter:function(e){clearTimeout(this.filter_delay);const t=e.target;this.collection.filter_model.set(t.name,$(t).val()),this.current_page=0,this.filter_delay=setTimeout(function(e){e.update()},500,this)},page_first:function(){this.current_page=0,this.update()},page_back:function(){this.current_page>0&&(this.current_page--,this.update())},page_forward:function(){this.current_page this.render_one(element)); - this.render_single_tabs(); + + if (this.logType == 'global') { + this.render_global_error_tab(); + } + else { + this.collection.forEach((element) => this.render_one_server(element)); + } }, - render_one: function(element) { - const servers = new LogCollection( + render_one_server: function(element) { + const files = new LogCollection( { - uuid: element.get('url'), - logType: element.get('logType') + uuid: element.get('id'), + logType: this.logType } ); const logList = new TabLogList({ - collection: servers, + collection: files, model: element, + logType: this.logType, logview: this.logview }); this.$el.append(logList.$el); - servers.fetch(); + files.fetch(); }, - render_single_tabs: function () { - const single_tab = new SingleTab({ - logview: this.logview, - log_name: 'global', - visible_name: 'Global Error Log', - log_type: 'errors'}); - single_tab.render(); - this.$el.append(single_tab.$el); + render_global_error_tab: function () { + const files = new LogCollection( + { + uuid: 'global', + logType: 'errors' + } + ); + const logList = new TabLogList({ + collection: files, + model: new Backbone.Model({ + server_name: 'Global Error Log', + id: 'global' + }), + logType: 'errors', + logview: this.logview + }); + this.$el.append(logList.$el); + files.fetch(); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js index fe82540f8..c0792ce47 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js @@ -33,29 +33,30 @@ const LogView = Backbone.View.extend({ className: 'content-box tab-content', events: { "keyup .filter input": "update_filter", + "click #paging_first": "page_first", "click #paging_back": "page_back", + "click #refresh": "update", "click #paging_forward": "page_forward", + "click #paging_last": "page_last", "change #entrycount": "change_entry_count", }, page_entry_count: 100, - current_page: 0, - current_filtered_collection: null, + filter_delay: -1, initialize: function() { this.collection = new LogLinesCollection(); - this.filter_model = new Backbone.Model(); - this.listenTo(this.collection, "sync", this.clear_and_render); - this.listenTo(this.collection, "update", this.clear_and_render); - this.listenTo(this.filter_model, "change", this.clear_and_render); + this.listenTo(this.collection, "sync", this.render); + this.listenTo(this.collection, "update", this.render); + this.listenTo(this.collection.filter_model, "change", this.render); this.type = ''; }, render: function() { - let tbody = this.$el.find('tbody'); + let tbody = this.$('tbody'); if (tbody.length < 1) { if (this.collection.length !== 0) { - this.$el.html(logViewer({log_type: this.type, model: this.filter_model})); - tbody = this.$el.find('tbody'); + this.$el.html(logViewer({log_type: this.type, model: this.collection.filter_model})); + tbody = this.$('tbody'); } else { this.$el.html(noDataAvailable); } @@ -63,57 +64,104 @@ const LogView = Backbone.View.extend({ else { tbody.html(''); } + if (this.collection.length !== 0) { if (this.current_filtered_collection == null) { - this.current_filtered_collection = this.collection.filter_collection(this.filter_model); + this.collection.forEach( + (model) => this.render_one(tbody, model) + ); } - const index_begin = this.current_page * this.page_entry_count; - const index_end = index_begin + this.page_entry_count; - this.current_filtered_collection.slice(index_begin, index_end).forEach( - (model) => this.render_one(tbody, model) - ); + } + + this.$('#entrycountdisplay').html(this.page_entry_count); + this.$('#currentpage').html(this.current_page + 1); + this.$('#pagecount').html(this.collection.page_count); + this.$('#totalcount').html(this.collection.total_entries); + this.$('#resultcount').html(this.collection.displayed_entries); + + if (this.current_page >= this.collection.page_count - 1) { + this.$('#paging_last').addClass("disabled"); + this.$('#paging_forward').addClass("disabled"); + } + else { + this.$('#paging_last').removeClass("disabled"); + this.$('#paging_forward').removeClass("disabled"); + } + + if (this.current_page <= 0) { + this.$('#paging_back').addClass("disabled"); + this.$('#paging_first').addClass("disabled"); + } + else { + this.$('#paging_back').removeClass("disabled"); + this.$('#paging_first').removeClass("disabled"); } }, + render_one: function(parent_element, model) { const logline = new LogViewLine({type: this.type, model: model}); logline.render(); parent_element.append(logline.$el); }, - get_log: function(type, uuid) { + + get_log: function(type, uuid, fileNo) { this.collection.uuid = uuid; this.collection.logType = type; + this.collection.fileNo = fileNo; this.type = type; + this.current_page = 0; this.$el.html(''); - this.filter_model.clear(); + this.collection.filter_model.clear(); this.update(); }, + update: function () { + this.collection.page = this.current_page; + this.collection.pageSize = this.page_entry_count; this.collection.fetch(); }, - clear_and_render: function() { - this.current_filtered_collection = null; - this.render(); - }, + update_filter: function (event) { + clearTimeout(this.filter_delay); const element = event.target; - this.filter_model.set(element.name, $(element).val()); + this.collection.filter_model.set(element.name, $(element).val()); + this.current_page = 0; + + // Delay update to avoid multiple requests during typing + this.filter_delay = setTimeout(function(instance) { + instance.update(); + }, 500, this); }, + + page_first: function () { + this.current_page = 0; + this.update(); + }, + page_back: function () { if (this.current_page > 0) { this.current_page--; - this.render(); + this.update(); } }, + page_forward: function () { - if ((this.current_page + 1) * this.page_entry_count < this.collection.length) { + if (this.current_page < this.collection.page_count) { this.current_page++; - this.render(); + this.update(); } }, + + page_last: function () { + this.current_page = this.collection.page_count - 1; + this.update(); + }, + change_entry_count: function (event) { this.page_entry_count = event.target.value; this.current_page = 0; - this.render(); + this.update(); } }); + export default LogView; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js index 60e1d3ba2..3533df639 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js @@ -9,8 +9,8 @@ let TabLogList = Backbone.View.extend({ }, initialize: function(data) { - this.listenTo(this.collection, "sync", this.render); this.listenTo(this.collection, "update", this.render); + this.logType = data.logType; this.logview = data.logview; }, @@ -21,21 +21,31 @@ let TabLogList = Backbone.View.extend({ renderCollection: function() { this.$el.addClass('dropdown'); + if (this.model.get('id') == "global") { + this.$el.addClass('active'); + this.logview.get_log('errors', 'global', -1); + } this.$el.html(''); this.$el.append( - TabTemplateCollection({model: this.collection, name: this.model.attributes.name}) + TabTemplateCollection({ + model: this.collection, + id: this.model.get('id'), + name: this.model.has('server_name') ? this.model.get('server_name') : "Port " + this.model.get('port') + }) ); }, mainMenuClick: function () { if (this.collection.models[0]) { - this.handleElementClick(this.collection.models[0].id); + this.handleElementClick(this.model.get('id'), this.collection.models[0].get('number')); + $(`#tab_${this.model.get('id')} li`).removeClass('active'); + $(`#subtab_item_${this.model.get('id')}_${this.collection.models[0].get('number')}`).parent().addClass('active'); } }, menuEntryClick: function (event) { - this.handleElementClick(event.target.dataset['modelUuid']); + this.handleElementClick(event.target.dataset['modelUuid'], event.target.dataset['modelFileno']); }, - handleElementClick: function (uuid) { - this.logview.get_log(this.model.get('logType'), uuid); + handleElementClick: function (uuid, fileNo) { + this.logview.get_log(this.logType, uuid, fileNo); } }); export default TabLogList; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js index dad07be5f..0051e00e4 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js @@ -1,15 +1,33 @@ -import {defaultEndpoints} from './config'; +import LogServerCollection from './models/LogServerCollection'; import LogCategoryList from './controller/LogCategoryList'; import LogView from './controller/LogView'; +// Skeleton with header (navigation) and footer (pagination) const logview = new LogView(); - +// Get type of log to display from volt view (data-log HTML attribute) +const type = $('#logapplication').data('log'); +// Query (HTTP or stream) server list +const servers = new LogServerCollection({ + logType: type +}); +// Render tabs with server logs (one tab per server) const menu = new LogCategoryList({ - collection: defaultEndpoints, - logview: logview + collection: servers, + logview: logview, + logType: type // 'errors', 'accesses' or 'global' }); -$(document.getElementById('logapplication')) +// Place log application to volt template +$('#logapplication') .append(menu.$el) .append(logview.$el); -menu.render(); + +if (type != 'global') { + // Global error log does not require server list + servers.fetch(); +} +else { + // Update of server list triggers render() which does not + // occur for global error log + menu.render(); +} diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js index 19febb1f3..0221235e1 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js @@ -3,10 +3,11 @@ import LogFileMenuEntry from './LogFileMenuEntry'; const LogCollection = Backbone.Collection.extend({ model: LogFileMenuEntry, url: function () { - return '/api/nginx/logs/' + this.logType; + return `/api/nginx/logs/${this.logType}/${this.uuid}`; }, initialize: function (params) { this.logType = params.logType; + this.uuid = params.uuid; } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js index 2e3eb2921..806f0c077 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js @@ -3,39 +3,29 @@ import LogLine from "./LogLine"; const LogLinesCollection = Backbone.Collection.extend({ model: LogLine, url: function () { - return `/api/nginx/logs/${this.logType}/${this.uuid}`; + return `/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`; }, initialize: function () { this.logType = 'none'; this.uuid = 'none'; + this.fileNo = -1; + this.page = 0; + this.pageSize = 0; + this.filter_model = new Backbone.Model(); }, parse: function(response) { if ('error' in response) { return []; } - return response; + else { + this.page_count = response.pages; + this.total_entries = response.total; + this.displayed_entries = response.found; + return response.lines; + } }, - filter_collection: function(filter_model) { - const filter_model_keys = filter_model.keys(); - return this.filter(function (model) { - if (!model) { - return false; - } - for (let i = 0; i < filter_model_keys.length; i++) { - const property = filter_model_keys[i]; - if (typeof(filter_model.get(property)) !== "string" - || filter_model.get(property).length === 0) { - continue; - } - if (!model.has(property)) { - return false; - } - if (!model.get(property).includes(filter_model.get(property))) { - return false; - } - } - return true; - }); + create_filter: function() { + return encodeURIComponent(JSON.stringify(this.filter_model)); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js new file mode 100644 index 000000000..9fdfdc75b --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js @@ -0,0 +1,13 @@ +import LogServerMenu from './LogServerMenu'; + +const LogServerCollection = Backbone.Collection.extend({ + model: LogServerMenu, + url: function () { + return `/api/nginx/logs/${this.logType}`; + }, + initialize: function (params) { + this.logType = params.logType; + } +}); + +export default LogServerCollection; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js new file mode 100644 index 000000000..170a06af6 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js @@ -0,0 +1,2 @@ +export default Backbone.Model.extend({ +}); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html index ee317a10c..d5ea04354 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html +++ b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html @@ -37,15 +37,16 @@ <%= _.escape(name) %> -