Replace root logger calls with module-specific loggers

This commit is contained in:
Ian Renton
2026-08-15 08:35:06 +01:00
parent 74bcd9cded
commit dd89ff4af7
64 changed files with 328 additions and 216 deletions
+3 -1
View File
@@ -6,6 +6,8 @@ import pytz
from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
class CleanupTimer:
"""Provides a timed cleanup of the spot list."""
@@ -63,7 +65,7 @@ class CleanupTimer:
except Exception:
self.status = "Error"
logging.exception("Exception in Cleanup thread")
logger.exception("Exception in Cleanup thread")
self._stop_event.wait(timeout=1)
+4 -2
View File
@@ -4,9 +4,11 @@ import os
import yaml
logger = logging.getLogger(__name__)
# Check you have a config file
if not os.path.isfile("config.yml"):
logging.error(
logger.error(
"Your config file is missing. Ensure you have copied config-example.yml to config.yml and updated it according to your needs."
)
exit()
@@ -14,7 +16,7 @@ if not os.path.isfile("config.yml"):
# Load config
with open("config.yml") as f:
config = yaml.safe_load(f)
logging.info("Loaded config.")
logger.info("Loaded config.")
BASE_URL = config.get("base_url", "http://localhost:8080")
MAX_SPOT_AGE = config.get("max_spot_age_sec", 3600)
+3 -1
View File
@@ -3,6 +3,8 @@ import threading
from core.config import config, create_provider_from_config
logger = logging.getLogger(__name__)
class DataProviders:
"""Global object for storing data providers."""
@@ -33,7 +35,7 @@ class DataProviders:
def start_providers(providers, provider_type):
"""Helper method to activate enabled providers in the list."""
logging.info(f"Starting {provider_type} providers...")
logger.info(f"Starting {provider_type} providers...")
for p in providers:
if p.enabled:
p.start()
+6 -4
View File
@@ -8,6 +8,8 @@ from core.config import MAX_ALERT_AGE, MAX_SPOT_AGE
from core.live_data_cache import LiveDataCache
from data.solar_conditions import SolarConditions
logger = logging.getLogger(__name__)
CACHE_DIR = "./cache/"
@@ -64,7 +66,7 @@ class DataStore:
# of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref"
# syntax for keys to keep it a single level.
self.sigrefs = diskcache.Cache(f"{CACHE_DIR}sigrefs")
logging.info(f"Loaded data for {len(self.sigrefs)} SIG references.")
logger.info(f"Loaded data for {len(self.sigrefs)} SIG references.")
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
@@ -83,7 +85,7 @@ class DataStore:
self.callsign_data_hamqth,
]:
unique_keys.update(c)
logging.info(f"Loaded data for {len(unique_keys)} callsigns.")
logger.info(f"Loaded data for {len(unique_keys)} callsigns.")
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
@@ -94,7 +96,7 @@ class DataStore:
snapshot_dir=f"{CACHE_DIR}spots",
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC,
)
logging.info(f"Loaded {len(self.spots.keys())} spots from a previous run.")
logger.info(f"Loaded {len(self.spots.keys())} spots from a previous run.")
self.alerts = LiveDataCache(
maxsize=self._MAX_ALERT_COUNT,
@@ -102,7 +104,7 @@ class DataStore:
snapshot_dir=f"{CACHE_DIR}alerts",
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC,
)
logging.info(f"Loaded {len(self.alerts.keys())} alerts from a previous run.")
logger.info(f"Loaded {len(self.alerts.keys())} alerts from a previous run.")
def regenerate_call_regex_to_dxcc_entity_map(self):
"""DXCC entity data from K0SWE includes a regex which we can use to match a callsign, and determine which DXCC
+3 -1
View File
@@ -7,6 +7,8 @@ from shapely.geometry import Point, Polygon
from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326")
TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326")
TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326")
@@ -172,7 +174,7 @@ def wab_wai_square_to_lat_lon(ref):
elif re.match(r"^W[AV][0-9]{2}$", ref):
return utm_grid_square_to_lat_lon(ref)
else:
logging.warning(f"Invalid WAB/WAI square: {ref}")
logger.warning(f"Invalid WAB/WAI square: {ref}")
return None
+5 -3
View File
@@ -5,6 +5,8 @@ import time
import diskcache
from cachetools import TTLCache
logger = logging.getLogger(__name__)
class LiveDataCache:
"""Cache for spots and alerts. Uses the fast in-memory TTLCache for normal data I/O, including the TTL to enforce
@@ -34,7 +36,7 @@ class LiveDataCache:
try:
callback(value)
except Exception:
logging.exception(f"Listener raised an exception for key {key}")
logger.exception(f"Listener raised an exception for key {key}")
def get(self, key, default=None):
with self._lock:
@@ -70,7 +72,7 @@ class LiveDataCache:
try:
self._disk_cache.set("snapshot", data)
except Exception as e:
logging.exception(f"Failed to write snapshot to {self._snapshot_dir}")
logger.exception(f"Failed to write snapshot to {self._snapshot_dir}")
def _load_snapshot(self):
data = self._disk_cache.get("snapshot")
@@ -83,7 +85,7 @@ class LiveDataCache:
# Only restore entries that would still be within TTL
if now - saved_at < self._ttl:
self._cache[key] = value
logging.info(f"Loaded snapshot from {self._snapshot_dir}")
logger.info(f"Loaded snapshot from {self._snapshot_dir}")
def _start_periodic_snapshot(self, interval):
def loop():
+6 -4
View File
@@ -6,6 +6,8 @@ from core.data_store import DATA_STORE
from core.geo_utils import wab_wai_square_to_lat_lon
from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
def get_sig_ref_info(sig, ref_id):
"""Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig name and
@@ -14,7 +16,7 @@ def get_sig_ref_info(sig, ref_id):
SIG we are getting data for."""
if sig is None or sig == "" or ref_id is None or ref_id == "":
logging.debug("Failed to look up sig_ref info, sig or ref were not set.")
logger.debug("Failed to look up sig_ref info, sig or ref were not set.")
return None
sig_ref = SIGRef(sig=sig, id=ref_id)
@@ -59,7 +61,7 @@ def get_sig_ref_info(sig, ref_id):
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
except:
logging.warning("Invalid lat/lon received for WAB/WAI reference")
logger.warning("Invalid lat/lon received for WAB/WAI reference")
return sig_ref
elif sig.upper() == "BOTA":
@@ -83,10 +85,10 @@ def get_sig_ref_info(sig, ref_id):
else:
# Maybe a super new reference we don't know about yet, but more likely a typo or a test reference,
# just silently ignore it.
logging.debug(f"{sig} database did not contain data for ref {ref_id}")
logger.debug(f"{sig} database did not contain data for ref {ref_id}")
except Exception:
logging.exception(f"Exception when looking up sig_ref info for {sig} ref {ref_id}")
logger.exception(f"Exception when looking up sig_ref info for {sig} ref {ref_id}")
return sig_ref
+3 -1
View File
@@ -16,6 +16,8 @@ from core.constants import (
from core.data_store import DATA_STORE
from data.callsign import Callsign
logger = logging.getLogger(__name__)
def safe_json_dumps(obj):
"""Safe version of json.dumps that also converts objects to dicts so they can be output, and ignores NaN floats
@@ -47,7 +49,7 @@ def infer_mode_type_from_mode(mode):
return "DATA"
else:
if mode.upper() != "OTHER":
logging.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
return None
+3 -1
View File
@@ -10,6 +10,8 @@ from core.call_lookup_helper import get_call_info
from core.sig_lookup_helper import populate_missing_sig_ref_info
from core.utils import get_flag_for_dxcc
logger = logging.getLogger(__name__)
@dataclass
class Alert:
@@ -128,7 +130,7 @@ class Alert:
self.dx_names = list(map(lambda c: get_call_info(c, credentials).name, self.dx_calls))
except Exception:
logging.exception("Exception while inferring missing data from spot")
logger.exception("Exception while inferring missing data from spot")
def to_json(self):
"""JSON serialise"""
+6 -4
View File
@@ -27,6 +27,8 @@ from core.utils import (
)
from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
@dataclass
class Spot:
@@ -340,7 +342,7 @@ class Spot:
self.propagation_mode = PROPAGATION_MODES[mode_tag]
else:
self.propagation_mode = mode_tag
logging.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
# Parse "de_grid -> dx_grid" structures from the comment
if self.comment:
@@ -363,12 +365,12 @@ class Spot:
self.dx_latitude = ll[0]
self.dx_longitude = ll[1]
except:
logging.debug("Invalid grid received for spot")
logger.debug("Invalid grid received for spot")
if self.dx_latitude and self.dx_longitude and not self.dx_grid:
try:
self.dx_grid = latlong_to_locator(self.dx_latitude, self.dx_longitude, 8)
except:
logging.debug("Invalid lat/lon received for spot")
logger.debug("Invalid lat/lon received for spot")
# QRT comment detection
if self.comment and not self.qrt:
@@ -444,7 +446,7 @@ class Spot:
self.de_grid = de_call_info.grid
except Exception:
logging.exception("Exception while inferring missing data from spot")
logger.exception("Exception while inferring missing data from spot")
def to_json(self):
"""JSON serialise"""
+9 -7
View File
@@ -9,6 +9,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS
from providers.alert.alert_provider import AlertProvider
logger = logging.getLogger(__name__)
class HTTPAlertProvider(AlertProvider):
"""Generic alert provider class for providers that request data via HTTP(S). Just for convenience to avoid code
@@ -24,7 +26,7 @@ class HTTPAlertProvider(AlertProvider):
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} alert API every {self._poll_interval!s} seconds.")
logger.info(f"Set up query of {self.name} alert API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPAlertProvider-{self.name}")
self._thread.start()
@@ -40,7 +42,7 @@ class HTTPAlertProvider(AlertProvider):
def _poll(self):
try:
# Request data from API
logging.debug(f"Polling {self.name} alert API...")
logger.debug(f"Polling {self.name} alert API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good
if http_response.ok:
@@ -52,18 +54,18 @@ class HTTPAlertProvider(AlertProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} alert API.")
logger.debug(f"Received data from {self.name} alert API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.")
logger.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} alerts API.")
logger.warning(f"Connection error when accessing {self.name} alerts API.")
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} alerts API.")
logger.warning(f"Timeout when accessing {self.name} alerts API.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in HTTP JSON Alert Provider ({self.name})")
logger.exception(f"Exception in HTTP JSON Alert Provider ({self.name})")
# Brief pause on error before the next poll, but still respond promptly to stop()
self._stop_event.wait(timeout=1)
+3 -1
View File
@@ -7,6 +7,8 @@ from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
logger = logging.getLogger(__name__)
class ParksNPeaks(HTTPAlertProvider):
"""Alert provider for Parks n Peaks"""
@@ -64,7 +66,7 @@ class ParksNPeaks(HTTPAlertProvider):
"LLOTA",
"QRP",
]:
logging.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!")
logger.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!")
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
# the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that
+4 -2
View File
@@ -11,6 +11,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider,
)
logger = logging.getLogger(__name__)
class ClublogAPI(APIQueryCallsignDataProvider):
"""Callsign data provider for Clublog's API."""
@@ -25,7 +27,7 @@ class ClublogAPI(APIQueryCallsignDataProvider):
self._callinfo = Callinfo(lookuplib)
else:
provider_config["enabled"] = False
logging.warning(
logger.warning(
"Clublog XML callsign data provider configured but no api key was provided, this has been disabled."
)
@@ -45,6 +47,6 @@ class ClublogAPI(APIQueryCallsignDataProvider):
except Exception:
self.status = "Error"
logging.exception("Exception when looking up data from Clublog API")
logger.exception("Exception when looking up data from Clublog API")
return callsign_data
+5 -3
View File
@@ -10,6 +10,8 @@ from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider,
)
logger = logging.getLogger(__name__)
class ClublogXML(FileDownloadCallsignDataProvider):
"""Callsign data provider for ClubLog's Country File, which provides basic callsign to DXCC entity mapping."""
@@ -25,7 +27,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
self._api_key = provider_config.get("api_key", "")
if self._api_key == "":
provider_config["enabled"] = False
logging.warning(
logger.warning(
"Clublog XML callsign data provider configured but no api key was provided, this has been disabled."
)
@@ -55,7 +57,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
return True
except Exception:
logging.exception("Exception when loading Clublog XML.")
logger.exception("Exception when loading Clublog XML.")
return False
def _perform_new_lookup(self, callsign, lookup_credentials):
@@ -71,6 +73,6 @@ class ClublogXML(FileDownloadCallsignDataProvider):
except Exception:
self.status = "Error"
logging.exception("Exception when looking up data from Clublog XML data")
logger.exception("Exception when looking up data from Clublog XML data")
return callsign_data
+4 -2
View File
@@ -9,6 +9,8 @@ from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider,
)
logger = logging.getLogger(__name__)
class CountryFiles(FileDownloadCallsignDataProvider):
"""Callsign data provider for Country-files.com, which provides basic callsign to DXCC entity mapping."""
@@ -35,7 +37,7 @@ class CountryFiles(FileDownloadCallsignDataProvider):
return True
except Exception:
logging.exception("Exception when loading Country Files cty.plist.")
logger.exception("Exception when loading Country Files cty.plist.")
return False
def _perform_new_lookup(self, callsign, lookup_credentials):
@@ -51,6 +53,6 @@ class CountryFiles(FileDownloadCallsignDataProvider):
except Exception:
self.status = "Error"
logging.exception("Exception when looking up data from Country file")
logger.exception("Exception when looking up data from Country file")
return callsign_data
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache
from providers.callsigndata.callsign_data_provider import CallsignDataProvider
logger = logging.getLogger(__name__)
class FileDownloadCallsignDataProvider(CallsignDataProvider):
"""Generic callsign data provider class for providers that fetch their data from the web by downloading a file."""
@@ -30,7 +32,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} callsign reference data every {self._poll_interval!s} days.")
logger.info(f"Set up query of {self.name} callsign reference data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadCallsignDataProvider-{self.name}")
self._thread.start()
@@ -47,7 +49,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
try:
# Request the file. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
# caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.name} callsign reference data...")
logger.debug(f"Downloading {self.name} callsign reference data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good
if http_response.ok:
@@ -60,26 +62,26 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
if ok:
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated callsign reference data from {self.name}")
logger.info(f"Updated callsign reference data from {self.name}")
else:
self.status = "Error"
logging.warning(f"Error updating callsign reference data from {self.name}.")
logger.warning(f"Error updating callsign reference data from {self.name}.")
else:
self.status = "Error"
logging.warning(
logger.warning(
f"HTTP {http_response.status_code} when downloading callsign reference data from {self.name}."
)
except ConnectionError:
self.status = "Error"
logging.warning(f"Connection error when downloading callsign reference data from {self.name}.")
logger.warning(f"Connection error when downloading callsign reference data from {self.name}.")
except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logging.warning(f"Timeout when downloading callsign reference data from {self.name}.")
logger.warning(f"Timeout when downloading callsign reference data from {self.name}.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in callsign reference data provider ({self.name})")
logger.exception(f"Exception in callsign reference data provider ({self.name})")
self._stop_event.wait(timeout=1)
def _handle_file(self, path):
+10 -8
View File
@@ -17,6 +17,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider,
)
logger = logging.getLogger(__name__)
class HamQTH(APIQueryCallsignDataProvider):
"""Callsign data provider for HamQTH."""
@@ -55,10 +57,10 @@ class HamQTH(APIQueryCallsignDataProvider):
session_id = str(dict_data["HamQTH"]["session"]["session_id"])
else:
# Log this failure at debug level only, not our problem if user entered the wrong password.
logging.debug("HamQTH login details incorrect, failed to look up with HamQTH.")
logger.debug("HamQTH login details incorrect, failed to look up with HamQTH.")
return None
except Exception:
logging.error("Exception when getting HamQTH session key")
logger.error("Exception when getting HamQTH session key")
return None
if not session_id:
@@ -71,7 +73,7 @@ class HamQTH(APIQueryCallsignDataProvider):
if home_call != callsign:
calls_to_try.append(home_call)
except ValueError:
logging.debug(f"Could not look up home call for callsign {callsign}")
logger.debug(f"Could not look up home call for callsign {callsign}")
# Try looking up each call using the API
for lookup_call in calls_to_try:
@@ -90,18 +92,18 @@ class HamQTH(APIQueryCallsignDataProvider):
return self.hamqth_response_to_callsign(callsign, data)
elif not response.from_cache:
logging.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using HamQTH")
logger.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using HamQTH")
except (KeyError, ValueError):
continue
except ConnectionError:
logging.warning(f"Connection error when looking up callsign {lookup_call} using HamQTH")
logger.warning(f"Connection error when looking up callsign {lookup_call} using HamQTH")
continue
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when looking up callsign {lookup_call} using HamQTH")
logger.warning(f"Timeout when looking up callsign {lookup_call} using HamQTH")
continue
except Exception:
logging.exception(f"Exception when looking up callsign {lookup_call} using HamQTH")
logger.exception(f"Exception when looking up callsign {lookup_call} using HamQTH")
continue
# Not found in HamQTH; return a Callsign object with no data so we cache that and don't keep retrying
@@ -109,7 +111,7 @@ class HamQTH(APIQueryCallsignDataProvider):
except Exception:
self.status = "Error"
logging.exception("Exception when looking up data from HamQTH")
logger.exception("Exception when looking up data from HamQTH")
# Return None, this won't be cached so we will be asked to query data again for this call next time.
return None
+12 -10
View File
@@ -16,6 +16,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider,
)
logger = logging.getLogger(__name__)
class QRZ(APIQueryCallsignDataProvider):
"""Callsign data provider for QRZ.com."""
@@ -53,10 +55,10 @@ class QRZ(APIQueryCallsignDataProvider):
session_key = str(session["Key"])
else:
# Log this failure at debug level only, not our problem if user entered the wrong password.
logging.debug("QRZ.com login details incorrect, failed to look up with QRZ.")
logger.debug("QRZ.com login details incorrect, failed to look up with QRZ.")
return None
except Exception:
logging.error("Exception when getting QRZ.com session key")
logger.error("Exception when getting QRZ.com session key")
return None
if not session_key:
@@ -69,7 +71,7 @@ class QRZ(APIQueryCallsignDataProvider):
if home_call != callsign:
calls_to_try.append(home_call)
except ValueError:
logging.debug(f"Could not look up home call for callsign {callsign}")
logger.debug(f"Could not look up home call for callsign {callsign}")
# Try looking up each call using the API
for lookup_call in calls_to_try:
@@ -93,25 +95,25 @@ class QRZ(APIQueryCallsignDataProvider):
elif "Session" in qrz_response and "Error" in qrz_response.get("Session"):
# Errors here are normally just "callsign not in database", no need to log that ourselves
# above debug level.
logging.debug(
logger.debug(
f"QRZ returned an error looking up callsign {lookup_call}: {qrz_response.get('Session').get('Error')}"
)
elif not response.from_cache:
logging.warning(f"QRZ returned a malformed response looking up callsign {lookup_call}")
logger.warning(f"QRZ returned a malformed response looking up callsign {lookup_call}")
elif not response.from_cache:
logging.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using QRZ")
logger.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using QRZ")
except (KeyError, ValueError):
continue
except ConnectionError:
logging.warning(f"Connection error when looking up callsign {lookup_call} using QRZ")
logger.warning(f"Connection error when looking up callsign {lookup_call} using QRZ")
continue
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when looking up callsign {lookup_call} using QRZ.")
logger.warning(f"Timeout when looking up callsign {lookup_call} using QRZ.")
continue
except Exception:
logging.exception(f"Exception when looking up callsign {lookup_call} using QRZ")
logger.exception(f"Exception when looking up callsign {lookup_call} using QRZ")
continue
# Not found in QRZ; return a Callsign object with no data so we cache that and don't keep retrying
@@ -119,7 +121,7 @@ class QRZ(APIQueryCallsignDataProvider):
except Exception:
self.status = "Error"
logging.exception("Exception when looking up data from QRZ.com")
logger.exception("Exception when looking up data from QRZ.com")
# Return None, this won't be cached so we will be asked to query data again for this call next time.
return None
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
logger = logging.getLogger(__name__)
class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
"""Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file."""
@@ -26,7 +28,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.sig_name} SIG ref data every {self._poll_interval!s} days.")
logger.info(f"Set up query of {self.sig_name} SIG ref data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}")
self._thread.start()
@@ -44,7 +46,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
try:
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
# caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.sig_name} SIG ref data...")
logger.debug(f"Downloading {self.sig_name} SIG ref data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good
if http_response.ok:
@@ -56,20 +58,20 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received SIG ref data for {self.sig_name}")
logger.debug(f"Received SIG ref data for {self.sig_name}")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.")
logger.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.")
except ConnectionError:
self.status = "Error"
logging.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.")
logger.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.")
except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logging.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.")
logger.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})")
logger.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})")
self._stop_event.wait(timeout=1)
def _http_response_to_data(self, http_response):
+3 -1
View File
@@ -8,6 +8,8 @@ from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider,
)
logger = logging.getLogger(__name__)
class IOTA(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Islands on the Air"""
@@ -31,7 +33,7 @@ class IOTA(FileDownloadSIGRefDataProvider):
try:
grid = latlong_to_locator(latitude, longitude, 6)
except ValueError:
logging.debug(
logger.debug(
"Error converting lat/lon to locator for an IOTA reference %f %f",
latitude,
longitude,
@@ -5,6 +5,8 @@ import pytz
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
logger = logging.getLogger(__name__)
class LocalFileSIGRefDataProvider(SIGRefDataProvider):
"""Generic SIG ref data provider class for providers that fetch their data from a local file on startup."""
@@ -14,7 +16,7 @@ class LocalFileSIGRefDataProvider(SIGRefDataProvider):
self._path = path
def start(self):
logging.debug(f"Loading {self.sig_name} SIG ref data from file.")
logger.debug(f"Loading {self.sig_name} SIG ref data from file.")
try:
new_data = self._file_to_data(self._path)
if new_data:
@@ -23,10 +25,10 @@ class LocalFileSIGRefDataProvider(SIGRefDataProvider):
self.last_update_time = datetime.now(pytz.UTC)
else:
self.status = "Error"
logging.info(f"Failed to load SIG ref data for {self.sig_name}")
logger.info(f"Failed to load SIG ref data for {self.sig_name}")
except Exception:
self.status = "Error"
logging.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})")
logger.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})")
def _file_to_data(self, path):
"""Load a file on the given path and turn it into SIG Ref data."""
@@ -5,6 +5,8 @@ import pytz
from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
class SIGRefDataProvider:
"""Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data."""
@@ -45,4 +47,4 @@ class SIGRefDataProvider:
break
self.reference_count = len(new_data)
logging.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
logger.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider,
)
logger = logging.getLogger(__name__)
class WCA(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for World Castles Award"""
@@ -36,7 +38,7 @@ class WCA(FileDownloadSIGRefDataProvider):
longitude = float(split[1])
grid = latlong_to_locator(latitude, longitude)
except ValueError:
logging.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
new_data.append(
SIGRef(
+9 -7
View File
@@ -11,6 +11,8 @@ from core.constants import HTTP_HEADERS
from providers.solarconditions.ionosonde_utils import compute_band_states
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
# Each station gets polled roughly once every hour (3600 seconds). Note that to avoid a burst of requests to the server
# every hour, the requests for data from each station are spaced out throughout the hour, leading to one request being
# sent every 1-2 minutes.
@@ -64,7 +66,7 @@ class GIROIonosonde(SolarConditionsProvider):
return stations
def start(self):
logging.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.")
logger.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.")
self._thread = Thread(target=self._run, name="GIROIonosondeDataProvider")
self._thread.start()
@@ -86,7 +88,7 @@ class GIROIonosonde(SolarConditionsProvider):
ursi = station["ursi"]
name = station["name"]
try:
logging.debug(f"Polling GIRO ionosonde data for {ursi} ({name})...")
logger.debug(f"Polling GIRO ionosonde data for {ursi} ({name})...")
now = datetime.now(timezone.utc)
from_time = now - timedelta(hours=HISTORY_HOURS)
cutoff_ts = from_time.timestamp()
@@ -127,11 +129,11 @@ class GIROIonosonde(SolarConditionsProvider):
self.update_data({"ionosonde_data": ionosonde_data})
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Updated ionosonde data for {ursi} ({name}).")
logger.debug(f"Updated ionosonde data for {ursi} ({name}).")
except Exception:
self.status = "Error"
logging.exception(f"Exception fetching GIRO ionosonde data for {ursi} ({name})")
logger.exception(f"Exception fetching GIRO ionosonde data for {ursi} ({name})")
def _fetch_station_data(self, ursi, from_time, to_time):
"""Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
@@ -142,14 +144,14 @@ class GIROIonosonde(SolarConditionsProvider):
try:
http_response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
if not http_response.ok:
logging.warning(f"HTTP {http_response.status_code} when calling Giro ionosonde API.")
logger.warning(f"HTTP {http_response.status_code} when calling Giro ionosonde API.")
return None, None, None
return self._parse_all(http_response.text)
except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing Giro ionosonde API.")
logger.warning("Timeout when accessing Giro ionosonde API.")
return None, None, None
except ConnectionError:
logging.warning("Connection error when accessing Giro ionosonde API.")
logger.warning("Connection error when accessing Giro ionosonde API.")
return None, None, None
@staticmethod
+5 -3
View File
@@ -9,6 +9,8 @@ from providers.solarconditions.http_solar_conditions_provider import (
HTTPSolarConditionsProvider,
)
logger = logging.getLogger(__name__)
POLL_INTERVAL = 3600 # 1 hour
URL = "https://www.hamqsl.com/solarxml.php"
@@ -24,14 +26,14 @@ class HamQSL(HTTPSolarConditionsProvider):
root = ElementTree.fromstring(http_response.text)
sd = root.find("solardata")
if sd is None:
logging.warning("HamQSL solar conditions API returned unexpected XML structure")
logger.warning("HamQSL solar conditions API returned unexpected XML structure")
return None
# Some error checking functions in case the data is janky.
def text(tag, default=None):
if sd is None:
logging.warning("HamQSL solar conditions API returned unexpected XML structure")
logger.warning("HamQSL solar conditions API returned unexpected XML structure")
return default
el = sd.find(tag)
return el.text.strip() if el is not None and el.text else default
@@ -79,7 +81,7 @@ class HamQSL(HTTPSolarConditionsProvider):
dt = dateutil_parser.parse(updated_str, tzinfos={tz_abbr: timezone})
updated = dt.astimezone(pytz.UTC).timestamp()
except (ValueError, IndexError):
logging.warning(f"HamQSL solar conditions API returned unrecognised timestamp format: {updated_str}")
logger.warning(f"HamQSL solar conditions API returned unrecognised timestamp format: {updated_str}")
# Return the data ready to be put into the solar conditions object.
return {
@@ -9,6 +9,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
class HTTPSolarConditionsProvider(SolarConditionsProvider):
"""Generic solar conditions provider for providers that request data via HTTP(S). Subclasses implement
@@ -22,7 +24,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
self._stop_event = Event()
def start(self):
logging.info(f"Set up query of {self.name} solar conditions API every {self._poll_interval!s} seconds.")
logger.info(f"Set up query of {self.name} solar conditions API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPSolarConditionsProvider-{self.name}")
self._thread.start()
@@ -37,7 +39,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
def _poll(self):
try:
logging.debug(f"Polling {self.name} solar conditions API...")
logger.debug(f"Polling {self.name} solar conditions API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good
if http_response.ok:
@@ -46,18 +48,18 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} solar conditions API.")
logger.debug(f"Received data from {self.name} solar conditions API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.")
logger.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} solar conditions API.")
logger.warning(f"Connection error when accessing {self.name} solar conditions API.")
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} solar conditions API.")
logger.warning(f"Timeout when accessing {self.name} solar conditions API.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in HTTP Solar Conditions Provider ({self.name})")
logger.exception(f"Exception in HTTP Solar Conditions Provider ({self.name})")
self._stop_event.wait(timeout=1)
def _http_response_to_solar_conditions(self, http_response):
+9 -7
View File
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from providers.solarconditions.ionosonde_utils import compute_band_states
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
POLL_INTERVAL = 900 # 15 minutes
KC2G_URL = "https://prop.kc2g.com/api/stations.json"
HISTORY_HOURS = 24
@@ -29,7 +31,7 @@ class KC2GProp(SolarConditionsProvider):
self._stop_event = Event()
def start(self):
logging.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.")
logger.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.")
self._thread = Thread(target=self._run, name="KC2GPropProvider")
self._thread.start()
@@ -44,10 +46,10 @@ class KC2GProp(SolarConditionsProvider):
def _poll(self):
try:
logging.debug("Polling KC2G ionosonde data...")
logger.debug("Polling KC2G ionosonde data...")
http_response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30))
if not http_response.ok:
logging.warning(f"HTTP {http_response.status_code} when calling KG2G ionosonde API.")
logger.warning(f"HTTP {http_response.status_code} when calling KG2G ionosonde API.")
return
now = datetime.now(timezone.utc)
@@ -114,13 +116,13 @@ class KC2GProp(SolarConditionsProvider):
self.update_data({"ionosonde_data": ionosonde_data})
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Updated KC2G ionosonde data for {updated_count} stations.")
logger.debug(f"Updated KC2G ionosonde data for {updated_count} stations.")
except ConnectionError:
logging.warning("Connection error when accessing KC2G ionosonde API.")
logger.warning("Connection error when accessing KC2G ionosonde API.")
except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing KC2G ionosonde API.")
logger.warning("Timeout when accessing KC2G ionosonde API.")
except Exception:
self.status = "Error"
logging.exception("Exception in KC2G ionosonde data provider")
logger.exception("Exception in KC2G ionosonde data provider")
self._stop_event.wait(timeout=1)
+12 -10
View File
@@ -6,6 +6,8 @@ from providers.solarconditions.http_solar_conditions_provider import (
HTTPSolarConditionsProvider,
)
logger = logging.getLogger(__name__)
POLL_INTERVAL = 10800 # Every 3 hours
URL = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
@@ -28,7 +30,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
start_idx = i
break
if start_idx is None:
logging.warning(f"NOAA 3-day forecast: could not find '{section_header}' section")
logger.warning(f"NOAA 3-day forecast: could not find '{section_header}' section")
return None
# Find the date header line by scanning the next few lines for month & day patterns
@@ -38,11 +40,11 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
date_header_idx = j
break
if date_header_idx is None:
logging.warning(f"NOAA 3-day forecast: could not find date header after '{section_header}'")
logger.warning(f"NOAA 3-day forecast: could not find date header after '{section_header}'")
return None
date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", lines[date_header_idx])
if not date_matches:
logging.warning(f"NOAA 3-day forecast: no dates in header: {lines[date_header_idx]}")
logger.warning(f"NOAA 3-day forecast: no dates in header: {lines[date_header_idx]}")
return None
# Figure out the date based on the line found
@@ -52,7 +54,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
dt = datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").replace(tzinfo=timezone.utc)
column_timestamps.append(dt.timestamp())
except ValueError:
logging.warning(f"NOAA 3-day forecast: could not parse date: {month_str} {day_str} {year}")
logger.warning(f"NOAA 3-day forecast: could not parse date: {month_str} {day_str} {year}")
return None
# Parse data rows. Each non-empty line should have a text label followed by percentage values
@@ -91,26 +93,26 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
start_idx = i
break
if start_idx is None:
logging.warning("NOAA K-index forecast: could not find 'NOAA Kp index breakdown' section")
logger.warning("NOAA K-index forecast: could not find 'NOAA Kp index breakdown' section")
return None
# Extract the year from the header line, e.g. "NOAA Kp index breakdown Apr 2-Apr 4, 2026"
header_line = lines[start_idx]
year_match = re.search(r"\b(\d{4})\b", header_line)
if not year_match:
logging.warning(f"NOAA K-index forecast: could not extract year from: {header_line}")
logger.warning(f"NOAA K-index forecast: could not extract year from: {header_line}")
return None
year = int(year_match.group(1))
# Parse the column date headers on the next line, e.g. " Apr 02 Apr 03 Apr 04"
if start_idx + 1 >= len(lines):
logging.warning("NOAA K-index forecast: missing date header line")
logger.warning("NOAA K-index forecast: missing date header line")
return None
date_header_line = lines[start_idx + 2]
date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", date_header_line)
if not date_matches:
logging.warning(f"NOAA K-index forecast: could not parse date headers from: {date_header_line}")
logger.warning(f"NOAA K-index forecast: could not parse date headers from: {date_header_line}")
return None
column_dates = []
@@ -118,7 +120,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
try:
column_dates.append(datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").date())
except ValueError:
logging.warning(f"NOAA K-index forecast: could not parse date: {month_str} {day_str} {year}")
logger.warning(f"NOAA K-index forecast: could not parse date: {month_str} {day_str} {year}")
return None
# Parse each data row, e.g. "00-03UT 2.00 3.00 2.00"
@@ -159,7 +161,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
k_index_forecast[key] = kp
if not k_index_forecast:
logging.warning("NOAA K-index forecast: no data rows parsed")
logger.warning("NOAA K-index forecast: no data rows parsed")
return None
# Parse Solar Radiation Storm Forecast (single row: "S1 or greater")
+5 -3
View File
@@ -9,6 +9,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class APRSIS(SpotProvider):
"""Spot provider for the APRS-IS."""
@@ -25,10 +27,10 @@ class APRSIS(SpotProvider):
def _connect(self):
self._aprsis = aprslib.IS(SERVER_OWNER_CALLSIGN)
self.status = "Connecting"
logging.info("APRS-IS connecting...")
logger.info("APRS-IS connecting...")
self._aprsis.connect()
self._aprsis.consumer(self._handle)
logging.info("APRS-IS connected.")
logger.info("APRS-IS connected.")
def stop(self):
self.status = "Shutting down"
@@ -60,4 +62,4 @@ class APRSIS(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Data received from APRS-IS.")
logger.debug("Data received from APRS-IS.")
+11 -9
View File
@@ -11,6 +11,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class DXCluster(SpotProvider):
"""Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config.
@@ -59,19 +61,19 @@ class DXCluster(SpotProvider):
while not connected and self._running:
try:
self.status = "Connecting"
logging.info(f"DX Cluster {self._hostname} connecting...")
logger.info(f"DX Cluster {self._hostname} connecting...")
self._telnet = telnetlib3.Telnet(self._hostname, self._port)
self._telnet.read_until(self._login_prompt.encode("latin-1"))
self._telnet.write(f"{self._login_callsign}\n".encode("latin-1"))
connected = True
logging.info(f"DX Cluster {self._hostname} connected.")
logger.info(f"DX Cluster {self._hostname} connected.")
except ConnectionRefusedError:
self.status = "Error"
logging.warning(f"Connection refused to DX cluster {self._hostname}")
logger.warning(f"Connection refused to DX cluster {self._hostname}")
sleep(300)
except Exception:
self.status = "Error"
logging.exception(f"Exception while connecting to DX Cluster Provider ({self._hostname}).")
logger.exception(f"Exception while connecting to DX Cluster Provider ({self._hostname}).")
sleep(5)
self.status = "Waiting for Data"
@@ -101,25 +103,25 @@ class DXCluster(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Data received from DX Cluster {self._hostname}.")
logger.debug(f"Data received from DX Cluster {self._hostname}.")
except EOFError:
connected = False
if self._running:
self.status = "Restarting"
logging.warning(f"Disconnected from DX Cluster {self._hostname}. Reconnecting...")
logger.warning(f"Disconnected from DX Cluster {self._hostname}. Reconnecting...")
sleep(5)
else:
logging.info(f"DX Cluster {self._hostname} shutting down...")
logger.info(f"DX Cluster {self._hostname} shutting down...")
self.status = "Shutting down"
except Exception:
connected = False
if self._running:
self.status = "Error"
logging.exception(f"Exception in DX Cluster Provider ({self._hostname})")
logger.exception(f"Exception in DX Cluster Provider ({self._hostname})")
sleep(5)
else:
logging.info(f"DX Cluster {self._hostname} shutting down...")
logger.info(f"DX Cluster {self._hostname} shutting down...")
self.status = "Shutting down"
self.status = "Disconnected"
+8 -6
View File
@@ -9,6 +9,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class GMA(HTTPSpotProvider):
"""Spot provider for General Mountain Activity"""
@@ -24,7 +26,7 @@ class GMA(HTTPSpotProvider):
self._api_key = provider_config.get("api_key", "")
if self._api_key == "":
provider_config["enabled"] = False
logging.warning("GMA spot provider configured but no api key was provided, this API will not be queried.")
logger.warning("GMA spot provider configured but no api key was provided, this API will not be queried.")
self._url_data_cache = URLDataCache("GMA")
super().__init__(
@@ -127,7 +129,7 @@ class GMA(HTTPSpotProvider):
spot.sig_refs[0].sig = "MOTA"
spot.sig = "MOTA"
case _:
logging.warning(
logger.warning(
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
)
spot.sig_refs[0].sig = ref_info["reftype"]
@@ -139,19 +141,19 @@ class GMA(HTTPSpotProvider):
elif not ref_response.from_cache:
if not ref_response.ok:
logging.warning(
logger.warning(
f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}"
)
else:
logging.warning(
logger.warning(
f"GMA API returned a malformed response when looking up ref {source_spot['REF']}"
)
except:
logging.exception(
logger.exception(
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, ignoring this spot for now"
)
else:
logging.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).")
logger.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).")
return new_spots
+4 -2
View File
@@ -11,6 +11,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class HEMA(HTTPSpotProvider):
"""Spot provider for HuMPs Excluding Marilyns Award"""
@@ -80,9 +82,9 @@ class HEMA(HTTPSpotProvider):
# code will do that for us.
new_spots.append(spot)
except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing HEMA spots API.")
logger.warning("Timeout when accessing HEMA spots API.")
except ConnectionError:
logging.warning("Connection error when accessing HEMA spots API.")
logger.warning("Connection error when accessing HEMA spots API.")
return new_spots
def can_submit_spot(self, sig):
+9 -7
View File
@@ -10,6 +10,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout
from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class HTTPSpotProvider(SpotProvider):
"""Generic spot provider class for providers that request data via HTTP(S). Just for convenience to avoid code
@@ -26,7 +28,7 @@ class HTTPSpotProvider(SpotProvider):
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} spot API every {self._poll_interval!s} seconds.")
logger.info(f"Set up query of {self.name} spot API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPSpotProvider-{self.name}")
self._thread.start()
@@ -50,7 +52,7 @@ class HTTPSpotProvider(SpotProvider):
def _poll(self):
try:
# Request data from API
logging.debug(f"Polling {self.name} spot API...")
logger.debug(f"Polling {self.name} spot API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good
if http_response.ok:
@@ -62,18 +64,18 @@ class HTTPSpotProvider(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.")
logger.debug(f"Received data from {self.name} spot API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.")
logger.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} spots API.")
logger.warning(f"Connection error when accessing {self.name} spots API.")
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} spots API.")
logger.warning(f"Timeout when accessing {self.name} spots API.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in HTTP Spot Provider ({self.name})")
logger.exception(f"Exception in HTTP Spot Provider ({self.name})")
self._stop_event.wait(timeout=1)
def _http_response_to_spots(self, http_response):
+3 -1
View File
@@ -10,6 +10,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class ParksNPeaks(HTTPSpotProvider):
"""Spot provider for Parks n Peaks"""
@@ -91,7 +93,7 @@ class ParksNPeaks(HTTPSpotProvider):
"SANPCPA",
"LLOTA",
]:
logging.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!")
logger.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!")
# Add new spot to the list
new_spots.append(spot)
+10 -8
View File
@@ -11,6 +11,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class RBN(SpotProvider):
"""Spot provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8
@@ -46,15 +48,15 @@ class RBN(SpotProvider):
while not connected and self._running:
try:
self.status = "Connecting"
logging.info(f"RBN port {self._port!s} connecting...")
logger.info(f"RBN port {self._port!s} connecting...")
self._telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self._port)
self._telnet.read_until("Please enter your call: ".encode("latin-1"))
self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1"))
connected = True
logging.info(f"RBN port {self._port!s} connected.")
logger.info(f"RBN port {self._port!s} connected.")
except Exception:
self.status = "Error"
logging.exception(f"Exception while connecting to RBN (port {self._port!s}).")
logger.exception(f"Exception while connecting to RBN (port {self._port!s}).")
sleep(5)
self.status = "Waiting for Data"
@@ -84,25 +86,25 @@ class RBN(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Data received from RBN on port {self._port!s}.")
logger.debug(f"Data received from RBN on port {self._port!s}.")
except EOFError:
connected = False
if self._running:
self.status = "Restarting"
logging.warning(f"Disconnected from RBN provider (port {self._port!s}). Reconnecting...")
logger.warning(f"Disconnected from RBN provider (port {self._port!s}). Reconnecting...")
sleep(5)
else:
logging.info(f"RBN provider (port {self._port!s}) shutting down...")
logger.info(f"RBN provider (port {self._port!s}) shutting down...")
self.status = "Shutting down"
except Exception:
connected = False
if self._running:
self.status = "Error"
logging.exception(f"Exception in RBN provider (port {self._port!s})")
logger.exception(f"Exception in RBN provider (port {self._port!s})")
sleep(5)
else:
logging.info(f"RBN provider (port {self._port!s}) shutting down...")
logger.info(f"RBN provider (port {self._port!s}) shutting down...")
self.status = "Shutting down"
self.status = "Disconnected"
+4 -2
View File
@@ -9,6 +9,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class SOTA(HTTPSpotProvider):
"""Spot provider for Summits on the Air"""
@@ -73,9 +75,9 @@ class SOTA(HTTPSpotProvider):
# that for us.
new_spots.append(spot)
except ConnectionError:
logging.warning("Connection error when accessing SOTA spots API")
logger.warning("Connection error when accessing SOTA spots API")
except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing SOTA spots API.")
logger.warning("Timeout when accessing SOTA spots API.")
return new_spots
def can_submit_spot(self, sig):
+9 -7
View File
@@ -8,6 +8,8 @@ from requests_sse import EventSource
from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class SSESpotProvider(SpotProvider):
"""Spot provider using Server-Sent Events."""
@@ -22,7 +24,7 @@ class SSESpotProvider(SpotProvider):
self._event_source = None
def start(self):
logging.info(f"Set up SSE connection to {self.name} spot API.")
logger.info(f"Set up SSE connection to {self.name} spot API.")
self._stop_event.clear()
self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}")
self._thread.daemon = True
@@ -37,12 +39,12 @@ class SSESpotProvider(SpotProvider):
try:
event_source.close()
except Exception:
logging.exception(f"Exception closing SSE connection for {self.name} during stop()")
logger.exception(f"Exception closing SSE connection for {self.name} during stop()")
if self._thread:
self._thread.join(timeout=15)
if self._thread.is_alive():
logging.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.")
logger.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.")
def _on_open(self):
self.status = "Waiting for Data"
@@ -57,7 +59,7 @@ class SSESpotProvider(SpotProvider):
def _run(self):
while not self._stop_event.is_set():
try:
logging.debug(f"Connecting to {self.name} spot API...")
logger.debug(f"Connecting to {self.name} spot API...")
self.status = "Connecting"
with EventSource(
self._url,
@@ -81,10 +83,10 @@ class SSESpotProvider(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.")
logger.debug(f"Received data from {self.name} spot API.")
except Exception:
logging.exception(
logger.exception(
f"Exception processing message from SSE Spot Provider ({self.name})"
)
finally:
@@ -92,7 +94,7 @@ class SSESpotProvider(SpotProvider):
except Exception:
self.status = "Error"
logging.exception(f"Exception in SSE Spot Provider ({self.name})")
logger.exception(f"Exception in SSE Spot Provider ({self.name})")
else:
self.status = "Disconnected"
self._stop_event.wait(timeout=5) # Wait before trying to reconnect
+7 -5
View File
@@ -9,6 +9,8 @@ from websocket import create_connection
from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class WebsocketSpotProvider(SpotProvider):
"""Spot provider using websockets."""
@@ -22,7 +24,7 @@ class WebsocketSpotProvider(SpotProvider):
self._last_event_id = None
def start(self):
logging.info(f"Set up websocket connection to {self.name} spot API.")
logger.info(f"Set up websocket connection to {self.name} spot API.")
self._stopped = False
self._thread = Thread(target=self._run, name=f"WebsocketSpotProvider-{self.name}")
self._thread.daemon = True
@@ -44,7 +46,7 @@ class WebsocketSpotProvider(SpotProvider):
def _run(self):
while not self._stopped:
try:
logging.debug(f"Connecting to {self.name} spot API...")
logger.debug(f"Connecting to {self.name} spot API...")
self.status = "Connecting"
self._ws = create_connection(self._url, header=HTTP_HEADERS)
self.status = "Connected"
@@ -57,14 +59,14 @@ class WebsocketSpotProvider(SpotProvider):
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.")
logger.debug(f"Received data from {self.name} spot API.")
except Exception:
logging.exception(f"Exception processing message from Websocket Spot Provider ({self.name})")
logger.exception(f"Exception processing message from Websocket Spot Provider ({self.name})")
except Exception as e:
self.status = "Error"
logging.exception(f"Exception in Websocket Spot Provider ({self.name})", e)
logger.exception(f"Exception in Websocket Spot Provider ({self.name})", e)
else:
self.status = "Disconnected"
sleep(5) # Wait before trying to reconnect
+3 -1
View File
@@ -11,6 +11,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class WOTA(HTTPSpotProvider):
"""Spot provider for Wainwrights on the Air"""
@@ -82,7 +84,7 @@ class WOTA(HTTPSpotProvider):
new_spots.append(spot)
except Exception as e:
logging.error("Exception parsing WOTA spot", e)
logger.error("Exception parsing WOTA spot", e)
return new_spots
def can_submit_spot(self, sig):
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.staticdata.local_file_static_data_provider import (
LocalFileStaticDataProvider,
)
logger = logging.getLogger(__name__)
class CQZoneData(LocalFileStaticDataProvider):
"""Static data provider for CQ zone geodata."""
@@ -34,5 +36,5 @@ class CQZoneData(LocalFileStaticDataProvider):
return True
except Exception:
logging.exception("Exception when loading CQ zone data.")
logger.exception("Exception when loading CQ zone data.")
return False
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache
from providers.staticdata.static_data_provider import StaticDataProvider
logger = logging.getLogger(__name__)
class FileDownloadStaticDataProvider(StaticDataProvider):
"""Generic static reference data provider class for providers that fetch their data from the web by downloading a
@@ -27,7 +29,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} static reference data every {self._poll_interval!s} days.")
logger.info(f"Set up query of {self.name} static reference data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadStaticDataProvider-{self.name}")
self._thread.start()
@@ -44,7 +46,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
try:
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
# caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.name} static reference data...")
logger.debug(f"Downloading {self.name} static reference data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good
if http_response.ok:
@@ -53,22 +55,22 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
if ok:
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated static reference data for {self.name}")
logger.info(f"Updated static reference data for {self.name}")
else:
self.status = "Error"
logging.warning(
logger.warning(
f"HTTP {http_response.status_code} when downloading static reference data for {self.name}."
)
except ConnectionError:
self.status = "Error"
logging.warning(f"Connection error when downloading static reference data for {self.name}.")
logger.warning(f"Connection error when downloading static reference data for {self.name}.")
except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logging.warning(f"Timeout when downloading static reference data for {self.name}.")
logger.warning(f"Timeout when downloading static reference data for {self.name}.")
except Exception:
self.status = "Error"
logging.exception(f"Exception in HTTP static reference data provider ({self.name})")
logger.exception(f"Exception in HTTP static reference data provider ({self.name})")
self._stop_event.wait(timeout=1)
def _handle_http_response(self, http_response):
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.staticdata.local_file_static_data_provider import (
LocalFileStaticDataProvider,
)
logger = logging.getLogger(__name__)
class ITUZoneData(LocalFileStaticDataProvider):
"""Static data provider for ITU zone geodata."""
@@ -34,5 +36,5 @@ class ITUZoneData(LocalFileStaticDataProvider):
return True
except Exception:
logging.exception("Exception when loading ITU zone data.")
logger.exception("Exception when loading ITU zone data.")
return False
+3 -1
View File
@@ -5,6 +5,8 @@ from providers.staticdata.file_download_static_data_provider import (
FileDownloadStaticDataProvider,
)
logger = logging.getLogger(__name__)
class K0SWE(FileDownloadStaticDataProvider):
"""Static data provider for K0SWE's dxcc.json, which provides callsign regex to DXCC entity mapping, plus DXCC to
@@ -44,5 +46,5 @@ class K0SWE(FileDownloadStaticDataProvider):
return True
except Exception:
logging.exception("Exception when loading K0SWE dxcc.json.")
logger.exception("Exception when loading K0SWE dxcc.json.")
return False
@@ -5,6 +5,8 @@ import pytz
from providers.staticdata.static_data_provider import StaticDataProvider
logger = logging.getLogger(__name__)
class LocalFileStaticDataProvider(StaticDataProvider):
"""Generic static reference data provider class for providers that fetch their data from a local file on startup."""
@@ -15,19 +17,19 @@ class LocalFileStaticDataProvider(StaticDataProvider):
self._stop = False
def start(self):
logging.debug(f"Loading {self.name} static reference data from file.")
logger.debug(f"Loading {self.name} static reference data from file.")
try:
ok = self._load_data(self._path)
if ok:
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated static reference data for {self.name}")
logger.info(f"Updated static reference data for {self.name}")
else:
self.status = "Error"
logging.error(f"Failed to load data for {self.name}")
logger.error(f"Failed to load data for {self.name}")
except Exception:
self.status = "Error"
logging.exception(f"Exception in local file Static Data Provider ({self.name})")
logger.exception(f"Exception in local file Static Data Provider ({self.name})")
def stop(self):
self._stop = True
+5 -3
View File
@@ -18,6 +18,8 @@ from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
@@ -220,7 +222,7 @@ class APISpotHandler(tornado.web.RequestHandler):
except NotImplementedError as e:
upstream_warning = str(e)
except Exception:
logging.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
logger.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
upstream_warning = (
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
)
@@ -244,7 +246,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to add spot API")
logger.exception("Exception when handling client request to add spot API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
self.set_header("Cache-Control", "no-store")
@@ -270,5 +272,5 @@ class APISpotHandler(tornado.web.RequestHandler):
)
return response.ok and response.json().get("success", False)
except Exception:
logging.exception("reCAPTCHA verification request failed")
logger.exception("reCAPTCHA verification request failed")
return False
+5 -3
View File
@@ -13,6 +13,8 @@ from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
logger = logging.getLogger(__name__)
class APIAlertsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/alerts"""
@@ -63,7 +65,7 @@ class APIAlertsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(f"Bad request - {e!s}"))
self.set_status(400)
except Exception:
logging.exception("Exception when handling client request to alerts API")
logger.exception("Exception when handling client request to alerts API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
self.set_header("Cache-Control", "no-store")
@@ -110,7 +112,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._sse_alert_broadcaster.register(self)
except Exception:
logging.exception("Exception when serving SSE socket")
logger.exception("Exception when serving SSE socket")
self.close()
def close(self):
@@ -129,7 +131,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
alert.infer_missing(self._credentials)
self.write_message(msg=safe_json_dumps(alert))
except Exception:
logging.exception("Exception in SSE callback, connection will be closed")
logger.exception("Exception in SSE callback, connection will be closed")
self.close()
+3 -1
View File
@@ -12,6 +12,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"]
CONTINENTS_SET = frozenset(CONTINENTS)
@@ -68,6 +70,6 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to dx stats API")
logger.exception("Exception when handling client request to dx stats API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+5 -3
View File
@@ -22,6 +22,8 @@ from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
class APILookupCallHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/lookup/call"""
@@ -66,7 +68,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
self.set_status(422)
except Exception:
logging.exception("Exception when handling client request to call lookup API")
logger.exception("Exception when handling client request to call lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
@@ -124,7 +126,7 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
self.set_status(422)
except Exception:
logging.exception("Exception when handling client request to sig ref lookup API")
logger.exception("Exception when handling client request to sig ref lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
@@ -192,7 +194,7 @@ class APILookupGridHandler(tornado.web.RequestHandler):
self.set_status(422)
except Exception:
logging.exception("Exception when handling client request to grid ref lookup API")
logger.exception("Exception when handling client request to grid ref lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+3 -1
View File
@@ -19,6 +19,8 @@ from core.constants import (
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APIOptionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/options"""
@@ -119,6 +121,6 @@ class APIOptionsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to options API")
logger.exception("Exception when handling client request to options API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+3 -1
View File
@@ -10,6 +10,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APISolarConditionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/solar"""
@@ -42,6 +44,6 @@ class APISolarConditionsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to solar conditions API")
logger.exception("Exception when handling client request to solar conditions API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+5 -3
View File
@@ -13,6 +13,8 @@ from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
logger = logging.getLogger(__name__)
class APISpotsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/spots"""
@@ -63,7 +65,7 @@ class APISpotsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(f"Bad request - {e!s}"))
self.set_status(400)
except Exception:
logging.exception("Excedption when handling client request to spots API")
logger.exception("Excedption when handling client request to spots API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
self.set_header("Cache-Control", "no-store")
@@ -112,7 +114,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._sse_spot_broadcaster.register(self)
except Exception:
logging.exception("Exception when serving SSE socket")
logger.exception("Exception when serving SSE socket")
self.close()
def close(self):
@@ -132,7 +134,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
spot.infer_missing(self._credentials)
self.write_message(msg=safe_json_dumps(spot))
except Exception:
logging.exception("Exception in SSE callback, connection will be closed")
logger.exception("Exception in SSE callback, connection will be closed")
self.close()
+3 -1
View File
@@ -10,6 +10,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APIStatusHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/status"""
@@ -42,6 +44,6 @@ class APIStatusHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to status API")
logger.exception("Exception when handling client request to status API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+3 -1
View File
@@ -15,6 +15,8 @@ from core.sig_utils import get_ref_regex_for_sig
from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot
logger = logging.getLogger(__name__)
class V1APISpotHandler(tornado.web.RequestHandler):
"""API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility."""
@@ -143,7 +145,7 @@ class V1APISpotHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json")
except Exception:
logging.exception("Exception when handling client request to add spot API")
logger.exception("Exception when handling client request to add spot API")
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
self.set_header("Cache-Control", "no-store")
+3 -1
View File
@@ -3,6 +3,8 @@ import threading
from tornado.ioloop import IOLoop
logger = logging.getLogger(__name__)
class SSEBroadcaster:
"""Bridge between DataStore listener callbacks (which fire on provider threads) to Tornado's async SSE handlers
@@ -35,5 +37,5 @@ class SSEBroadcaster:
handler.callback(value)
except Exception:
# Connection probably dropped, ignore and de-register the handler to stop getting future items.
logging.debug("Failed to push to an SSE client; dropping it")
logger.debug("Failed to push to an SSE client; dropping it")
self.unregister(handler)
+7 -5
View File
@@ -34,6 +34,8 @@ from server.handlers.metrics import PrometheusMetricsHandler
from server.handlers.pagetemplate import PageTemplateHandler
from server.sse_broadcaster import SSEBroadcaster
logger = logging.getLogger(__name__)
_HERE = os.path.dirname(__file__ or "")
@@ -159,7 +161,7 @@ class WebServer:
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
if self._api_only_mode:
logging.info("API-only mode is enabled. Web UI will not be served.")
logger.info("API-only mode is enabled. Web UI will not be served.")
ui_routes = [
(
r"/",
@@ -238,8 +240,8 @@ class WebServer:
debug=False,
)
app.listen(self._port, xheaders=True)
logging.info(f"Web server running on port {WEB_SERVER_PORT!s}")
logging.info(f"You can access your copy of Spothole at {BASE_URL}")
logger.info(f"Web server running on port {WEB_SERVER_PORT!s}")
logger.info(f"You can access your copy of Spothole at {BASE_URL}")
await self._shutdown_event.wait()
@@ -249,9 +251,9 @@ def request_log(handler):
if LOG_WEB_REQUESTS:
if handler.get_status() < 500:
log_method = logging.info
log_method = logger.info
else:
log_method = logging.warning
log_method = logger.warning
request = handler.request
client_ip = request.remote_ip
+5 -3
View File
@@ -12,6 +12,8 @@ from core.data_store import DATA_STORE
from core.status_reporter import StatusReporter
from server.webserver import WEB_SERVER
logger = logging.getLogger(__name__)
# Globals
run = True
@@ -21,7 +23,7 @@ def shutdown(_signum=None, _frame=None):
global run
logging.info("Stopping program...")
logger.info("Stopping program...")
WEB_SERVER.stop()
DATA_PROVIDERS.stop()
CLEANUP_TIMER.stop()
@@ -41,8 +43,8 @@ if __name__ == "__main__":
root.handlers.clear()
root.addHandler(handler)
logging.info("Starting...")
logging.info(f"This is Spothole version {SOFTWARE_VERSION}. This instance is run by {SERVER_OWNER_CALLSIGN}.")
logger.info("Starting...")
logger.info(f"This is Spothole version {SOFTWARE_VERSION}. This instance is run by {SERVER_OWNER_CALLSIGN}.")
# Shut down gracefully on SIGINT
signal.signal(signal.SIGINT, shutdown)
+1 -1
View File
@@ -76,7 +76,7 @@
</div>
<script src="/static/js/add-spot.js?v=1786779098"></script>
<script src="/static/js/add-spot.js?v=1786779306"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -82,7 +82,7 @@
</div>
<script src="/static/js/alerts.js?v=1786779098"></script>
<script src="/static/js/alerts.js?v=1786779307"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -79,8 +79,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script>
<script src="/static/js/bands.js?v=1786779098"></script>
<script src="/static/js/spotsbandsandmap.js?v=1786779307"></script>
<script src="/static/js/bands.js?v=1786779307"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1786779098" type="text/css">
<link rel="stylesheet" href="/static/css/style.css?v=1786779306" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -15,10 +15,10 @@
window.fetchEventSource = fetchEventSource;
</script>
<script src="/static/js/utils.js?v=1786779098"></script>
<script src="/static/js/ui-ham.js?v=1786779098"></script>
<script src="/static/js/geo.js?v=1786779098"></script>
<script src="/static/js/common.js?v=1786779098"></script>
<script src="/static/js/utils.js?v=1786779306"></script>
<script src="/static/js/ui-ham.js?v=1786779306"></script>
<script src="/static/js/geo.js?v=1786779306"></script>
<script src="/static/js/common.js?v=1786779306"></script>
{% end %}
{% block body %}
<div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1786779098"></script>
<script src="/static/js/conditions.js?v=1786779307"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -112,8 +112,8 @@
<script src="/static/vendor/js/leaflet-cqzones.js"></script>
<script src="/static/vendor/js/leaflet-workedallbritainireland.js" type="module"></script>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script>
<script src="/static/js/map.js?v=1786779098"></script>
<script src="/static/js/spotsbandsandmap.js?v=1786779307"></script>
<script src="/static/js/map.js?v=1786779307"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -118,8 +118,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script>
<script src="/static/js/spots.js?v=1786779098"></script>
<script src="/static/js/spotsbandsandmap.js?v=1786779306"></script>
<script src="/static/js/spots.js?v=1786779306"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -86,7 +86,7 @@
</div>
</div>
<script src="/static/js/status.js?v=1786779098"></script>
<script src="/static/js/status.js?v=1786779307"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");