mirror of
https://github.com/craigerl/aprsd.git
synced 2025-04-04 10:38:38 -04:00
Merge pull request #54 from craigerl/stats-web-ui
Added aprsd web index page
This commit is contained in:
commit
cabe374909
@ -5,6 +5,9 @@ APRSD
|
||||
.. image:: https://badge.fury.io/py/aprsd.svg
|
||||
:target: https://badge.fury.io/py/aprsd
|
||||
|
||||
.. image:: http://hits.dwyl.com/craigerl/aprsd.svg
|
||||
:target: http://hits.dwyl.com/craigerl/aprsd
|
||||
|
||||
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
|
||||
:target: https://github.com/craigerl/aprsd/actions
|
||||
|
||||
|
39
aprsd-lnav.json
Normal file
39
aprsd-lnav.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"aprsd" : {
|
||||
"title" : "APRSD APRS-IS server log format",
|
||||
"description" : "Log formats used by ARPRSD server",
|
||||
"url" : "http://github.com/craigerl/aprsd",
|
||||
"regex" : {
|
||||
"std" : {
|
||||
"pattern" : "^\\[(?<timestamp>\\d{2}\\/\\d{2}\\/\\d{4} \\d{2}:\\d{2}:\\d{2} ([AaPp][Mm]))\\] \\[(?<thread>\\w+\\s*)\\] \\[(?<alert_level>\\w+\\s*)\\] (?<body>([^-]*)-*)\\s\\[(?<file>([^:]*))\\:(?<line>\\d+)\\]"
|
||||
}
|
||||
},
|
||||
"level-field" : "alert_level",
|
||||
"level" : {
|
||||
"info" : "INFO",
|
||||
"error" : "ERROR",
|
||||
"warning" : "WARN",
|
||||
"debug" : "DEBUG",
|
||||
"fatal" : "FATAL",
|
||||
"info" : "UNKNOWN"
|
||||
},
|
||||
"value" : {
|
||||
"alert_level": { "kind" : "string", "identifier" : true },
|
||||
"thread": { "kind" : "string", "identifier" : true },
|
||||
"body" : { "kind" : "string" },
|
||||
"file" : { "kind" : "string" }
|
||||
},
|
||||
"timestamp-field" : "timestamp",
|
||||
"timestamp-format" : [
|
||||
"%m/%d/%Y %I:%M:%S %p"
|
||||
],
|
||||
"sample" : [
|
||||
{
|
||||
"line" : "[03/30/2021 08:57:44 PM] [MainThread ] [INFO ] Skipping Custom Plugins directory. - [/home/waboring/devel/aprsd/aprsd/plugin.py:232]"
|
||||
},
|
||||
{
|
||||
"line" : "[03/30/2021 08:57:44 PM] [KeepAlive ] [DEBUG] Uptime (0:00:00.577754) Tracker(0) Msgs: TX:0 RX:0 EmailThread: N/A RAM: Current:50289 Peak:99697 - [/home/waboring/devel/aprsd/aprsd/threads.py:89]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -3,9 +3,17 @@ import select
|
||||
import time
|
||||
|
||||
import aprsd
|
||||
from aprsd import stats
|
||||
import aprslib
|
||||
from aprslib import is_py3
|
||||
from aprslib.exceptions import LoginError
|
||||
from aprslib.exceptions import (
|
||||
ConnectionDrop,
|
||||
ConnectionError,
|
||||
GenericError,
|
||||
LoginError,
|
||||
ParseError,
|
||||
UnknownFormat,
|
||||
)
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -18,6 +26,7 @@ class Client:
|
||||
config = None
|
||||
|
||||
connected = False
|
||||
server_string = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
@ -153,7 +162,16 @@ class Aprsdis(aprslib.IS):
|
||||
|
||||
self.logger.debug("Server: %s", test)
|
||||
|
||||
_, _, callsign, status, _ = test.split(" ", 4)
|
||||
a, b, callsign, status, e = test.split(" ", 4)
|
||||
s = e.split(",")
|
||||
if len(s):
|
||||
server_string = s[0].replace("server ", "")
|
||||
else:
|
||||
server_string = e.replace("server ", "")
|
||||
|
||||
self.logger.info("Connected to {}".format(server_string))
|
||||
self.server_string = server_string
|
||||
stats.APRSDStats().set_aprsis_server(server_string)
|
||||
|
||||
if callsign == "":
|
||||
raise LoginError("Server responded with empty callsign???")
|
||||
@ -171,11 +189,67 @@ class Aprsdis(aprslib.IS):
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error("Failed to login")
|
||||
self.logger.error("Failed to login '{}'".format(e))
|
||||
raise LoginError("Failed to login")
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
"""
|
||||
When a position sentence is received, it will be passed to the callback function
|
||||
|
||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||
You can still exit the loop, by raising StopIteration in the callback function
|
||||
|
||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ConnectionError("not connected to a server")
|
||||
|
||||
line = b""
|
||||
|
||||
while True:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b"#":
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||
stats.APRSDStats().set_aprsis_keepalive()
|
||||
except ParseError as exp:
|
||||
self.logger.log(11, "%s\n Packet: %s", exp.args[0], exp.args[1])
|
||||
except UnknownFormat as exp:
|
||||
self.logger.log(9, "%s\n Packet: %s", exp.args[0], exp.args[1])
|
||||
except LoginError as exp:
|
||||
self.logger.error("%s: %s", exp.__class__.__name__, exp.args[0])
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (ConnectionDrop, ConnectionError):
|
||||
self.close()
|
||||
|
||||
if not immortal:
|
||||
raise
|
||||
else:
|
||||
self.connect(blocking=blocking)
|
||||
continue
|
||||
except GenericError:
|
||||
pass
|
||||
except StopIteration:
|
||||
break
|
||||
except Exception:
|
||||
self.logger.error("APRS Packet: %s", line)
|
||||
raise
|
||||
|
||||
if not blocking:
|
||||
break
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = Client()
|
||||
|
@ -1,8 +1,11 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import sys
|
||||
|
||||
import aprsd
|
||||
from aprsd import messaging, plugin, stats
|
||||
from aprsd import messaging, plugin, stats, utils
|
||||
import flask
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
@ -39,9 +42,14 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
users = self.users
|
||||
|
||||
@auth.login_required
|
||||
def index(self):
|
||||
return "Hello"
|
||||
# return flask.render_template("index.html", message=msg)
|
||||
stats = self._stats()
|
||||
return flask.render_template(
|
||||
"index.html",
|
||||
initial_stats=stats,
|
||||
callsign=self.config["aprs"]["login"],
|
||||
)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
@ -67,29 +75,71 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def stats(self):
|
||||
def _stats(self):
|
||||
stats_obj = stats.APRSDStats()
|
||||
track = messaging.MsgTrack()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
|
||||
stats_dict = stats_obj.stats()
|
||||
|
||||
result = {
|
||||
"version": aprsd.__version__,
|
||||
"uptime": stats_obj.uptime,
|
||||
"time": now.strftime(time_format),
|
||||
"size_tracker": len(track),
|
||||
"stats": stats_obj.stats(),
|
||||
"stats": stats_dict,
|
||||
}
|
||||
return json.dumps(result)
|
||||
|
||||
return result
|
||||
|
||||
def stats(self):
|
||||
return json.dumps(self._stats())
|
||||
|
||||
|
||||
def init_flask(config):
|
||||
def setup_logging(config, flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
|
||||
if not config["aprsd"]["web"].get("logging_enabled", False):
|
||||
# disable web logging
|
||||
flask_log.disabled = True
|
||||
flask_app.logger.disabled = True
|
||||
return
|
||||
|
||||
log_level = utils.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
for handler in flask_app.logger.handlers:
|
||||
handler.setFormatter(log_formatter)
|
||||
print(handler)
|
||||
|
||||
flask_log.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(sh)
|
||||
|
||||
|
||||
def init_flask(config, loglevel, quiet):
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="",
|
||||
static_folder="web/static",
|
||||
template_folder="web/templates",
|
||||
)
|
||||
setup_logging(config, flask_app, loglevel, quiet)
|
||||
server = APRSDFlask()
|
||||
server.set_config(config)
|
||||
# flask_app.route('/', methods=['GET'])(server.index)
|
||||
flask_app.route("/", methods=["GET"])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
|
@ -215,6 +215,15 @@ def check(loglevel, config_file, health_url, timeout):
|
||||
LOG.error("Email thread is very old! {}".format(d))
|
||||
sys.exit(-1)
|
||||
|
||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||
delta = parse_delta_str(aprsis_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error("APRS-IS last update is very old! {}".format(d))
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
@ -42,13 +42,6 @@ import click_completion
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
@ -175,7 +168,7 @@ def signal_handler(sig, frame):
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = LOG_LEVELS[loglevel]
|
||||
log_level = utils.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||
@ -485,7 +478,7 @@ def server(
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
app = flask.init_flask(config)
|
||||
app = flask.init_flask(config, loglevel, quiet)
|
||||
app.run(
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
port=config["aprsd"]["web"]["port"],
|
||||
|
@ -2,6 +2,9 @@ import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import aprsd
|
||||
from aprsd import utils
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@ -12,6 +15,7 @@ class APRSDStats:
|
||||
config = None
|
||||
|
||||
start_time = None
|
||||
_aprsis_keepalive = None
|
||||
|
||||
_msgs_tracked = 0
|
||||
_msgs_tx = 0
|
||||
@ -26,12 +30,16 @@ class APRSDStats:
|
||||
_email_tx = 0
|
||||
_email_rx = 0
|
||||
|
||||
_mem_current = 0
|
||||
_mem_peak = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
# any initializetion here
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance.start_time = datetime.datetime.now()
|
||||
cls._instance._aprsis_keepalive = datetime.datetime.now()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
@ -43,6 +51,42 @@ class APRSDStats:
|
||||
with self.lock:
|
||||
return str(datetime.datetime.now() - self.start_time)
|
||||
|
||||
@property
|
||||
def memory(self):
|
||||
with self.lock:
|
||||
return self._mem_current
|
||||
|
||||
def set_memory(self, memory):
|
||||
with self.lock:
|
||||
self._mem_current = memory
|
||||
|
||||
@property
|
||||
def memory_peak(self):
|
||||
with self.lock:
|
||||
return self._mem_peak
|
||||
|
||||
def set_memory_peak(self, memory):
|
||||
with self.lock:
|
||||
self._mem_peak = memory
|
||||
|
||||
@property
|
||||
def aprsis_server(self):
|
||||
with self.lock:
|
||||
return self._aprsis_server
|
||||
|
||||
def set_aprsis_server(self, server):
|
||||
with self.lock:
|
||||
self._aprsis_server = server
|
||||
|
||||
@property
|
||||
def aprsis_keepalive(self):
|
||||
with self.lock:
|
||||
return self._aprsis_keepalive
|
||||
|
||||
def set_aprsis_keepalive(self):
|
||||
with self.lock:
|
||||
self._aprsis_keepalive = datetime.datetime.now()
|
||||
|
||||
@property
|
||||
def msgs_tx(self):
|
||||
with self.lock:
|
||||
@ -126,7 +170,30 @@ class APRSDStats:
|
||||
|
||||
def stats(self):
|
||||
now = datetime.datetime.now()
|
||||
if self._email_thread_last_time:
|
||||
last_update = str(now - self._email_thread_last_time)
|
||||
else:
|
||||
last_update = "never"
|
||||
|
||||
if self._aprsis_keepalive:
|
||||
last_aprsis_keepalive = str(now - self._aprsis_keepalive)
|
||||
else:
|
||||
last_aprsis_keepalive = "never"
|
||||
|
||||
stats = {
|
||||
"aprsd": {
|
||||
"version": aprsd.__version__,
|
||||
"uptime": self.uptime,
|
||||
"memory_current": self.memory,
|
||||
"memory_current_str": utils.human_size(self.memory),
|
||||
"memory_peak": self.memory_peak,
|
||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
||||
},
|
||||
"aprs-is": {
|
||||
"server": self.aprsis_server,
|
||||
"callsign": self.config["aprs"]["login"],
|
||||
"last_update": last_aprsis_keepalive,
|
||||
},
|
||||
"messages": {
|
||||
"tracked": self.msgs_tracked,
|
||||
"sent": self.msgs_tx,
|
||||
@ -136,9 +203,10 @@ class APRSDStats:
|
||||
"mic-e recieved": self.msgs_mice_rx,
|
||||
},
|
||||
"email": {
|
||||
"enabled": self.config["aprsd"]["email"]["enabled"],
|
||||
"sent": self._email_tx,
|
||||
"recieved": self._email_rx,
|
||||
"thread_last_update": str(now - self._email_thread_last_time),
|
||||
"thread_last_update": last_update,
|
||||
},
|
||||
}
|
||||
return stats
|
||||
|
@ -1,12 +1,13 @@
|
||||
import abc
|
||||
import datetime
|
||||
import gc
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import tracemalloc
|
||||
|
||||
from aprsd import client, messaging, plugin, stats, trace
|
||||
from aprsd import client, messaging, plugin, stats, trace, utils
|
||||
import aprslib
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@ -74,26 +75,33 @@ class KeepAliveThread(APRSDThread):
|
||||
|
||||
def loop(self):
|
||||
if self.cntr % 6 == 0:
|
||||
nuked = gc.collect()
|
||||
tracker = messaging.MsgTrack()
|
||||
stats_obj = stats.APRSDStats()
|
||||
now = datetime.datetime.now()
|
||||
last_email = stats.APRSDStats().email_thread_time
|
||||
last_email = stats_obj.email_thread_time
|
||||
if last_email:
|
||||
email_thread_time = str(now - last_email)
|
||||
else:
|
||||
email_thread_time = "N/A"
|
||||
|
||||
last_msg_time = str(now - stats_obj.aprsis_keepalive)
|
||||
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
stats_obj.set_memory(current)
|
||||
stats_obj.set_memory_peak(peak)
|
||||
LOG.debug(
|
||||
"Uptime ({}) Tracker({}) "
|
||||
"Msgs: TX:{} RX:{} EmailThread: {} RAM: Current:{} Peak:{}".format(
|
||||
"Msgs: TX:{} RX:{} Last: {} - EmailThread: {} - RAM: Current:{} Peak:{} Nuked: {}".format(
|
||||
stats_obj.uptime,
|
||||
len(tracker),
|
||||
stats_obj.msgs_tx,
|
||||
stats_obj.msgs_rx,
|
||||
last_msg_time,
|
||||
email_thread_time,
|
||||
current,
|
||||
peak,
|
||||
utils.human_size(current),
|
||||
utils.human_size(peak),
|
||||
nuked,
|
||||
),
|
||||
)
|
||||
self.cntr += 1
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import errno
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@ -11,9 +12,17 @@ from aprsd import plugin
|
||||
import click
|
||||
import yaml
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s.%(funcName)s:%(lineno)d]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
@ -37,6 +46,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"units": "imperial",
|
||||
"web": {
|
||||
"enabled": True,
|
||||
"logging_enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
@ -351,3 +361,10 @@ def parse_config(config_file):
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def human_size(bytes, units=None):
|
||||
""" Returns a human readable string representation of bytes """
|
||||
if not units:
|
||||
units = [" bytes", "KB", "MB", "GB", "TB", "PB", "EB"]
|
||||
return str(bytes) + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])
|
||||
|
@ -1,4 +1,337 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
|
||||
<body><h1>{{ message }}</h1></body>
|
||||
<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>
|
||||
|
||||
|
||||
<script type="text/javascript"">
|
||||
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
var color = Chart.helpers.color;
|
||||
|
||||
window.chartColors = {
|
||||
red: 'rgb(255, 99, 132)',
|
||||
orange: 'rgb(255, 159, 64)',
|
||||
yellow: 'rgb(255, 205, 86)',
|
||||
green: 'rgb(26, 181, 77)',
|
||||
blue: 'rgb(54, 162, 235)',
|
||||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(201, 203, 207)',
|
||||
black: 'rgb(0, 0, 0)'
|
||||
};
|
||||
|
||||
function start_charts() {
|
||||
Chart.scaleService.updateScaleDefaults('linear', {
|
||||
ticks: {
|
||||
min: 0
|
||||
}
|
||||
});
|
||||
|
||||
memory_chart = new Chart($("#memChart"), {
|
||||
label: 'Memory Usage',
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Peak Ram usage',
|
||||
borderColor: window.chartColors.red,
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
label: 'Current Ram usage',
|
||||
borderColor: window.chartColors.blue,
|
||||
data: [],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Memory Usage',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
offset: true,
|
||||
ticks: {
|
||||
major: { enabled: true },
|
||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||
source: 'data',
|
||||
maxRotation: 0,
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 75,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
message_chart = new Chart($("#messageChart"), {
|
||||
label: 'Messages',
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Messages Sent',
|
||||
borderColor: window.chartColors.green,
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
label: 'Messages Recieved',
|
||||
borderColor: window.chartColors.yellow,
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
label: 'Ack Sent',
|
||||
borderColor: window.chartColors.purple,
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
label: 'Ack Recieved',
|
||||
borderColor: window.chartColors.black,
|
||||
data: [],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'APRS Messages',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
offset: true,
|
||||
ticks: {
|
||||
major: { enabled: true },
|
||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||
source: 'data',
|
||||
maxRotation: 0,
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 75,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
email_chart = new Chart($("#emailChart"), {
|
||||
label: 'Email Messages',
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Sent',
|
||||
borderColor: window.chartColors.green,
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
label: 'Recieved',
|
||||
borderColor: window.chartColors.yellow,
|
||||
data: [],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Email Messages',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
offset: true,
|
||||
ticks: {
|
||||
major: { enabled: true },
|
||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||
source: 'data',
|
||||
maxRotation: 0,
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 75,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function addData(chart, label, newdata) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets.forEach((dataset) => {
|
||||
dataset.data.push(newdata);
|
||||
});
|
||||
chart.update();
|
||||
}
|
||||
|
||||
function updateDualData(chart, label, first, second) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(first);
|
||||
chart.data.datasets[1].data.push(second);
|
||||
chart.update();
|
||||
}
|
||||
function updateQuadData(chart, label, first, second, third, fourth) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(first);
|
||||
chart.data.datasets[1].data.push(second);
|
||||
chart.data.datasets[2].data.push(third);
|
||||
chart.data.datasets[3].data.push(fourth);
|
||||
chart.update();
|
||||
}
|
||||
|
||||
function update_stats( data ) {
|
||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||
$("#aprsis").text( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] );
|
||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||
$("#jsonstats").html(html_pretty);
|
||||
short_time = data["time"].split(/\s(.+)/)[1];
|
||||
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
||||
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
||||
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||
}
|
||||
|
||||
function start_update() {
|
||||
|
||||
(function statsworker() {
|
||||
$.ajax({
|
||||
url: "/stats",
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
update_stats(data);
|
||||
},
|
||||
complete: function() {
|
||||
setTimeout(statsworker, 10000);
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
start_charts();
|
||||
|
||||
$("#toggleStats").click(function() {
|
||||
$("#jsonstats").fadeToggle(1000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
background: #eeeeee;
|
||||
margin: 2em;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
font-family: system-ui, sans-serif;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 2em;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: 2em;
|
||||
height: 80vh;
|
||||
}
|
||||
footer {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
height: 10vh;
|
||||
}
|
||||
#graphs {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
#graphs_center {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
#left {
|
||||
margin-right: 2px;
|
||||
height: 300px;
|
||||
}
|
||||
#right {
|
||||
height: 300px;
|
||||
}
|
||||
#center {
|
||||
height: 300px;
|
||||
}
|
||||
#messageChart, #emailChart, #memChart {
|
||||
border: 1px solid #ccc;
|
||||
background: #ddd;
|
||||
}
|
||||
#stats {
|
||||
margin: auto;
|
||||
width: 80%;
|
||||
}
|
||||
#jsonstats {
|
||||
display: none;
|
||||
}
|
||||
#title {
|
||||
font-size: 4em;
|
||||
}
|
||||
#uptime, #aprsis {
|
||||
font-size: 1em;
|
||||
}
|
||||
#callsign {
|
||||
font-size: 1.4em;
|
||||
color: #00F;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div id="title">APRSD version <span id="version"></div></div>
|
||||
<div id="callsign">{{ callsign }}</div>
|
||||
<div id="aprsis"></div>
|
||||
<div id="uptime"></div>
|
||||
</header>
|
||||
|
||||
<div id="main">
|
||||
<div id="graphs">
|
||||
<div id="left"><canvas id="messageChart"></canvas></div>
|
||||
<div id="right"><canvas class="right" id="emailChart"></canvas></div>
|
||||
</div>
|
||||
<div id="graphs_center">
|
||||
<div id="center"><canvas id="memChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div id="stats">
|
||||
<button id="toggleStats">Toggle raw json</button>
|
||||
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user