aprsd 3.3.4__py2.py3-none-any.whl → 3.4.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aprsd/client.py +133 -20
- aprsd/clients/aprsis.py +6 -3
- aprsd/clients/fake.py +1 -1
- aprsd/clients/kiss.py +1 -1
- aprsd/cmds/completion.py +13 -27
- aprsd/cmds/fetch_stats.py +53 -57
- aprsd/cmds/healthcheck.py +32 -30
- aprsd/cmds/list_plugins.py +2 -2
- aprsd/cmds/listen.py +33 -17
- aprsd/cmds/send_message.py +2 -2
- aprsd/cmds/server.py +26 -9
- aprsd/cmds/webchat.py +34 -29
- aprsd/conf/common.py +46 -31
- aprsd/log/log.py +28 -6
- aprsd/main.py +4 -17
- aprsd/packets/__init__.py +3 -2
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +456 -321
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +83 -66
- aprsd/packets/seen_list.py +30 -19
- aprsd/packets/tracker.py +60 -62
- aprsd/packets/watch_list.py +64 -38
- aprsd/plugin.py +41 -16
- aprsd/plugins/email.py +35 -7
- aprsd/plugins/time.py +3 -2
- aprsd/plugins/version.py +4 -5
- aprsd/plugins/weather.py +0 -1
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +46 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +3 -2
- aprsd/threads/aprsd.py +67 -36
- aprsd/threads/keep_alive.py +55 -49
- aprsd/threads/log_monitor.py +46 -0
- aprsd/threads/rx.py +43 -24
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +36 -17
- aprsd/utils/__init__.py +12 -0
- aprsd/utils/counter.py +6 -3
- aprsd/utils/json.py +20 -0
- aprsd/utils/objectstore.py +22 -17
- aprsd/web/admin/static/css/prism.css +4 -189
- aprsd/web/admin/static/js/charts.js +9 -7
- aprsd/web/admin/static/js/echarts.js +71 -9
- aprsd/web/admin/static/js/main.js +47 -6
- aprsd/web/admin/static/js/prism.js +11 -2246
- aprsd/web/admin/templates/index.html +18 -7
- aprsd/web/chat/static/js/gps.js +3 -1
- aprsd/web/chat/static/js/main.js +4 -3
- aprsd/web/chat/static/js/send-message.js +5 -2
- aprsd/web/chat/templates/index.html +1 -0
- aprsd/wsgi.py +62 -127
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -63
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/WHEEL +1 -1
- aprsd-3.4.0.dist-info/pbr.json +1 -0
- aprsd/plugins/query.py +0 -81
- aprsd/rpc/__init__.py +0 -14
- aprsd/rpc/client.py +0 -165
- aprsd/rpc/server.py +0 -99
- aprsd/stats.py +0 -266
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd-3.3.4.dist-info/pbr.json +0 -1
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
- {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
aprsd/packets/watch_list.py
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
import datetime
|
2
2
|
import logging
|
3
|
-
import threading
|
4
3
|
|
5
4
|
from oslo_config import cfg
|
6
|
-
import wrapt
|
7
5
|
|
8
6
|
from aprsd import utils
|
7
|
+
from aprsd.packets import collector, core
|
9
8
|
from aprsd.utils import objectstore
|
10
9
|
|
11
10
|
|
@@ -17,56 +16,75 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|
17
16
|
"""Global watch list and info for callsigns."""
|
18
17
|
|
19
18
|
_instance = None
|
20
|
-
lock = threading.Lock()
|
21
19
|
data = {}
|
22
20
|
|
23
21
|
def __new__(cls, *args, **kwargs):
|
24
22
|
if cls._instance is None:
|
25
23
|
cls._instance = super().__new__(cls)
|
26
|
-
cls._instance._init_store()
|
27
|
-
cls._instance.data = {}
|
28
24
|
return cls._instance
|
29
25
|
|
30
|
-
def __init__(self
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
26
|
+
def __init__(self):
|
27
|
+
super().__init__()
|
28
|
+
self._update_from_conf()
|
29
|
+
|
30
|
+
def _update_from_conf(self, config=None):
|
31
|
+
with self.lock:
|
32
|
+
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
33
|
+
for callsign in CONF.watch_list.callsigns:
|
34
|
+
call = callsign.replace("*", "")
|
35
|
+
# FIXME(waboring) - we should fetch the last time we saw
|
36
|
+
# a beacon from a callsign or some other mechanism to find
|
37
|
+
# last time a message was seen by aprs-is. For now this
|
38
|
+
# is all we can do.
|
39
|
+
if call not in self.data:
|
40
|
+
self.data[call] = {
|
41
|
+
"last": None,
|
42
|
+
"packet": None,
|
43
|
+
}
|
44
|
+
|
45
|
+
def stats(self, serializable=False) -> dict:
|
46
|
+
stats = {}
|
47
|
+
with self.lock:
|
48
|
+
for callsign in self.data:
|
49
|
+
stats[callsign] = {
|
50
|
+
"last": self.data[callsign]["last"],
|
51
|
+
"packet": self.data[callsign]["packet"],
|
52
|
+
"age": self.age(callsign),
|
53
|
+
"old": self.is_old(callsign),
|
45
54
|
}
|
55
|
+
return stats
|
46
56
|
|
47
57
|
def is_enabled(self):
|
48
58
|
return CONF.watch_list.enabled
|
49
59
|
|
50
60
|
def callsign_in_watchlist(self, callsign):
|
51
|
-
|
61
|
+
with self.lock:
|
62
|
+
return callsign in self.data
|
63
|
+
|
64
|
+
def rx(self, packet: type[core.Packet]) -> None:
|
65
|
+
"""Track when we got a packet from the network."""
|
66
|
+
callsign = packet.from_call
|
52
67
|
|
53
|
-
@wrapt.synchronized(lock)
|
54
|
-
def update_seen(self, packet):
|
55
|
-
if packet.addresse:
|
56
|
-
callsign = packet.addresse
|
57
|
-
else:
|
58
|
-
callsign = packet.from_call
|
59
68
|
if self.callsign_in_watchlist(callsign):
|
60
|
-
self.
|
61
|
-
|
69
|
+
with self.lock:
|
70
|
+
self.data[callsign]["last"] = datetime.datetime.now()
|
71
|
+
self.data[callsign]["packet"] = packet
|
72
|
+
|
73
|
+
def tx(self, packet: type[core.Packet]) -> None:
|
74
|
+
"""We don't care about TX packets."""
|
62
75
|
|
63
76
|
def last_seen(self, callsign):
|
64
|
-
|
65
|
-
|
77
|
+
with self.lock:
|
78
|
+
if self.callsign_in_watchlist(callsign):
|
79
|
+
return self.data[callsign]["last"]
|
66
80
|
|
67
81
|
def age(self, callsign):
|
68
82
|
now = datetime.datetime.now()
|
69
|
-
|
83
|
+
last_seen_time = self.last_seen(callsign)
|
84
|
+
if last_seen_time:
|
85
|
+
return str(now - last_seen_time)
|
86
|
+
else:
|
87
|
+
return None
|
70
88
|
|
71
89
|
def max_delta(self, seconds=None):
|
72
90
|
if not seconds:
|
@@ -83,14 +101,22 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|
83
101
|
We put this here so any notification plugin can use this
|
84
102
|
same test.
|
85
103
|
"""
|
86
|
-
|
104
|
+
if not self.callsign_in_watchlist(callsign):
|
105
|
+
return False
|
87
106
|
|
88
|
-
|
89
|
-
|
107
|
+
age = self.age(callsign)
|
108
|
+
if age:
|
109
|
+
delta = utils.parse_delta_str(age)
|
110
|
+
d = datetime.timedelta(**delta)
|
90
111
|
|
91
|
-
|
112
|
+
max_delta = self.max_delta(seconds=seconds)
|
92
113
|
|
93
|
-
|
94
|
-
|
114
|
+
if d > max_delta:
|
115
|
+
return True
|
116
|
+
else:
|
117
|
+
return False
|
95
118
|
else:
|
96
119
|
return False
|
120
|
+
|
121
|
+
|
122
|
+
collector.PacketCollector().register(WatchList)
|
aprsd/plugin.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
2
3
|
import abc
|
3
4
|
import importlib
|
4
5
|
import inspect
|
@@ -42,7 +43,7 @@ class APRSDPluginSpec:
|
|
42
43
|
"""A hook specification namespace."""
|
43
44
|
|
44
45
|
@hookspec
|
45
|
-
def filter(self, packet: packets.
|
46
|
+
def filter(self, packet: type[packets.Packet]):
|
46
47
|
"""My special little hook that you can customize."""
|
47
48
|
|
48
49
|
|
@@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|
65
66
|
self.threads = self.create_threads() or []
|
66
67
|
self.start_threads()
|
67
68
|
|
68
|
-
def start_threads(self):
|
69
|
+
def start_threads(self) -> None:
|
69
70
|
if self.enabled and self.threads:
|
70
71
|
if not isinstance(self.threads, list):
|
71
72
|
self.threads = [self.threads]
|
@@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|
90
91
|
)
|
91
92
|
|
92
93
|
@property
|
93
|
-
def message_count(self):
|
94
|
+
def message_count(self) -> int:
|
94
95
|
return self.message_counter
|
95
96
|
|
96
|
-
def help(self):
|
97
|
+
def help(self) -> str:
|
97
98
|
return "Help!"
|
98
99
|
|
99
100
|
@abc.abstractmethod
|
@@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|
118
119
|
thread.stop()
|
119
120
|
|
120
121
|
@abc.abstractmethod
|
121
|
-
def filter(self, packet: packets.
|
122
|
+
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
122
123
|
pass
|
123
124
|
|
124
125
|
@abc.abstractmethod
|
125
|
-
def process(self, packet: packets.
|
126
|
+
def process(self, packet: type[packets.Packet]):
|
126
127
|
"""This is called when the filter passes."""
|
127
128
|
|
128
129
|
|
@@ -154,7 +155,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|
154
155
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
155
156
|
|
156
157
|
@hookimpl
|
157
|
-
def filter(self, packet: packets.
|
158
|
+
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
158
159
|
result = packets.NULL_MESSAGE
|
159
160
|
if self.enabled:
|
160
161
|
wl = watch_list.WatchList()
|
@@ -206,14 +207,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|
206
207
|
self.enabled = True
|
207
208
|
|
208
209
|
@hookimpl
|
209
|
-
def filter(self, packet: packets.
|
210
|
-
LOG.
|
210
|
+
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
|
211
|
+
LOG.debug(f"{self.__class__.__name__} called")
|
211
212
|
if not self.enabled:
|
212
213
|
result = f"{self.__class__.__name__} isn't enabled"
|
213
214
|
LOG.warning(result)
|
214
215
|
return result
|
215
216
|
|
216
|
-
if not isinstance(packet, packets.
|
217
|
+
if not isinstance(packet, packets.MessagePacket):
|
217
218
|
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
218
219
|
return packets.NULL_MESSAGE
|
219
220
|
|
@@ -226,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|
226
227
|
# and is an APRS message format and has a message.
|
227
228
|
if (
|
228
229
|
tocall == CONF.callsign
|
229
|
-
and isinstance(packet, packets.
|
230
|
+
and isinstance(packet, packets.MessagePacket)
|
230
231
|
and message
|
231
232
|
):
|
232
233
|
if re.search(self.command_regex, message, re.IGNORECASE):
|
@@ -269,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
|
|
269
270
|
def help(self):
|
270
271
|
return "Help: send APRS help or help <plugin>"
|
271
272
|
|
272
|
-
def process(self, packet: packets.
|
273
|
+
def process(self, packet: packets.MessagePacket):
|
273
274
|
LOG.info("HelpPlugin")
|
274
275
|
# fromcall = packet.get("from")
|
275
276
|
message = packet.message_text
|
@@ -343,6 +344,28 @@ class PluginManager:
|
|
343
344
|
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
344
345
|
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
345
346
|
|
347
|
+
def stats(self, serializable=False) -> dict:
|
348
|
+
"""Collect and return stats for all plugins."""
|
349
|
+
def full_name_with_qualname(obj):
|
350
|
+
return "{}.{}".format(
|
351
|
+
obj.__class__.__module__,
|
352
|
+
obj.__class__.__qualname__,
|
353
|
+
)
|
354
|
+
|
355
|
+
plugin_stats = {}
|
356
|
+
plugins = self.get_plugins()
|
357
|
+
if plugins:
|
358
|
+
|
359
|
+
for p in plugins:
|
360
|
+
plugin_stats[full_name_with_qualname(p)] = {
|
361
|
+
"enabled": p.enabled,
|
362
|
+
"rx": p.rx_count,
|
363
|
+
"tx": p.tx_count,
|
364
|
+
"version": p.version,
|
365
|
+
}
|
366
|
+
|
367
|
+
return plugin_stats
|
368
|
+
|
346
369
|
def is_plugin(self, obj):
|
347
370
|
for c in inspect.getmro(obj):
|
348
371
|
if issubclass(c, APRSDPluginBase):
|
@@ -368,7 +391,9 @@ class PluginManager:
|
|
368
391
|
try:
|
369
392
|
module_name, class_name = module_class_string.rsplit(".", 1)
|
370
393
|
module = importlib.import_module(module_name)
|
371
|
-
|
394
|
+
# Commented out because the email thread starts in a different context
|
395
|
+
# and hence gives a different singleton for the EmailStats
|
396
|
+
# module = importlib.reload(module)
|
372
397
|
except Exception as ex:
|
373
398
|
if not module_name:
|
374
399
|
LOG.error(f"Failed to load Plugin {module_class_string}")
|
@@ -469,12 +494,12 @@ class PluginManager:
|
|
469
494
|
|
470
495
|
LOG.info("Completed Plugin Loading.")
|
471
496
|
|
472
|
-
def run(self, packet: packets.
|
497
|
+
def run(self, packet: packets.MessagePacket):
|
473
498
|
"""Execute all the plugins run method."""
|
474
499
|
with self.lock:
|
475
500
|
return self._pluggy_pm.hook.filter(packet=packet)
|
476
501
|
|
477
|
-
def run_watchlist(self, packet: packets.
|
502
|
+
def run_watchlist(self, packet: packets.Packet):
|
478
503
|
with self.lock:
|
479
504
|
return self._watchlist_pm.hook.filter(packet=packet)
|
480
505
|
|
aprsd/plugins/email.py
CHANGED
@@ -11,7 +11,7 @@ import time
|
|
11
11
|
import imapclient
|
12
12
|
from oslo_config import cfg
|
13
13
|
|
14
|
-
from aprsd import packets, plugin,
|
14
|
+
from aprsd import packets, plugin, threads, utils
|
15
15
|
from aprsd.threads import tx
|
16
16
|
from aprsd.utils import trace
|
17
17
|
|
@@ -60,6 +60,38 @@ class EmailInfo:
|
|
60
60
|
self._delay = val
|
61
61
|
|
62
62
|
|
63
|
+
@utils.singleton
|
64
|
+
class EmailStats:
|
65
|
+
"""Singleton object to store stats related to email."""
|
66
|
+
_instance = None
|
67
|
+
tx = 0
|
68
|
+
rx = 0
|
69
|
+
email_thread_last_time = None
|
70
|
+
|
71
|
+
def stats(self, serializable=False):
|
72
|
+
if CONF.email_plugin.enabled:
|
73
|
+
last_check_time = self.email_thread_last_time
|
74
|
+
if serializable and last_check_time:
|
75
|
+
last_check_time = last_check_time.isoformat()
|
76
|
+
stats = {
|
77
|
+
"tx": self.tx,
|
78
|
+
"rx": self.rx,
|
79
|
+
"last_check_time": last_check_time,
|
80
|
+
}
|
81
|
+
else:
|
82
|
+
stats = {}
|
83
|
+
return stats
|
84
|
+
|
85
|
+
def tx_inc(self):
|
86
|
+
self.tx += 1
|
87
|
+
|
88
|
+
def rx_inc(self):
|
89
|
+
self.rx += 1
|
90
|
+
|
91
|
+
def email_thread_update(self):
|
92
|
+
self.email_thread_last_time = datetime.datetime.now()
|
93
|
+
|
94
|
+
|
63
95
|
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
64
96
|
"""Email Plugin."""
|
65
97
|
|
@@ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
190
222
|
def _imap_connect():
|
191
223
|
imap_port = CONF.email_plugin.imap_port
|
192
224
|
use_ssl = CONF.email_plugin.imap_use_ssl
|
193
|
-
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
194
|
-
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
195
|
-
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
196
|
-
# format(msg, CONFIG['imap']['login']))
|
197
225
|
|
198
226
|
try:
|
199
227
|
server = imapclient.IMAPClient(
|
@@ -440,7 +468,7 @@ def send_email(to_addr, content):
|
|
440
468
|
[to_addr],
|
441
469
|
msg.as_string(),
|
442
470
|
)
|
443
|
-
|
471
|
+
EmailStats().tx_inc()
|
444
472
|
except Exception:
|
445
473
|
LOG.exception("Sendmail Error!!!!")
|
446
474
|
server.quit()
|
@@ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|
545
573
|
|
546
574
|
def loop(self):
|
547
575
|
time.sleep(5)
|
548
|
-
|
576
|
+
EmailStats().email_thread_update()
|
549
577
|
# always sleep for 5 seconds and see if we need to check email
|
550
578
|
# This allows CTRL-C to stop the execution of this loop sooner
|
551
579
|
# than check_email_delay time
|
aprsd/plugins/time.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import logging
|
2
2
|
import re
|
3
|
-
import time
|
4
3
|
|
5
4
|
from oslo_config import cfg
|
6
5
|
import pytz
|
6
|
+
from tzlocal import get_localzone
|
7
7
|
|
8
8
|
from aprsd import packets, plugin, plugin_utils
|
9
9
|
from aprsd.utils import fuzzy, trace
|
@@ -22,7 +22,8 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
|
22
22
|
short_description = "What is the current local time."
|
23
23
|
|
24
24
|
def _get_local_tz(self):
|
25
|
-
|
25
|
+
lz = get_localzone()
|
26
|
+
return pytz.timezone(str(lz))
|
26
27
|
|
27
28
|
def _get_utcnow(self):
|
28
29
|
return pytz.datetime.datetime.utcnow()
|
aprsd/plugins/version.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import logging
|
2
2
|
|
3
3
|
import aprsd
|
4
|
-
from aprsd import plugin
|
4
|
+
from aprsd import plugin
|
5
|
+
from aprsd.stats import collector
|
5
6
|
|
6
7
|
|
7
8
|
LOG = logging.getLogger("APRSD")
|
@@ -23,10 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
23
24
|
# fromcall = packet.get("from")
|
24
25
|
# message = packet.get("message_text", None)
|
25
26
|
# ack = packet.get("msgNo", "0")
|
26
|
-
|
27
|
-
s = stats_obj.stats()
|
28
|
-
print(s)
|
27
|
+
s = collector.Collector().collect()
|
29
28
|
return "APRSD ver:{} uptime:{}".format(
|
30
29
|
aprsd.__version__,
|
31
|
-
s["
|
30
|
+
s["APRSDStats"]["uptime"],
|
32
31
|
)
|
aprsd/plugins/weather.py
CHANGED
@@ -110,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|
110
110
|
|
111
111
|
@trace.trace
|
112
112
|
def process(self, packet):
|
113
|
-
print("FISTY")
|
114
113
|
fromcall = packet.get("from")
|
115
114
|
message = packet.get("message_text", None)
|
116
115
|
# ack = packet.get("msgNo", "0")
|
aprsd/stats/__init__.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from aprsd import client as aprs_client
|
2
|
+
from aprsd import plugin
|
3
|
+
from aprsd.packets import packet_list, seen_list, tracker, watch_list
|
4
|
+
from aprsd.plugins import email
|
5
|
+
from aprsd.stats import app, collector
|
6
|
+
from aprsd.threads import aprsd
|
7
|
+
|
8
|
+
|
9
|
+
# Create the collector and register all the objects
|
10
|
+
# that APRSD has that implement the stats protocol
|
11
|
+
stats_collector = collector.Collector()
|
12
|
+
stats_collector.register_producer(app.APRSDStats)
|
13
|
+
stats_collector.register_producer(packet_list.PacketList)
|
14
|
+
stats_collector.register_producer(watch_list.WatchList)
|
15
|
+
stats_collector.register_producer(tracker.PacketTrack)
|
16
|
+
stats_collector.register_producer(plugin.PluginManager)
|
17
|
+
stats_collector.register_producer(aprsd.APRSDThreadList)
|
18
|
+
stats_collector.register_producer(email.EmailStats)
|
19
|
+
stats_collector.register_producer(aprs_client.APRSClientStats)
|
20
|
+
stats_collector.register_producer(seen_list.SeenList)
|
aprsd/stats/app.py
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
import datetime
|
2
|
+
import tracemalloc
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
import aprsd
|
7
|
+
from aprsd import utils
|
8
|
+
|
9
|
+
|
10
|
+
CONF = cfg.CONF
|
11
|
+
|
12
|
+
|
13
|
+
class APRSDStats:
|
14
|
+
"""The AppStats class is used to collect stats from the application."""
|
15
|
+
|
16
|
+
_instance = None
|
17
|
+
start_time = None
|
18
|
+
|
19
|
+
def __new__(cls, *args, **kwargs):
|
20
|
+
"""Have to override the new method to make this a singleton
|
21
|
+
|
22
|
+
instead of using @singletone decorator so the unit tests work.
|
23
|
+
"""
|
24
|
+
if not cls._instance:
|
25
|
+
cls._instance = super().__new__(cls)
|
26
|
+
cls._instance.start_time = datetime.datetime.now()
|
27
|
+
return cls._instance
|
28
|
+
|
29
|
+
def uptime(self):
|
30
|
+
return datetime.datetime.now() - self.start_time
|
31
|
+
|
32
|
+
def stats(self, serializable=False) -> dict:
|
33
|
+
current, peak = tracemalloc.get_traced_memory()
|
34
|
+
uptime = self.uptime()
|
35
|
+
if serializable:
|
36
|
+
uptime = str(uptime)
|
37
|
+
stats = {
|
38
|
+
"version": aprsd.__version__,
|
39
|
+
"uptime": uptime,
|
40
|
+
"callsign": CONF.callsign,
|
41
|
+
"memory_current": int(current),
|
42
|
+
"memory_current_str": utils.human_size(current),
|
43
|
+
"memory_peak": int(peak),
|
44
|
+
"memory_peak_str": utils.human_size(peak),
|
45
|
+
}
|
46
|
+
return stats
|
aprsd/stats/collector.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Callable, Protocol, runtime_checkable
|
3
|
+
|
4
|
+
from aprsd.utils import singleton
|
5
|
+
|
6
|
+
|
7
|
+
LOG = logging.getLogger("APRSD")
|
8
|
+
|
9
|
+
|
10
|
+
@runtime_checkable
|
11
|
+
class StatsProducer(Protocol):
|
12
|
+
"""The StatsProducer protocol is used to define the interface for collecting stats."""
|
13
|
+
def stats(self, serializeable=False) -> dict:
|
14
|
+
"""provide stats in a dictionary format."""
|
15
|
+
...
|
16
|
+
|
17
|
+
|
18
|
+
@singleton
|
19
|
+
class Collector:
|
20
|
+
"""The Collector class is used to collect stats from multiple StatsProducer instances."""
|
21
|
+
def __init__(self):
|
22
|
+
self.producers: list[Callable] = []
|
23
|
+
|
24
|
+
def collect(self, serializable=False) -> dict:
|
25
|
+
stats = {}
|
26
|
+
for name in self.producers:
|
27
|
+
cls = name()
|
28
|
+
if isinstance(cls, StatsProducer):
|
29
|
+
try:
|
30
|
+
stats[cls.__class__.__name__] = cls.stats(serializable=serializable).copy()
|
31
|
+
except Exception as e:
|
32
|
+
LOG.error(f"Error in producer {name} (stats): {e}")
|
33
|
+
else:
|
34
|
+
raise TypeError(f"{cls} is not an instance of StatsProducer")
|
35
|
+
return stats
|
36
|
+
|
37
|
+
def register_producer(self, producer_name: Callable):
|
38
|
+
self.producers.append(producer_name)
|
aprsd/threads/__init__.py
CHANGED
@@ -3,8 +3,9 @@ import queue
|
|
3
3
|
# Make these available to anyone importing
|
4
4
|
# aprsd.threads
|
5
5
|
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
6
|
-
from .
|
7
|
-
|
6
|
+
from .rx import ( # noqa: F401
|
7
|
+
APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
|
8
|
+
)
|
8
9
|
|
9
10
|
|
10
11
|
packet_queue = queue.Queue(maxsize=20)
|
aprsd/threads/aprsd.py
CHANGED
@@ -2,6 +2,7 @@ import abc
|
|
2
2
|
import datetime
|
3
3
|
import logging
|
4
4
|
import threading
|
5
|
+
from typing import List
|
5
6
|
|
6
7
|
import wrapt
|
7
8
|
|
@@ -9,43 +10,10 @@ import wrapt
|
|
9
10
|
LOG = logging.getLogger("APRSD")
|
10
11
|
|
11
12
|
|
12
|
-
class APRSDThreadList:
|
13
|
-
"""Singleton class that keeps track of application wide threads."""
|
14
|
-
|
15
|
-
_instance = None
|
16
|
-
|
17
|
-
threads_list = []
|
18
|
-
lock = threading.Lock()
|
19
|
-
|
20
|
-
def __new__(cls, *args, **kwargs):
|
21
|
-
if cls._instance is None:
|
22
|
-
cls._instance = super().__new__(cls)
|
23
|
-
cls.threads_list = []
|
24
|
-
return cls._instance
|
25
|
-
|
26
|
-
@wrapt.synchronized(lock)
|
27
|
-
def add(self, thread_obj):
|
28
|
-
self.threads_list.append(thread_obj)
|
29
|
-
|
30
|
-
@wrapt.synchronized(lock)
|
31
|
-
def remove(self, thread_obj):
|
32
|
-
self.threads_list.remove(thread_obj)
|
33
|
-
|
34
|
-
@wrapt.synchronized(lock)
|
35
|
-
def stop_all(self):
|
36
|
-
"""Iterate over all threads and call stop on them."""
|
37
|
-
for th in self.threads_list:
|
38
|
-
LOG.info(f"Stopping Thread {th.name}")
|
39
|
-
if hasattr(th, "packet"):
|
40
|
-
LOG.info(F"{th.name} packet {th.packet}")
|
41
|
-
th.stop()
|
42
|
-
|
43
|
-
@wrapt.synchronized(lock)
|
44
|
-
def __len__(self):
|
45
|
-
return len(self.threads_list)
|
46
|
-
|
47
|
-
|
48
13
|
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
14
|
+
"""Base class for all threads in APRSD."""
|
15
|
+
|
16
|
+
loop_count = 1
|
49
17
|
|
50
18
|
def __init__(self, name):
|
51
19
|
super().__init__(name=name)
|
@@ -79,6 +47,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|
79
47
|
def run(self):
|
80
48
|
LOG.debug("Starting")
|
81
49
|
while not self._should_quit():
|
50
|
+
self.loop_count += 1
|
82
51
|
can_loop = self.loop()
|
83
52
|
self._last_loop = datetime.datetime.now()
|
84
53
|
if not can_loop:
|
@@ -86,3 +55,65 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|
86
55
|
self._cleanup()
|
87
56
|
APRSDThreadList().remove(self)
|
88
57
|
LOG.debug("Exiting")
|
58
|
+
|
59
|
+
|
60
|
+
class APRSDThreadList:
|
61
|
+
"""Singleton class that keeps track of application wide threads."""
|
62
|
+
|
63
|
+
_instance = None
|
64
|
+
|
65
|
+
threads_list: List[APRSDThread] = []
|
66
|
+
lock = threading.Lock()
|
67
|
+
|
68
|
+
def __new__(cls, *args, **kwargs):
|
69
|
+
if cls._instance is None:
|
70
|
+
cls._instance = super().__new__(cls)
|
71
|
+
cls.threads_list = []
|
72
|
+
return cls._instance
|
73
|
+
|
74
|
+
def stats(self, serializable=False) -> dict:
|
75
|
+
stats = {}
|
76
|
+
for th in self.threads_list:
|
77
|
+
age = th.loop_age()
|
78
|
+
if serializable:
|
79
|
+
age = str(age)
|
80
|
+
stats[th.name] = {
|
81
|
+
"name": th.name,
|
82
|
+
"class": th.__class__.__name__,
|
83
|
+
"alive": th.is_alive(),
|
84
|
+
"age": th.loop_age(),
|
85
|
+
"loop_count": th.loop_count,
|
86
|
+
}
|
87
|
+
return stats
|
88
|
+
|
89
|
+
@wrapt.synchronized(lock)
|
90
|
+
def add(self, thread_obj):
|
91
|
+
self.threads_list.append(thread_obj)
|
92
|
+
|
93
|
+
@wrapt.synchronized(lock)
|
94
|
+
def remove(self, thread_obj):
|
95
|
+
self.threads_list.remove(thread_obj)
|
96
|
+
|
97
|
+
@wrapt.synchronized(lock)
|
98
|
+
def stop_all(self):
|
99
|
+
"""Iterate over all threads and call stop on them."""
|
100
|
+
for th in self.threads_list:
|
101
|
+
LOG.info(f"Stopping Thread {th.name}")
|
102
|
+
if hasattr(th, "packet"):
|
103
|
+
LOG.info(F"{th.name} packet {th.packet}")
|
104
|
+
th.stop()
|
105
|
+
|
106
|
+
@wrapt.synchronized(lock)
|
107
|
+
def info(self):
|
108
|
+
"""Go through all the threads and collect info about each."""
|
109
|
+
info = {}
|
110
|
+
for thread in self.threads_list:
|
111
|
+
alive = thread.is_alive()
|
112
|
+
age = thread.loop_age()
|
113
|
+
key = thread.__class__.__name__
|
114
|
+
info[key] = {"alive": True if alive else False, "age": age, "name": thread.name}
|
115
|
+
return info
|
116
|
+
|
117
|
+
@wrapt.synchronized(lock)
|
118
|
+
def __len__(self):
|
119
|
+
return len(self.threads_list)
|