mirror of
https://github.com/craigerl/aprsd.git
synced 2024-09-28 08:06:37 -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
|
.. image:: https://badge.fury.io/py/aprsd.svg
|
||||||
:target: https://badge.fury.io/py/aprsd
|
: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
|
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
|
||||||
:target: https://github.com/craigerl/aprsd/actions
|
: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 time
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
|
from aprsd import stats
|
||||||
import aprslib
|
import aprslib
|
||||||
from aprslib import is_py3
|
from aprslib import is_py3
|
||||||
from aprslib.exceptions import LoginError
|
from aprslib.exceptions import (
|
||||||
|
ConnectionDrop,
|
||||||
|
ConnectionError,
|
||||||
|
GenericError,
|
||||||
|
LoginError,
|
||||||
|
ParseError,
|
||||||
|
UnknownFormat,
|
||||||
|
)
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -18,6 +26,7 @@ class Client:
|
|||||||
config = None
|
config = None
|
||||||
|
|
||||||
connected = False
|
connected = False
|
||||||
|
server_string = None
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""This magic turns this into a singleton."""
|
"""This magic turns this into a singleton."""
|
||||||
@ -153,7 +162,16 @@ class Aprsdis(aprslib.IS):
|
|||||||
|
|
||||||
self.logger.debug("Server: %s", test)
|
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 == "":
|
if callsign == "":
|
||||||
raise LoginError("Server responded with empty callsign???")
|
raise LoginError("Server responded with empty callsign???")
|
||||||
@ -171,11 +189,67 @@ class Aprsdis(aprslib.IS):
|
|||||||
self.logger.error(str(e))
|
self.logger.error(str(e))
|
||||||
self.close()
|
self.close()
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.close()
|
self.close()
|
||||||
self.logger.error("Failed to login")
|
self.logger.error("Failed to login '{}'".format(e))
|
||||||
raise LoginError("Failed to login")
|
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():
|
def get_client():
|
||||||
cl = Client()
|
cl = Client()
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from logging import NullHandler
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
import sys
|
||||||
|
|
||||||
import aprsd
|
from aprsd import messaging, plugin, stats, utils
|
||||||
from aprsd import messaging, plugin, stats
|
|
||||||
import flask
|
import flask
|
||||||
import flask_classful
|
import flask_classful
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
@ -39,9 +42,14 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
|
|
||||||
users = self.users
|
users = self.users
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
def index(self):
|
def index(self):
|
||||||
return "Hello"
|
stats = self._stats()
|
||||||
# return flask.render_template("index.html", message=msg)
|
return flask.render_template(
|
||||||
|
"index.html",
|
||||||
|
initial_stats=stats,
|
||||||
|
callsign=self.config["aprs"]["login"],
|
||||||
|
)
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def messages(self):
|
def messages(self):
|
||||||
@ -67,29 +75,71 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
track.save()
|
track.save()
|
||||||
return json.dumps({"messages": "saved"})
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
def stats(self):
|
def _stats(self):
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
track = messaging.MsgTrack()
|
track = messaging.MsgTrack()
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
|
|
||||||
|
stats_dict = stats_obj.stats()
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"version": aprsd.__version__,
|
"time": now.strftime(time_format),
|
||||||
"uptime": stats_obj.uptime,
|
|
||||||
"size_tracker": len(track),
|
"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(
|
flask_app = flask.Flask(
|
||||||
"aprsd",
|
"aprsd",
|
||||||
static_url_path="",
|
static_url_path="",
|
||||||
static_folder="web/static",
|
static_folder="web/static",
|
||||||
template_folder="web/templates",
|
template_folder="web/templates",
|
||||||
)
|
)
|
||||||
|
setup_logging(config, flask_app, loglevel, quiet)
|
||||||
server = APRSDFlask()
|
server = APRSDFlask()
|
||||||
server.set_config(config)
|
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("/stats", methods=["GET"])(server.stats)
|
||||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||||
flask_app.route("/save", methods=["GET"])(server.save)
|
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))
|
LOG.error("Email thread is very old! {}".format(d))
|
||||||
sys.exit(-1)
|
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)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,13 +42,6 @@ import click_completion
|
|||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
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"])
|
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
|
# to disable logging to stdout, but still log to file
|
||||||
# use the --quiet option on the cmdln
|
# use the --quiet option on the cmdln
|
||||||
def setup_logging(config, loglevel, quiet):
|
def setup_logging(config, loglevel, quiet):
|
||||||
log_level = LOG_LEVELS[loglevel]
|
log_level = utils.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||||
@ -485,7 +478,7 @@ def server(
|
|||||||
|
|
||||||
if web_enabled:
|
if web_enabled:
|
||||||
flask_enabled = True
|
flask_enabled = True
|
||||||
app = flask.init_flask(config)
|
app = flask.init_flask(config, loglevel, quiet)
|
||||||
app.run(
|
app.run(
|
||||||
host=config["aprsd"]["web"]["host"],
|
host=config["aprsd"]["web"]["host"],
|
||||||
port=config["aprsd"]["web"]["port"],
|
port=config["aprsd"]["web"]["port"],
|
||||||
|
@ -2,6 +2,9 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import aprsd
|
||||||
|
from aprsd import utils
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
@ -12,6 +15,7 @@ class APRSDStats:
|
|||||||
config = None
|
config = None
|
||||||
|
|
||||||
start_time = None
|
start_time = None
|
||||||
|
_aprsis_keepalive = None
|
||||||
|
|
||||||
_msgs_tracked = 0
|
_msgs_tracked = 0
|
||||||
_msgs_tx = 0
|
_msgs_tx = 0
|
||||||
@ -26,12 +30,16 @@ class APRSDStats:
|
|||||||
_email_tx = 0
|
_email_tx = 0
|
||||||
_email_rx = 0
|
_email_rx = 0
|
||||||
|
|
||||||
|
_mem_current = 0
|
||||||
|
_mem_peak = 0
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
# any initializetion here
|
# any initializetion here
|
||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
cls._instance.start_time = datetime.datetime.now()
|
cls._instance.start_time = datetime.datetime.now()
|
||||||
|
cls._instance._aprsis_keepalive = datetime.datetime.now()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
@ -43,6 +51,42 @@ class APRSDStats:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
return str(datetime.datetime.now() - self.start_time)
|
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
|
@property
|
||||||
def msgs_tx(self):
|
def msgs_tx(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@ -126,7 +170,30 @@ class APRSDStats:
|
|||||||
|
|
||||||
def stats(self):
|
def stats(self):
|
||||||
now = datetime.datetime.now()
|
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 = {
|
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": {
|
"messages": {
|
||||||
"tracked": self.msgs_tracked,
|
"tracked": self.msgs_tracked,
|
||||||
"sent": self.msgs_tx,
|
"sent": self.msgs_tx,
|
||||||
@ -136,9 +203,10 @@ class APRSDStats:
|
|||||||
"mic-e recieved": self.msgs_mice_rx,
|
"mic-e recieved": self.msgs_mice_rx,
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
|
"enabled": self.config["aprsd"]["email"]["enabled"],
|
||||||
"sent": self._email_tx,
|
"sent": self._email_tx,
|
||||||
"recieved": self._email_rx,
|
"recieved": self._email_rx,
|
||||||
"thread_last_update": str(now - self._email_thread_last_time),
|
"thread_last_update": last_update,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import abc
|
import abc
|
||||||
import datetime
|
import datetime
|
||||||
|
import gc
|
||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import tracemalloc
|
import tracemalloc
|
||||||
|
|
||||||
from aprsd import client, messaging, plugin, stats, trace
|
from aprsd import client, messaging, plugin, stats, trace, utils
|
||||||
import aprslib
|
import aprslib
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -74,26 +75,33 @@ class KeepAliveThread(APRSDThread):
|
|||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if self.cntr % 6 == 0:
|
if self.cntr % 6 == 0:
|
||||||
|
nuked = gc.collect()
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
last_email = stats.APRSDStats().email_thread_time
|
last_email = stats_obj.email_thread_time
|
||||||
if last_email:
|
if last_email:
|
||||||
email_thread_time = str(now - last_email)
|
email_thread_time = str(now - last_email)
|
||||||
else:
|
else:
|
||||||
email_thread_time = "N/A"
|
email_thread_time = "N/A"
|
||||||
|
|
||||||
|
last_msg_time = str(now - stats_obj.aprsis_keepalive)
|
||||||
|
|
||||||
current, peak = tracemalloc.get_traced_memory()
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
|
stats_obj.set_memory(current)
|
||||||
|
stats_obj.set_memory_peak(peak)
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Uptime ({}) Tracker({}) "
|
"Uptime ({}) Tracker({}) "
|
||||||
"Msgs: TX:{} RX:{} EmailThread: {} RAM: Current:{} Peak:{}".format(
|
"Msgs: TX:{} RX:{} Last: {} - EmailThread: {} - RAM: Current:{} Peak:{} Nuked: {}".format(
|
||||||
stats_obj.uptime,
|
stats_obj.uptime,
|
||||||
len(tracker),
|
len(tracker),
|
||||||
stats_obj.msgs_tx,
|
stats_obj.msgs_tx,
|
||||||
stats_obj.msgs_rx,
|
stats_obj.msgs_rx,
|
||||||
|
last_msg_time,
|
||||||
email_thread_time,
|
email_thread_time,
|
||||||
current,
|
utils.human_size(current),
|
||||||
peak,
|
utils.human_size(peak),
|
||||||
|
nuked,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.cntr += 1
|
self.cntr += 1
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
@ -11,9 +12,17 @@ from aprsd import plugin
|
|||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
LOG_LEVELS = {
|
||||||
|
"CRITICAL": logging.CRITICAL,
|
||||||
|
"ERROR": logging.ERROR,
|
||||||
|
"WARNING": logging.WARNING,
|
||||||
|
"INFO": logging.INFO,
|
||||||
|
"DEBUG": logging.DEBUG,
|
||||||
|
}
|
||||||
|
|
||||||
DEFAULT_LOG_FORMAT = (
|
DEFAULT_LOG_FORMAT = (
|
||||||
"[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]"
|
"[%(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"
|
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||||
@ -37,6 +46,7 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"units": "imperial",
|
"units": "imperial",
|
||||||
"web": {
|
"web": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
|
"logging_enabled": True,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8001,
|
"port": 8001,
|
||||||
"users": {
|
"users": {
|
||||||
@ -351,3 +361,10 @@ def parse_config(config_file):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return config
|
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>
|
<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>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user