From d863474c133d7650f9c79d1a88f0a4a04a2b056f Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 31 Oct 2024 09:17:36 -0400 Subject: [PATCH 1/4] Added some changes to listen to collect stats and only show those stats during listen --- aprsd/cmds/listen.py | 85 +++++++++++++++++++++++++++++++++++++++- aprsd/stats/collector.py | 5 +++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py index 646d3c9..60f48c5 100644 --- a/aprsd/cmds/listen.py +++ b/aprsd/cmds/listen.py @@ -10,12 +10,13 @@ import sys import time import click +from loguru import logger from oslo_config import cfg from rich.console import Console # local imports here import aprsd -from aprsd import cli_helper, packets, plugin, threads +from aprsd import cli_helper, packets, plugin, threads, utils from aprsd.client import client_factory from aprsd.main import cli from aprsd.packets import collector as packet_collector @@ -24,12 +25,14 @@ from aprsd.packets import seen_list from aprsd.stats import collector from aprsd.threads import keep_alive, rx from aprsd.threads import stats as stats_thread +from aprsd.threads.aprsd import APRSDThread # setup the global logger # log.basicConfig(level=log.DEBUG) # level=10 LOG = logging.getLogger("APRSD") CONF = cfg.CONF +LOGU = logger console = Console() @@ -46,6 +49,41 @@ def signal_handler(sig, frame): collector.Collector().collect() +@utils.singleton +class SimplePacketStats: + def __init__(self): + self.total_rx = 0 + self.total_tx = 0 + self.types = {} + + def rx(self, packet): + self.total_rx += 1 + ptype = packet.__class__.__name__ + if ptype not in self.types: + self.types[ptype] = {"tx": 0, "rx": 0} + self.types[ptype]["rx"] += 1 + + def tx(self, packet): + self.total_tx += 1 + ptype = packet.__class__.__name__ + if ptype not in self.types: + self.types[ptype] = {"tx": 0, "rx": 0} + self.types[ptype]["tx"] += 1 + + def flush(self): + pass + + def load(self): + pass + + def stats(self, serializable=False): + return { + "total_rx": self.total_rx, + "total_tx": self.total_tx, + "types": self.types, + } + + class APRSDListenThread(rx.APRSDRXThread): def __init__(self, packet_queue, packet_filter=None, plugin_manager=None): super().__init__(packet_queue) @@ -88,6 +126,28 @@ class APRSDListenThread(rx.APRSDRXThread): packet_collector.PacketCollector().rx(packet) +class ListenStatsThread(APRSDThread): + def __init__(self): + super().__init__("SimpleStats") + self._last_total_rx = 0 + + def loop(self): + if self.loop_count % 10 == 0: + # log the stats every 10 seconds + stats_json = collector.Collector().collect() + stats = stats_json["SimplePacketStats"] + total_rx = stats["total_rx"] + rate = (total_rx - self._last_total_rx) / 10 + LOG.warning(f"RX Rate: {rate} pps Total RX: {total_rx} - {self._last_total_rx}") + #LOG.error(stats) + self._last_total_rx = total_rx + for k, v in stats["types"].items(): + LOGU.opt(colors=True).warning(f"Type: {k} RX: {v['rx']} TX: {v['tx']}") + + time.sleep(1) + return True + + @cli.command() @cli_helper.add_options(cli_helper.common_options) @click.option( @@ -201,6 +261,27 @@ def listen( # just deregister the class from the packet collector packet_collector.PacketCollector().unregister(seen_list.SeenList) + packet_collector.PacketCollector().register(SimplePacketStats) + + from aprsd.client import stats as client_stats + from aprsd.packets.packet_list import PacketList # noqa: F401 + from aprsd.packets.seen_list import SeenList # noqa: F401 + from aprsd.packets.tracker import PacketTrack # noqa: F401 + from aprsd.packets.watch_list import WatchList # noqa: F401 + from aprsd.plugins import email + from aprsd.threads import aprsd as aprsd_thread + c = collector.Collector() + # c.unregister_producer(app.APRSDStats) + c.unregister_producer(PacketList) + c.unregister_producer(WatchList) + #c.unregister_producer(PacketTrack) + c.unregister_producer(plugin.PluginManager) + c.unregister_producer(aprsd_thread.APRSDThreadList) + c.unregister_producer(email.EmailStats) + c.unregister_producer(client_stats.APRSClientStats) + c.unregister_producer(seen_list.SeenList) + c.register_producer(SimplePacketStats) + pm = None pm = plugin.PluginManager() if load_plugins: @@ -222,6 +303,8 @@ def listen( ) LOG.debug("Start APRSDListenThread") listen_thread.start() + listen_stats = ListenStatsThread() + listen_stats.start() keepalive.start() LOG.debug("keepalive Join") diff --git a/aprsd/stats/collector.py b/aprsd/stats/collector.py index c3da428..d899031 100644 --- a/aprsd/stats/collector.py +++ b/aprsd/stats/collector.py @@ -35,3 +35,8 @@ class Collector: if not isinstance(producer_name, StatsProducer): raise TypeError(f"Producer {producer_name} is not a StatsProducer") self.producers.append(producer_name) + + def unregister_producer(self, producer_name: Callable): + if not isinstance(producer_name, StatsProducer): + raise TypeError(f"Producer {producer_name} is not a StatsProducer") + self.producers.remove(producer_name) From fbfac97140932046921a3aa60c496133f4a368e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 00:26:57 +0000 Subject: [PATCH 2/4] Bump werkzeug from 3.0.4 to 3.0.6 Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.4 to 3.0.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.0.4...3.0.6) --- updated-dependencies: - dependency-name: werkzeug dependency-type: indirect ... Signed-off-by: dependabot[bot] --- requirements.txt | 225 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 66 deletions(-) diff --git a/requirements.txt b/requirements.txt index f648c6e..46c5903 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,69 +4,162 @@ # # pip-compile --annotation-style=line requirements.in # -aprslib==0.7.2 # via -r requirements.in -attrs==24.2.0 # via ax253, kiss3, rush -ax253==0.1.5.post1 # via kiss3 -beautifulsoup4==4.12.3 # via -r requirements.in -bidict==0.23.1 # via python-socketio -bitarray==3.0.0 # via ax253, kiss3 -blinker==1.8.2 # via flask -certifi==2024.8.30 # via requests -charset-normalizer==3.4.0 # via requests -click==8.1.7 # via -r requirements.in, click-params, flask -click-params==0.5.0 # via -r requirements.in -commonmark==0.9.1 # via rich -dataclasses==0.6 # via -r requirements.in -dataclasses-json==0.6.7 # via -r requirements.in -debtcollector==3.0.0 # via oslo-config -deprecated==1.2.14 # via click-params -flask==3.0.3 # via -r requirements.in, flask-httpauth, flask-socketio -flask-httpauth==4.8.0 # via -r requirements.in -flask-socketio==5.4.1 # via -r requirements.in -geographiclib==2.0 # via geopy -geopy==2.4.1 # via -r requirements.in -h11==0.14.0 # via wsproto -idna==3.10 # via requests -imapclient==3.0.1 # via -r requirements.in -importlib-metadata==8.5.0 # via ax253, kiss3 -itsdangerous==2.2.0 # via flask -jinja2==3.1.4 # via flask -kiss3==8.0.0 # via -r requirements.in -loguru==0.7.2 # via -r requirements.in -markupsafe==3.0.2 # via jinja2, werkzeug -marshmallow==3.23.0 # via dataclasses-json -mypy-extensions==1.0.0 # via typing-inspect -netaddr==1.3.0 # via oslo-config -oslo-config==9.6.0 # via -r requirements.in -oslo-i18n==6.4.0 # via oslo-config -packaging==24.1 # via marshmallow -pbr==6.1.0 # via oslo-i18n, stevedore -pluggy==1.5.0 # via -r requirements.in -pygments==2.18.0 # via rich -pyserial==3.5 # via pyserial-asyncio -pyserial-asyncio==0.6 # via kiss3 -python-engineio==4.10.1 # via python-socketio -python-socketio==5.11.4 # via -r requirements.in, flask-socketio -pytz==2024.2 # via -r requirements.in -pyyaml==6.0.2 # via -r requirements.in, oslo-config -requests==2.32.3 # via -r requirements.in, oslo-config, update-checker -rfc3986==2.0.0 # via oslo-config -rich==12.6.0 # via -r requirements.in -rush==2021.4.0 # via -r requirements.in -shellingham==1.5.4 # via -r requirements.in -simple-websocket==1.1.0 # via python-engineio -six==1.16.0 # via -r requirements.in -soupsieve==2.6 # via beautifulsoup4 -stevedore==5.3.0 # via oslo-config -tabulate==0.9.0 # via -r requirements.in -thesmuggler==1.0.1 # via -r requirements.in -typing-extensions==4.12.2 # via typing-inspect -typing-inspect==0.9.0 # via dataclasses-json -tzlocal==5.2 # via -r requirements.in -update-checker==0.18.0 # via -r requirements.in -urllib3==2.2.3 # via requests -validators==0.22.0 # via click-params -werkzeug==3.0.4 # via flask -wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated -wsproto==1.2.0 # via simple-websocket -zipp==3.20.2 # via importlib-metadata +aprslib==0.7.2 + # via -r requirements.in +attrs==24.2.0 + # via + # ax253 + # kiss3 + # rush +ax253==0.1.5.post1 + # via kiss3 +beautifulsoup4==4.12.3 + # via -r requirements.in +bidict==0.23.1 + # via python-socketio +bitarray==3.0.0 + # via + # ax253 + # kiss3 +blinker==1.8.2 + # via flask +certifi==2024.8.30 + # via requests +charset-normalizer==3.4.0 + # via requests +click==8.1.7 + # via + # -r requirements.in + # click-params + # flask +click-params==0.5.0 + # via -r requirements.in +commonmark==0.9.1 + # via rich +dataclasses==0.6 + # via -r requirements.in +dataclasses-json==0.6.7 + # via -r requirements.in +debtcollector==3.0.0 + # via oslo-config +deprecated==1.2.14 + # via click-params +flask==3.0.3 + # via + # -r requirements.in + # flask-httpauth + # flask-socketio +flask-httpauth==4.8.0 + # via -r requirements.in +flask-socketio==5.4.1 + # via -r requirements.in +geographiclib==2.0 + # via geopy +geopy==2.4.1 + # via -r requirements.in +h11==0.14.0 + # via wsproto +idna==3.10 + # via requests +imapclient==3.0.1 + # via -r requirements.in +importlib-metadata==8.5.0 + # via + # ax253 + # kiss3 +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via flask +kiss3==8.0.0 + # via -r requirements.in +loguru==0.7.2 + # via -r requirements.in +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +marshmallow==3.23.0 + # via dataclasses-json +mypy-extensions==1.0.0 + # via typing-inspect +netaddr==1.3.0 + # via oslo-config +oslo-config==9.6.0 + # via -r requirements.in +oslo-i18n==6.4.0 + # via oslo-config +packaging==24.1 + # via marshmallow +pbr==6.1.0 + # via + # oslo-i18n + # stevedore +pluggy==1.5.0 + # via -r requirements.in +pygments==2.18.0 + # via rich +pyserial==3.5 + # via pyserial-asyncio +pyserial-asyncio==0.6 + # via kiss3 +python-engineio==4.10.1 + # via python-socketio +python-socketio==5.11.4 + # via + # -r requirements.in + # flask-socketio +pytz==2024.2 + # via -r requirements.in +pyyaml==6.0.2 + # via + # -r requirements.in + # oslo-config +requests==2.32.3 + # via + # -r requirements.in + # oslo-config + # update-checker +rfc3986==2.0.0 + # via oslo-config +rich==12.6.0 + # via -r requirements.in +rush==2021.4.0 + # via -r requirements.in +shellingham==1.5.4 + # via -r requirements.in +simple-websocket==1.1.0 + # via python-engineio +six==1.16.0 + # via -r requirements.in +soupsieve==2.6 + # via beautifulsoup4 +stevedore==5.3.0 + # via oslo-config +tabulate==0.9.0 + # via -r requirements.in +thesmuggler==1.0.1 + # via -r requirements.in +typing-extensions==4.12.2 + # via typing-inspect +typing-inspect==0.9.0 + # via dataclasses-json +tzlocal==5.2 + # via -r requirements.in +update-checker==0.18.0 + # via -r requirements.in +urllib3==2.2.3 + # via requests +validators==0.22.0 + # via click-params +werkzeug==3.0.6 + # via flask +wrapt==1.16.0 + # via + # -r requirements.in + # debtcollector + # deprecated +wsproto==1.2.0 + # via simple-websocket +zipp==3.20.2 + # via importlib-metadata From 3fd606946db8df368da0cd0e9f6e9a0c18d36525 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 31 Oct 2024 17:39:04 -0400 Subject: [PATCH 3/4] Fix a small issue with packet sending failures When a packet _send_direct() failed to send due to a network timeout or client issue, we don't want to count that as a send attempt for the packet. This patch catches that and allows for another retry. --- aprsd/cmds/admin.py | 5 ++++- aprsd/threads/tx.py | 30 ++++++++++++++++++++++++++---- aprsd/wsgi.py | 1 - 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/aprsd/cmds/admin.py b/aprsd/cmds/admin.py index cc77bed..6f3a8d9 100644 --- a/aprsd/cmds/admin.py +++ b/aprsd/cmds/admin.py @@ -14,7 +14,10 @@ from aprsd.main import cli os.environ["APRSD_ADMIN_COMMAND"] = "1" -from aprsd import wsgi as aprsd_wsgi +# this import has to happen AFTER we set the +# above environment variable, so that the code +# inside the wsgi.py has the value +from aprsd import wsgi as aprsd_wsgi # noqa CONF = cfg.CONF diff --git a/aprsd/threads/tx.py b/aprsd/threads/tx.py index 25e61b1..b7ed45b 100644 --- a/aprsd/threads/tx.py +++ b/aprsd/threads/tx.py @@ -89,6 +89,9 @@ def _send_direct(packet, aprs_client=None): except Exception as e: LOG.error(f"Failed to send packet: {packet}") LOG.error(e) + return False + else: + return True class SendPacketThread(aprsd_threads.APRSDThread): @@ -150,8 +153,17 @@ class SendPacketThread(aprsd_threads.APRSDThread): # no attempt time, so lets send it, and start # tracking the time. packet.last_send_time = int(round(time.time())) - _send_direct(packet) - packet.send_count += 1 + sent = False + try: + sent = _send_direct(packet) + except Exception: + LOG.error(f"Failed to send packet: {packet}") + else: + # If an exception happens while sending + # we don't want this attempt to count + # against the packet + if sent: + packet.send_count += 1 time.sleep(1) # Make sure we get called again. @@ -199,8 +211,18 @@ class SendAckThread(aprsd_threads.APRSDThread): send_now = True if send_now: - _send_direct(self.packet) - self.packet.send_count += 1 + sent = False + try: + sent = _send_direct(self.packet) + except Exception: + LOG.error(f"Failed to send packet: {self.packet}") + else: + # If an exception happens while sending + # we don't want this attempt to count + # against the packet + if sent: + self.packet.send_count += 1 + self.packet.last_send_time = int(round(time.time())) time.sleep(1) diff --git a/aprsd/wsgi.py b/aprsd/wsgi.py index 0d7d2d6..47da201 100644 --- a/aprsd/wsgi.py +++ b/aprsd/wsgi.py @@ -269,7 +269,6 @@ def init_app(config_file=None, log_level=None): return log_level -print(f"__name__ = {__name__}") if __name__ == "__main__": async_mode = "threading" From df0ca0448373b1ce5707b01b2acaca9a50340715 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 31 Oct 2024 09:17:36 -0400 Subject: [PATCH 4/4] Added some changes to listen to collect stats and only show those stats during listen --- aprsd/cmds/listen.py | 78 +++++++++----------------------------------- 1 file changed, 15 insertions(+), 63 deletions(-) diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py index 60f48c5..0c2ab0e 100644 --- a/aprsd/cmds/listen.py +++ b/aprsd/cmds/listen.py @@ -49,41 +49,6 @@ def signal_handler(sig, frame): collector.Collector().collect() -@utils.singleton -class SimplePacketStats: - def __init__(self): - self.total_rx = 0 - self.total_tx = 0 - self.types = {} - - def rx(self, packet): - self.total_rx += 1 - ptype = packet.__class__.__name__ - if ptype not in self.types: - self.types[ptype] = {"tx": 0, "rx": 0} - self.types[ptype]["rx"] += 1 - - def tx(self, packet): - self.total_tx += 1 - ptype = packet.__class__.__name__ - if ptype not in self.types: - self.types[ptype] = {"tx": 0, "rx": 0} - self.types[ptype]["tx"] += 1 - - def flush(self): - pass - - def load(self): - pass - - def stats(self, serializable=False): - return { - "total_rx": self.total_rx, - "total_tx": self.total_tx, - "types": self.types, - } - - class APRSDListenThread(rx.APRSDRXThread): def __init__(self, packet_queue, packet_filter=None, plugin_manager=None): super().__init__(packet_queue) @@ -127,22 +92,31 @@ class APRSDListenThread(rx.APRSDRXThread): class ListenStatsThread(APRSDThread): + """Log the stats from the PacketList.""" + def __init__(self): - super().__init__("SimpleStats") + super().__init__("SimpleStatsLog") self._last_total_rx = 0 def loop(self): if self.loop_count % 10 == 0: # log the stats every 10 seconds stats_json = collector.Collector().collect() - stats = stats_json["SimplePacketStats"] - total_rx = stats["total_rx"] + stats = stats_json["PacketList"] + total_rx = stats["rx"] rate = (total_rx - self._last_total_rx) / 10 - LOG.warning(f"RX Rate: {rate} pps Total RX: {total_rx} - {self._last_total_rx}") - #LOG.error(stats) + LOGU.opt(colors=True).info( + f"RX Rate: {rate} pps " + f"Total RX: {total_rx} " + f"RX Last 10 secs: {total_rx - self._last_total_rx}", + ) self._last_total_rx = total_rx for k, v in stats["types"].items(): - LOGU.opt(colors=True).warning(f"Type: {k} RX: {v['rx']} TX: {v['tx']}") + thread_hex = f"fg {utils.hex_from_name(k)}" + LOGU.opt(colors=True).info( + f"<{thread_hex}>{k:<15} " + f"RX: {v['rx']} TX: {v['tx']}", + ) time.sleep(1) return True @@ -255,33 +229,11 @@ def listen( aprs_client.set_filter(filter) keepalive = keep_alive.KeepAliveThread() - # keepalive.start() if not CONF.enable_seen_list: # just deregister the class from the packet collector packet_collector.PacketCollector().unregister(seen_list.SeenList) - packet_collector.PacketCollector().register(SimplePacketStats) - - from aprsd.client import stats as client_stats - from aprsd.packets.packet_list import PacketList # noqa: F401 - from aprsd.packets.seen_list import SeenList # noqa: F401 - from aprsd.packets.tracker import PacketTrack # noqa: F401 - from aprsd.packets.watch_list import WatchList # noqa: F401 - from aprsd.plugins import email - from aprsd.threads import aprsd as aprsd_thread - c = collector.Collector() - # c.unregister_producer(app.APRSDStats) - c.unregister_producer(PacketList) - c.unregister_producer(WatchList) - #c.unregister_producer(PacketTrack) - c.unregister_producer(plugin.PluginManager) - c.unregister_producer(aprsd_thread.APRSDThreadList) - c.unregister_producer(email.EmailStats) - c.unregister_producer(client_stats.APRSClientStats) - c.unregister_producer(seen_list.SeenList) - c.register_producer(SimplePacketStats) - pm = None pm = plugin.PluginManager() if load_plugins: