mirror of
https://github.com/craigerl/aprsd.git
synced 2025-09-04 22:27:50 -04:00
Added Logfile tab in Admin ui
This patch adds a live view of the aprsd logfile in the admin ui. This uses a new Log QueueHandler and the threads.logging_queue to push log entries into a queue. The flask websockets server will push those log entries up to a connected client browser.
This commit is contained in:
parent
4f088e0a4a
commit
d6b3df93f1
@ -486,6 +486,64 @@ class SendMessageNamespace(Namespace):
|
||||
LOG.debug(f"WS json {data}")
|
||||
|
||||
|
||||
class LogMonitorThread(threads.APRSDThread):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("LogMonitorThread")
|
||||
|
||||
def loop(self):
|
||||
global socketio
|
||||
try:
|
||||
record = threads.logging_queue.get(block=True, timeout=5)
|
||||
json_record = self.json_record(record)
|
||||
socketio.emit(
|
||||
"log_entry", json_record,
|
||||
namespace="/logs",
|
||||
)
|
||||
except Exception:
|
||||
# Just ignore thi
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def json_record(self, record):
|
||||
entry = {}
|
||||
entry["filename"] = record.filename
|
||||
entry["funcName"] = record.funcName
|
||||
entry["levelname"] = record.levelname
|
||||
entry["lineno"] = record.lineno
|
||||
entry["module"] = record.module
|
||||
entry["name"] = record.name
|
||||
entry["pathname"] = record.pathname
|
||||
entry["process"] = record.process
|
||||
entry["processName"] = record.processName
|
||||
if hasattr(record, "stack_info"):
|
||||
entry["stack_info"] = record.stack_info
|
||||
else:
|
||||
entry["stack_info"] = None
|
||||
entry["thread"] = record.thread
|
||||
entry["threadName"] = record.threadName
|
||||
entry["message"] = record.getMessage()
|
||||
return entry
|
||||
|
||||
|
||||
class LoggingNamespace(Namespace):
|
||||
|
||||
def on_connect(self):
|
||||
global socketio
|
||||
LOG.debug("Web socket connected")
|
||||
socketio.emit(
|
||||
"connected", {"data": "/logs Connected"},
|
||||
namespace="/logs",
|
||||
)
|
||||
self.log_thread = LogMonitorThread()
|
||||
self.log_thread.start()
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("WS Disconnected")
|
||||
self.log_thread.stop()
|
||||
|
||||
|
||||
def setup_logging(config, flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
|
||||
@ -548,4 +606,5 @@ def init_flask(config, loglevel, quiet):
|
||||
# eventlet.monkey_patch()
|
||||
|
||||
socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config))
|
||||
socketio.on_namespace(LoggingNamespace("/logs"))
|
||||
return socketio, flask_app
|
||||
|
@ -195,6 +195,20 @@ def setup_logging(config, loglevel, quiet):
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if (
|
||||
utils.check_config_option(
|
||||
config, ["aprsd", "web", "enabled"],
|
||||
default_fail=False,
|
||||
)
|
||||
):
|
||||
qh = logging.handlers.QueueHandler(threads.logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=utils.QUEUE_LOG_FORMAT,
|
||||
datefmt=utils.QUEUE_DATE_FORMAT,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
@ -506,10 +520,7 @@ def server(
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
try:
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"])
|
||||
except Exception:
|
||||
web_enabled = False
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False)
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
|
@ -17,6 +17,7 @@ RX_THREAD = "RX"
|
||||
EMAIL_THREAD = "Email"
|
||||
|
||||
rx_msg_queue = queue.Queue(maxsize=20)
|
||||
logging_queue = queue.Queue(maxsize=50)
|
||||
msg_queues = {
|
||||
"rx": rx_msg_queue,
|
||||
}
|
||||
|
@ -26,12 +26,17 @@ LOG_LEVELS = {
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]"
|
||||
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
|
||||
QUEUE_LOG_FORMAT = (
|
||||
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
# an example of what should be in the ~/.aprsd/config.yml
|
||||
DEFAULT_CONFIG_DICT = {
|
||||
@ -288,7 +293,7 @@ def conf_option_exists(conf, chain):
|
||||
|
||||
def check_config_option(config, chain, default_fail=None):
|
||||
result = conf_option_exists(config, chain.copy())
|
||||
if not result:
|
||||
if result is None:
|
||||
raise Exception(
|
||||
"'{}' was not in config file".format(
|
||||
chain,
|
||||
|
189
aprsd/web/static/css/prism.css
Normal file
189
aprsd/web/static/css/prism.css
Normal file
@ -0,0 +1,189 @@
|
||||
/* PrismJS 1.24.1
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
|
||||
/**
|
||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
||||
* @author Rose Pritchard
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #e2777a;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: #f08d49;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: #cc99cd;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
|
||||
div.code-toolbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar {
|
||||
position: absolute;
|
||||
top: .3em;
|
||||
right: .2em;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.code-toolbar:hover > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
||||
IE11 and old Edge versions don't support :focus-within. */
|
||||
div.code-toolbar:focus-within > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
-webkit-user-select: none; /* for button */
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span {
|
||||
color: #bbb;
|
||||
font-size: .8em;
|
||||
padding: 0 .5em;
|
||||
background: #f5f2f0;
|
||||
background: rgba(224, 224, 224, 0.2);
|
||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
26
aprsd/web/static/js/logs.js
Normal file
26
aprsd/web/static/js/logs.js
Normal file
@ -0,0 +1,26 @@
|
||||
function init_logs() {
|
||||
const socket = io("/logs");
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to logs socketio");
|
||||
});
|
||||
|
||||
socket.on('connected', function(msg) {
|
||||
console.log("Connected to /logs");
|
||||
console.log(msg);
|
||||
});
|
||||
|
||||
socket.on('log_entry', function(data) {
|
||||
update_logs(data);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
function update_logs(data) {
|
||||
var code_block = $('#logtext')
|
||||
entry = data["message"]
|
||||
const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log');
|
||||
code_block.append(html_pretty + "<br>");
|
||||
var div = document.getElementById('logContainer');
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
2247
aprsd/web/static/js/prism.js
Normal file
2247
aprsd/web/static/js/prism.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -5,10 +5,6 @@
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
|
||||
@ -16,14 +12,17 @@
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
<link rel="stylesheet" href="/static/css/prism.css">
|
||||
<script src="/static/js/prism.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/charts.js"></script>
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
<script src="/static/js/packets.js"></script>
|
||||
<script src="/static/js/logs.js"></script>
|
||||
|
||||
|
||||
<script type="text/javascript"">
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
|
||||
var memory_chart = null
|
||||
@ -35,6 +34,7 @@
|
||||
start_update();
|
||||
start_charts();
|
||||
init_messages();
|
||||
init_logs();
|
||||
|
||||
$("#toggleStats").click(function() {
|
||||
$("#jsonstats").fadeToggle(1000);
|
||||
@ -48,6 +48,10 @@
|
||||
$("#configjson").html(html_pretty);
|
||||
$("#jsonstats").fadeToggle(1000);
|
||||
|
||||
//var log_text_pretty = $('#logtext').text();
|
||||
//const log_pretty = Prism.highlight( log_text_pretty, Prism.languages.log, 'log');
|
||||
//$('#logtext').html(log_pretty);
|
||||
|
||||
$('.ui.accordion').accordion({exclusive: false});
|
||||
$('.menu .item').tab('change tab', 'charts-tab');
|
||||
});
|
||||
@ -79,6 +83,7 @@
|
||||
<div class="item" data-tab="plugin-tab">Plugins</div>
|
||||
<div class="item" data-tab="config-tab">Config</div>
|
||||
<div class="item" data-tab="send-tab">Send Message</div>
|
||||
<div class="item" data-tab="log-tab">LogFile</div>
|
||||
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
||||
</div>
|
||||
|
||||
@ -171,6 +176,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="log-tab">
|
||||
<h3 class="ui dividing header">LOGFILE</h3>
|
||||
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
||||
<h3 class="ui dividing header">Raw JSON</h3>
|
||||
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre>
|
||||
|
Loading…
x
Reference in New Issue
Block a user