mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
Logviewer streams (#1017)
* www/nginx: add logviewer support for streams * www/nginx: add release note * www/nginx: bugfix used incorrect variable name * www/nginx: remove useless brackets
This commit is contained in:
@@ -11,6 +11,7 @@ Plugin Changelog
|
||||
1.5
|
||||
|
||||
* Add proxy options for ignore client abort and disabling buffering
|
||||
* Add logviewer support for streams
|
||||
|
||||
1.4 (Development only)
|
||||
|
||||
|
||||
@@ -57,6 +57,28 @@ class LogsController extends ApiControllerBase
|
||||
return $this->call_configd('error', $uuid);
|
||||
}
|
||||
}
|
||||
public function stream_accessesAction($uuid = null)
|
||||
{
|
||||
$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 {
|
||||
// emulate REST call for a specific log /stream_accesses/uuid
|
||||
return $this->call_configd_stream('streamaccess', $uuid);
|
||||
}
|
||||
}
|
||||
public function stream_errorsAction($uuid = null)
|
||||
{
|
||||
$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 {
|
||||
// emulate REST call for a specific log /stream_errors/uuid
|
||||
return $this->call_configd_stream('streamerror', $uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private function call_configd($type, $uuid)
|
||||
{
|
||||
@@ -68,6 +90,16 @@ class LogsController extends ApiControllerBase
|
||||
$data = $backend->configdRun('nginx log ' . $type . ' ' . $uuid);
|
||||
return json_decode($data, true);
|
||||
}
|
||||
private function call_configd_stream($type, $uuid)
|
||||
{
|
||||
if (!$this->stream_exists($uuid)) {
|
||||
$this->response->setStatusCode(404, "Not Found");
|
||||
}
|
||||
|
||||
$backend = new Backend();
|
||||
$data = $backend->configdRun('nginx log ' . $type . ' ' . $uuid);
|
||||
return json_decode($data, true);
|
||||
}
|
||||
|
||||
private function list_vhosts()
|
||||
{
|
||||
@@ -77,10 +109,23 @@ class LogsController extends ApiControllerBase
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
private function list_streams()
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->nginx->stream_server->__items as $item) {
|
||||
$data[] = array('id' => $item->getAttributes()['uuid'], 'port' => (string)$item->listen_port);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function vhost_exists($uuid)
|
||||
{
|
||||
$data = $this->nginx->getNodeByReference('http_server.'. $uuid);
|
||||
return isset($data);
|
||||
}
|
||||
private function stream_exists($uuid)
|
||||
{
|
||||
$data = $this->nginx->getNodeByReference('stream_server.'. $uuid);
|
||||
return isset($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/*
|
||||
|
||||
Copyright (C) 2018 Fabian Franz
|
||||
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.
|
||||
*/
|
||||
|
||||
namespace OPNsense\Nginx;
|
||||
|
||||
class StreamAccessLogLine
|
||||
{
|
||||
public $remote_ip;
|
||||
public $time;
|
||||
public $status;
|
||||
public $bytes_sent;
|
||||
public $bytes_received;
|
||||
public $session_time;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/*
|
||||
|
||||
Copyright (C) 2018 Fabian Franz
|
||||
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.
|
||||
*/
|
||||
|
||||
namespace OPNsense\Nginx;
|
||||
|
||||
class StreamAccessLogParser
|
||||
{
|
||||
private $file_name;
|
||||
private $lines;
|
||||
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->lines = file($this->file_name);
|
||||
$this->result = array_map([$this, 'parse_line'], $this->lines);
|
||||
}
|
||||
private function parse_line($line)
|
||||
{
|
||||
$container = new StreamAccessLogLine();
|
||||
if (preg_match(self::LogLineRegex, $line, $data)) {
|
||||
$container->remote_ip = $data[1];
|
||||
$container->time = $data[2];
|
||||
$container->status = $data[3];
|
||||
$container->bytes_sent = $data[4];
|
||||
$container->bytes_received = $data[5];
|
||||
$container->session_time = $data[6];
|
||||
}
|
||||
return $container;
|
||||
}
|
||||
|
||||
public function get_result()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
</script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/lib/lodash.min.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/lib/backbone-min.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/dist/configuration.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/dist/configuration.min.js') }}"></script>
|
||||
<style>
|
||||
#frm_sni_hostname_mapdlg .col-md-4,
|
||||
#frm_ipacl_dlg .col-md-4 {
|
||||
|
||||
@@ -29,4 +29,4 @@
|
||||
|
||||
<script src="{{ cache_safe('/ui/js/nginx/lib/lodash.min.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/lib/backbone-min.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/dist/logviewer.js') }}"></script>
|
||||
<script src="{{ cache_safe('/ui/js/nginx/dist/logviewer.min.js') }}"></script>
|
||||
|
||||
@@ -30,6 +30,7 @@ require_once 'config.inc';
|
||||
use OPNsense\Nginx\Nginx;
|
||||
use OPNsense\Nginx\ErrorLogParser;
|
||||
use OPNsense\Nginx\AccessLogParser;
|
||||
use OPNsense\Nginx\StreamAccessLogParser;
|
||||
|
||||
$log_prefix = '/var/log/nginx/';
|
||||
$log_suffix = '.log';
|
||||
@@ -45,35 +46,73 @@ $mode = $_SERVER['argv'][1];
|
||||
$server = $_SERVER['argv'][2];
|
||||
$nginx = new Nginx();
|
||||
|
||||
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 = [];
|
||||
foreach (explode(',', $server_names) as $server_name) {
|
||||
$log_file_name = $log_prefix . basename($server_name) . '.' . $mode . $log_suffix;
|
||||
// this entry has no log file, ignore it
|
||||
if (!file_exists($log_file_name)) {
|
||||
continue;
|
||||
}
|
||||
$logparser = null;
|
||||
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 = [];
|
||||
foreach (explode(',', $server_names) as $server_name) {
|
||||
$log_file_name = $log_prefix . basename($server_name) . '.' . $mode . $log_suffix;
|
||||
// this entry has no log file, ignore it
|
||||
if (!file_exists($log_file_name)) {
|
||||
continue;
|
||||
}
|
||||
$logparser = null;
|
||||
|
||||
if ($mode == 'error') {
|
||||
$logparser = new ErrorLogParser($log_file_name);
|
||||
} elseif ($mode == 'access') {
|
||||
$logparser = new AccessLogParser($log_file_name);
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
$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"}');
|
||||
}
|
||||
// we cannot parse the file - something went wrong
|
||||
if ($logparser == null) {
|
||||
continue;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
$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"}');
|
||||
}
|
||||
$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"}');
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,10 +1,18 @@
|
||||
export const defaultEndpoints = new Backbone.Collection([
|
||||
{
|
||||
"name": 'Access Logs',
|
||||
"name": 'HTTP Access Logs',
|
||||
"logType" : 'accesses'
|
||||
},
|
||||
{
|
||||
"name": 'Error Logs',
|
||||
"name": 'HTTP Error Logs',
|
||||
"logType" : 'errors'
|
||||
},
|
||||
{
|
||||
"name": 'Stream Access Logs',
|
||||
"logType" : 'stream_accesses'
|
||||
},
|
||||
{
|
||||
"name": 'Stream Error Logs',
|
||||
"logType" : 'stream_errors'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import accessLogLine from '../templates/AccessLogLine.html';
|
||||
import streamAccessLogLine from '../templates/StreamAccessLogLine.html';
|
||||
import errorLogLine from '../templates/ErrorLogLine.html';
|
||||
import logViewer from '../templates/logviewer.html';
|
||||
import LogLinesCollection from "../models/LogLinesCollection";
|
||||
import noDataAvailable from '../templates/noDataAvailable.html';
|
||||
|
||||
|
||||
const LogViewLine = Backbone.View.extend({
|
||||
@@ -17,6 +19,8 @@ const LogViewLine = Backbone.View.extend({
|
||||
get_template: function() {
|
||||
if (this.type === 'accesses') {
|
||||
return accessLogLine;
|
||||
} else if (this.type === 'stream_accesses') {
|
||||
return streamAccessLogLine;
|
||||
} else {
|
||||
return errorLogLine;
|
||||
}
|
||||
@@ -43,15 +47,21 @@ const LogView = Backbone.View.extend({
|
||||
render: function() {
|
||||
let tbody = this.$el.find('tbody');
|
||||
if (tbody.length < 1) {
|
||||
this.$el.html(logViewer({log_type: this.type, model: this.filter_model}));
|
||||
tbody = this.$el.find('tbody');
|
||||
if (this.collection.length !== 0) {
|
||||
this.$el.html(logViewer({log_type: this.type, model: this.filter_model}));
|
||||
tbody = this.$el.find('tbody');
|
||||
} else {
|
||||
this.$el.html(noDataAvailable);
|
||||
}
|
||||
}
|
||||
else {
|
||||
tbody.html('');
|
||||
}
|
||||
this.collection.filter_collection(this.filter_model).forEach(
|
||||
(model) => this.render_one(tbody, model)
|
||||
);
|
||||
if (this.collection.length !== 0) {
|
||||
this.collection.filter_collection(this.filter_model).forEach(
|
||||
(model) => this.render_one(tbody, model)
|
||||
);
|
||||
}
|
||||
},
|
||||
render_one: function(parent_element, model) {
|
||||
const logline = new LogViewLine({type: this.type, model: model});
|
||||
|
||||
@@ -9,6 +9,12 @@ const LogLinesCollection = Backbone.Collection.extend({
|
||||
this.logType = 'none';
|
||||
this.uuid = 'none';
|
||||
},
|
||||
parse: function(response) {
|
||||
if ('error' in response) {
|
||||
return [];
|
||||
}
|
||||
return response;
|
||||
},
|
||||
filter_collection: function(filter_model) {
|
||||
const filter_model_keys = filter_model.keys();
|
||||
return this.filter(function (model) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<td class="time"><%= model.escape('time') %></td>
|
||||
<td class="remote_ip"><%= model.escape('remote_ip') %></td>
|
||||
<td class="status"><%= model.escape('status') %></td>
|
||||
<td class="bytes_sent"><%= model.escape('bytes_sent') %></td>
|
||||
<td class="bytes_received"><%= model.escape('bytes_received') %></td>
|
||||
<td class="session_time"><%= model.escape('session_time') %></td>
|
||||
@@ -45,7 +45,7 @@
|
||||
data-model-cid="<%= row.cid %>"
|
||||
data-model-uuid="<%= row.escape('id') %>"
|
||||
id="subtab_item_<%= row.escape('id') %>"
|
||||
href="#subtab_<%= row.escape('id') %>"><%= row.escape('server_name') %></a>
|
||||
href="#subtab_<%= row.escape('id') %>"><%= row.has("server_name") ? row.escape("server_name") : row.escape("port") %></a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
|
||||
@@ -29,5 +29,5 @@
|
||||
*/
|
||||
%>
|
||||
<a data-toggle="tab" href="#subtab_item_<%= model.escape('id') %>">
|
||||
<b><%= model.escape("server_name") %></b>
|
||||
<b><%= model.has("server_name") ? model.escape("server_name") : model.escape("port") %></b>
|
||||
</a>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<% if (log_type === 'errors') { %>
|
||||
<% if (log_type === 'errors' || log_type === 'stream_errors') { %>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Severity</th>
|
||||
<th>Number</th>
|
||||
<th>Message</th>
|
||||
<% } else { %>
|
||||
<% } else if (log_type === 'accesses') { %>
|
||||
<th>Time</th>
|
||||
<th>Remote IP</th>
|
||||
<th>Username</th>
|
||||
@@ -17,16 +17,23 @@
|
||||
<th>User Agent</th>
|
||||
<th>Forwarded For</th>
|
||||
<th>Request Line</th>
|
||||
<% } else {%>
|
||||
<th>Time</th>
|
||||
<th>Remote IP</th>
|
||||
<th>Status</th>
|
||||
<th>Bytes Sent</th>
|
||||
<th>Bytes Received</th>
|
||||
<th>Session Time</th>
|
||||
<% } %>
|
||||
</tr>
|
||||
<tr class="filter">
|
||||
<% if (log_type === 'errors') { %>
|
||||
<% if (log_type === 'errors' || log_type === 'stream_errors') { %>
|
||||
<td><input type="text" value="<%= model.escape('date') %>" name="date" /></td>
|
||||
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
|
||||
<td><input type="text" value="<%= model.escape('severity') %>" name="severity" /></td>
|
||||
<td><input type="text" value="<%= model.escape('number') %>" name="number" /></td>
|
||||
<td><input type="text" value="<%= model.escape('message') %>" name="message" /></td>
|
||||
<% } else { %>
|
||||
<% } else if (log_type === 'accesses') { %>
|
||||
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
|
||||
<td><input type="text" value="<%= model.escape('remote_ip') %>" name="remote_ip" /></td>
|
||||
<td><input type="text" value="<%= model.escape('username') %>" name="username" /></td>
|
||||
@@ -36,6 +43,13 @@
|
||||
<td><input type="text" value="<%= model.escape('user_agent') %>" name="user_agent" /></td>
|
||||
<td><input type="text" value="<%= model.escape('forwarded_for') %>" name="forwarded_for" /></td>
|
||||
<td><input type="text" value="<%= model.escape('request_line') %>" name="request_line" /></td>
|
||||
<% } else { %>
|
||||
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
|
||||
<td><input type="text" value="<%= model.escape('remote_ip') %>" name="remote_ip" /></td>
|
||||
<td><input type="text" value="<%= model.escape('status') %>" name="status" /></td>
|
||||
<td><input type="text" value="<%= model.escape('bytes_sent') %>" name="bytes_sent" /></td>
|
||||
<td><input type="text" value="<%= model.escape('bytes_received') %>" name="bytes_received" /></td>
|
||||
<td><input type="text" value="<%= model.escape('session_time') %>" name="session_time" /></td>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="text-align: center;">
|
||||
No data avialable
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
filename: '[name].min.js'
|
||||
},
|
||||
mode: 'production',
|
||||
module: {
|
||||
|
||||
Reference in New Issue
Block a user