1
0
mirror of https://github.com/craigerl/aprsd.git synced 2024-12-22 09:31:42 -05:00

Allow stats collector to serialize upon creation

This does some cleanup with the stats collector and
usage of the stats.  The patch adds a new optional
param to the collector's collect() method to tell
the object to provide serializable stats.  This is
used for the webchat app that sends stats to the
browser.
This commit is contained in:
Hemna 2024-04-02 14:07:37 -04:00
parent e2e58530b2
commit 71d72adf06
15 changed files with 132 additions and 49 deletions

View File

@ -28,7 +28,7 @@ factory = None
@singleton @singleton
class APRSClientStats: class APRSClientStats:
def stats(self): def stats(self, serializable=False):
client = factory.create() client = factory.create()
stats = { stats = {
"transport": client.transport(), "transport": client.transport(),
@ -38,7 +38,10 @@ class APRSClientStats:
if client.transport() == TRANSPORT_APRSIS: if client.transport() == TRANSPORT_APRSIS:
stats["server_string"] = client.client.server_string stats["server_string"] = client.client.server_string
stats["sever_keepalive"] = client.client.aprsd_keepalive keepalive = client.client.aprsd_keepalive
if keepalive:
keepalive = keepalive.isoformat()
stats["sever_keepalive"] = keepalive
elif client.transport() == TRANSPORT_TCPKISS: elif client.transport() == TRANSPORT_TCPKISS:
stats["host"] = CONF.kiss_tcp.host stats["host"] = CONF.kiss_tcp.host
stats["port"] = CONF.kiss_tcp.port stats["port"] = CONF.kiss_tcp.port
@ -96,7 +99,9 @@ class Client:
def reset(self): def reset(self):
"""Call this to force a rebuild/reconnect.""" """Call this to force a rebuild/reconnect."""
LOG.info("Resetting client connection.")
if self._client: if self._client:
self._client.close()
del self._client del self._client
self._create_client() self._create_client()
else: else:
@ -131,6 +136,10 @@ class Client:
def is_alive(self): def is_alive(self):
pass pass
@abc.abstractmethod
def close(self):
pass
class APRSISClient(Client): class APRSISClient(Client):
@ -195,6 +204,11 @@ class APRSISClient(Client):
LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!") LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
return False return False
def close(self):
if self._client:
self._client.stop()
self._client.close()
@staticmethod @staticmethod
def transport(): def transport():
return TRANSPORT_APRSIS return TRANSPORT_APRSIS
@ -239,7 +253,6 @@ class APRSISClient(Client):
return aprs_client return aprs_client
def consumer(self, callback, blocking=False, immortal=False, raw=False): def consumer(self, callback, blocking=False, immortal=False, raw=False):
if self.is_alive():
self._client.consumer( self._client.consumer(
callback, blocking=blocking, callback, blocking=blocking,
immortal=immortal, raw=raw, immortal=immortal, raw=raw,
@ -296,6 +309,10 @@ class KISSClient(Client):
else: else:
return False return False
def close(self):
if self._client:
self._client.stop()
@staticmethod @staticmethod
def transport(): def transport():
if CONF.kiss_serial.enabled: if CONF.kiss_serial.enabled:
@ -350,6 +367,9 @@ class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
def is_alive(self): def is_alive(self):
return True return True
def close(self):
pass
def setup_connection(self): def setup_connection(self):
self.connected = True self.connected = True
return fake.APRSDFakeClient() return fake.APRSDFakeClient()

View File

@ -63,7 +63,7 @@ def signal_handler(sig, frame):
time.sleep(1.5) time.sleep(1.5)
# packets.WatchList().save() # packets.WatchList().save()
# packets.SeenList().save() # packets.SeenList().save()
LOG.info(stats.APRSDStats()) LOG.info(stats.stats_collector.collect())
LOG.info("Telling flask to bail.") LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0)) signal.signal(signal.SIGTERM, sys.exit(0))
@ -378,7 +378,7 @@ def _get_transport(stats):
transport = "aprs-is" transport = "aprs-is"
aprs_connection = ( aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >" "APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"]) "{}</a>".format(stats["APRSClientStats"]["server_string"])
) )
elif client.KISSClient.is_enabled(): elif client.KISSClient.is_enabled():
transport = client.KISSClient.transport() transport = client.KISSClient.transport()
@ -414,12 +414,13 @@ def location(callsign):
@flask_app.route("/") @flask_app.route("/")
def index(): def index():
stats = _stats() stats = _stats()
LOG.error(stats)
# For development # For development
html_template = "index.html" html_template = "index.html"
LOG.debug(f"Template {html_template}") LOG.debug(f"Template {html_template}")
transport, aprs_connection = _get_transport(stats) transport, aprs_connection = _get_transport(stats["stats"])
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}") LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
stats["transport"] = transport stats["transport"] = transport
@ -454,18 +455,17 @@ def send_message_status():
def _stats(): def _stats():
stats_obj = stats.APRSDStats()
now = datetime.datetime.now() now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S" time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats() stats_dict = stats.stats_collector.collect(serializable=True)
# Webchat doesnt need these # Webchat doesnt need these
if "watch_list" in stats_dict["aprsd"]: if "WatchList" in stats_dict:
del stats_dict["aprsd"]["watch_list"] del stats_dict["WatchList"]
if "seen_list" in stats_dict["aprsd"]: if "seen_list" in stats_dict:
del stats_dict["aprsd"]["seen_list"] del stats_dict["seen_list"]
if "threads" in stats_dict["aprsd"]: if "APRSDThreadList" in stats_dict:
del stats_dict["aprsd"]["threads"] del stats_dict["APRSDThreadList"]
# del stats_dict["email"] # del stats_dict["email"]
# del stats_dict["plugins"] # del stats_dict["plugins"]
# del stats_dict["messages"] # del stats_dict["messages"]
@ -544,7 +544,7 @@ class SendMessageNamespace(Namespace):
LOG.debug(f"Long {long}") LOG.debug(f"Long {long}")
tx.send( tx.send(
packets.GPSPacket( packets.BeaconPacket(
from_call=CONF.callsign, from_call=CONF.callsign,
to_call="APDW16", to_call="APDW16",
latitude=lat, latitude=lat,

View File

@ -218,6 +218,11 @@ class BulletinPacket(Packet):
bid: Optional[str] = field(default="1") bid: Optional[str] = field(default="1")
message_text: Optional[str] = field(default=None) message_text: Optional[str] = field(default=None)
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
return f"{self.from_call}:BLN{self.bid}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
return f"BLN{self.bid} {self.message_text}" return f"BLN{self.bid} {self.message_text}"
@ -385,6 +390,14 @@ class BeaconPacket(GPSPacket):
f"{self.payload}" f"{self.payload}"
) )
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
if self.raw_timestamp:
return f"{self.from_call}:{self.raw_timestamp}"
else:
return f"{self.from_call}:{self.human_info.replace(' ','')}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
h_str = [] h_str = []
@ -407,6 +420,11 @@ class MicEPacket(GPSPacket):
# 0 to 360 # 0 to 360
course: int = 0 course: int = 0
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
return f"{self.from_call}:{self.human_info.replace(' ', '')}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
h_info = super().human_info h_info = super().human_info
@ -428,6 +446,14 @@ class TelemetryPacket(GPSPacket):
# 0 to 360 # 0 to 360
course: int = 0 course: int = 0
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
if self.raw_timestamp:
return f"{self.from_call}:{self.raw_timestamp}"
else:
return f"{self.from_call}:{self.human_info.replace(' ','')}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
h_info = super().human_info h_info = super().human_info
@ -548,6 +574,14 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
raw = cls._translate(cls, kvs) # type: ignore raw = cls._translate(cls, kvs) # type: ignore
return super().from_dict(raw) return super().from_dict(raw)
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
if self.raw_timestamp:
return f"{self.from_call}:{self.raw_timestamp}"
elif self.wx_raw_timestamp:
return f"{self.from_call}:{self.wx_raw_timestamp}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
h_str = [] h_str = []
@ -643,6 +677,11 @@ class ThirdPartyPacket(Packet, DataClassJsonMixin):
obj.subpacket = factory(obj.subpacket) # type: ignore obj.subpacket = factory(obj.subpacket) # type: ignore
return obj return obj
@property
def key(self) -> str:
"""Build a key for finding this packet in a dict."""
return f"{self.from_call}:{self.subpacket.key}"
@property @property
def human_info(self) -> str: def human_info(self) -> str:
sub_info = self.subpacket.human_info sub_info = self.subpacket.human_info
@ -772,8 +811,7 @@ def factory(raw_packet: dict[Any, Any]) -> type[Packet]:
if "latitude" in raw: if "latitude" in raw:
packet_class = GPSPacket packet_class = GPSPacket
else: else:
LOG.warning(f"Unknown packet type {packet_type}") # LOG.warning(raw)
LOG.warning(raw)
packet_class = UnknownPacket packet_class = UnknownPacket
raw.get("addresse", raw.get("to_call")) raw.get("addresse", raw.get("to_call"))

View File

@ -95,7 +95,7 @@ class PacketList(MutableMapping):
def total_tx(self): def total_tx(self):
return self._total_tx return self._total_tx
def stats(self) -> dict: def stats(self, serializable=False) -> dict:
stats = { stats = {
"total_tracked": self.total_tx() + self.total_rx(), "total_tracked": self.total_tx() + self.total_rx(),
"rx": self.total_rx(), "rx": self.total_rx(),

View File

@ -58,15 +58,17 @@ class PacketTrack(objectstore.ObjectStoreMixin):
return self.data.values() return self.data.values()
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def stats(self): def stats(self, serializable=False):
stats = { stats = {
"total_tracked": self.total_tracked, "total_tracked": self.total_tracked,
} }
pkts = {} pkts = {}
for key in self.data: for key in self.data:
last_send_time = self.data[key].last_send_time
last_send_attempt = self.data[key]._last_send_attempt
pkts[key] = { pkts[key] = {
"last_send_time": self.data[key].last_send_time, "last_send_time": last_send_time,
"last_send_attempt": self.data[key]._last_send_attempt, "last_send_attempt": last_send_attempt,
"retry_count": self.data[key].retry_count, "retry_count": self.data[key].retry_count,
"message": self.data[key].raw, "message": self.data[key].raw,
} }

View File

@ -43,7 +43,7 @@ class WatchList(objectstore.ObjectStoreMixin):
} }
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def stats(self) -> dict: def stats(self, serializable=False) -> dict:
stats = {} stats = {}
for callsign in self.data: for callsign in self.data:
stats[callsign] = { stats[callsign] = {

View File

@ -344,7 +344,7 @@ class PluginManager:
self._watchlist_pm = pluggy.PluginManager("aprsd") self._watchlist_pm = pluggy.PluginManager("aprsd")
self._watchlist_pm.add_hookspecs(APRSDPluginSpec) self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
def stats(self) -> dict: def stats(self, serializable=False) -> dict:
"""Collect and return stats for all plugins.""" """Collect and return stats for all plugins."""
def full_name_with_qualname(obj): def full_name_with_qualname(obj):
return "{}.{}".format( return "{}.{}".format(

View File

@ -68,12 +68,15 @@ class EmailStats:
rx = 0 rx = 0
email_thread_last_time = None email_thread_last_time = None
def stats(self): def stats(self, serializable=False):
if CONF.email_plugin.enabled: if CONF.email_plugin.enabled:
last_check_time = self.email_thread_last_time
if serializable and last_check_time:
last_check_time = last_check_time.isoformat()
stats = { stats = {
"tx": self.tx, "tx": self.tx,
"rx": self.rx, "rx": self.rx,
"last_check_time": self.email_thread_last_time, "last_check_time": last_check_time,
} }
else: else:
stats = {} stats = {}

View File

@ -30,11 +30,14 @@ class APRSDStats:
def uptime(self): def uptime(self):
return datetime.datetime.now() - self.start_time return datetime.datetime.now() - self.start_time
def stats(self) -> dict: def stats(self, serializable=False) -> dict:
current, peak = tracemalloc.get_traced_memory() current, peak = tracemalloc.get_traced_memory()
uptime = self.uptime()
if serializable:
uptime = str(uptime)
stats = { stats = {
"version": aprsd.__version__, "version": aprsd.__version__,
"uptime": self.uptime(), "uptime": uptime,
"callsign": CONF.callsign, "callsign": CONF.callsign,
"memory_current": int(current), "memory_current": int(current),
"memory_current_str": utils.human_size(current), "memory_current_str": utils.human_size(current),

View File

@ -5,7 +5,8 @@ from aprsd.utils import singleton
class StatsProducer(Protocol): class StatsProducer(Protocol):
"""The StatsProducer protocol is used to define the interface for collecting stats.""" """The StatsProducer protocol is used to define the interface for collecting stats."""
def stats(self) -> dict: def stats(self, serializeable=False) -> dict:
"""provide stats in a dictionary format."""
... ...
@ -15,11 +16,11 @@ class Collector:
def __init__(self): def __init__(self):
self.producers: dict[str, StatsProducer] = {} self.producers: dict[str, StatsProducer] = {}
def collect(self): def collect(self, serializable=False) -> dict:
stats = {} stats = {}
for name, producer in self.producers.items(): for name, producer in self.producers.items():
# No need to put in empty stats # No need to put in empty stats
tmp_stats = producer.stats() tmp_stats = producer.stats(serializable=serializable)
if tmp_stats: if tmp_stats:
stats[name] = tmp_stats stats[name] = tmp_stats
return stats return stats

View File

@ -13,7 +13,7 @@ LOG = logging.getLogger("APRSD")
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta): class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
"""Base class for all threads in APRSD.""" """Base class for all threads in APRSD."""
loop_interval = 1 loop_count = 1
def __init__(self, name): def __init__(self, name):
super().__init__(name=name) super().__init__(name=name)
@ -49,7 +49,6 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
while not self._should_quit(): while not self._should_quit():
self.loop_count += 1 self.loop_count += 1
can_loop = self.loop() can_loop = self.loop()
self.loop_interval += 1
self._last_loop = datetime.datetime.now() self._last_loop = datetime.datetime.now()
if not can_loop: if not can_loop:
self.stop() self.stop()
@ -72,9 +71,12 @@ class APRSDThreadList:
cls.threads_list = [] cls.threads_list = []
return cls._instance return cls._instance
def stats(self) -> dict: def stats(self, serializable=False) -> dict:
stats = {} stats = {}
for th in self.threads_list: for th in self.threads_list:
age = th.loop_age()
if serializable:
age = str(age)
stats[th.__class__.__name__] = { stats[th.__class__.__name__] = {
"name": th.name, "name": th.name,
"alive": th.is_alive(), "alive": th.is_alive(),

View File

@ -116,5 +116,6 @@ class KeepAliveThread(APRSDThread):
level, msg = utils._check_version() level, msg = utils._check_version()
if level: if level:
LOG.warning(msg) LOG.warning(msg)
self.cntr += 1
time.sleep(1) time.sleep(1)
return True return True

View File

@ -6,7 +6,7 @@ import time
import aprslib import aprslib
from oslo_config import cfg from oslo_config import cfg
from aprsd import client, packets, plugin, stats from aprsd import client, packets, plugin
from aprsd.packets import log as packet_log from aprsd.packets import log as packet_log
from aprsd.threads import APRSDThread, tx from aprsd.threads import APRSDThread, tx
@ -27,7 +27,7 @@ class APRSDRXThread(APRSDThread):
self._client.stop() self._client.stop()
def loop(self): def loop(self):
LOG.debug(f"RX_MSG-LOOP {self.loop_interval}") LOG.debug(f"RX_MSG-LOOP {self.loop_count}")
if not self._client: if not self._client:
self._client = client.factory.create() self._client = client.factory.create()
time.sleep(1) time.sleep(1)
@ -53,21 +53,21 @@ class APRSDRXThread(APRSDThread):
aprslib.exceptions.ConnectionError, aprslib.exceptions.ConnectionError,
): ):
LOG.error("Connection dropped, reconnecting") LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs # Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client() # This will cause a reconnect, next time client.get_client()
# is called # is called
self._client.reset() self._client.reset()
except Exception as ex: time.sleep(5)
LOG.error("Something bad happened!!!") except Exception:
LOG.exception(ex) # LOG.exception(ex)
return False LOG.error("Resetting connection and trying again.")
self._client.reset()
time.sleep(5)
# Continue to loop # Continue to loop
return True return True
def _process_packet(self, *args, **kwargs): def _process_packet(self, *args, **kwargs):
"""Intermediate callback so we can update the keepalive time.""" """Intermediate callback so we can update the keepalive time."""
stats.APRSDStats().set_aprsis_keepalive()
# Now call the 'real' packet processing for a RX'x packet # Now call the 'real' packet processing for a RX'x packet
self.process_packet(*args, **kwargs) self.process_packet(*args, **kwargs)

View File

@ -77,7 +77,11 @@ def _send_direct(packet, aprs_client=None):
packet.update_timestamp() packet.update_timestamp()
packet_log.log(packet, tx=True) packet_log.log(packet, tx=True)
try:
cl.send(packet) cl.send(packet)
except Exception as e:
LOG.error(f"Failed to send packet: {packet}")
LOG.error(e)
class SendPacketThread(aprsd_threads.APRSDThread): class SendPacketThread(aprsd_threads.APRSDThread):
@ -232,7 +236,15 @@ class BeaconSendThread(aprsd_threads.APRSDThread):
comment="APRSD GPS Beacon", comment="APRSD GPS Beacon",
symbol=CONF.beacon_symbol, symbol=CONF.beacon_symbol,
) )
try:
# Only send it once
pkt.retry_count = 1
send(pkt, direct=True) send(pkt, direct=True)
except Exception as e:
LOG.error(f"Failed to send beacon: {e}")
client.factory.create().reset()
time.sleep(5)
self._loop_cnt += 1 self._loop_cnt += 1
time.sleep(1) time.sleep(1)
return True return True

View File

@ -19,9 +19,10 @@ function show_aprs_icon(item, symbol) {
function ord(str){return str.charCodeAt(0);} function ord(str){return str.charCodeAt(0);}
function update_stats( data ) { function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] ); console.log(data);
$("#version").text( data["stats"]["APRSDStats"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] ); $("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); $("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] );
short_time = data["time"].split(/\s(.+)/)[1]; short_time = data["time"].split(/\s(.+)/)[1];
} }