From d4634030185fe0324ef0cc8a617815c6bae6dd3c Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Tue, 23 Dec 2025 14:23:50 +0000 Subject: [PATCH] Implement web server metrics in Tornado #3 --- core/status_reporter.py | 18 ++++++----- server/handlers/api/addspot.py | 12 +++++++- server/handlers/api/alerts.py | 13 +++++++- server/handlers/api/lookups.py | 21 +++++++++++++ server/handlers/api/options.py | 12 +++++++- server/handlers/api/spots.py | 13 +++++++- server/handlers/api/status.py | 12 +++++++- server/handlers/pagetemplate.py | 14 ++++++++- server/webserver.py | 54 ++++++++++++++------------------- 9 files changed, 124 insertions(+), 45 deletions(-) diff --git a/core/status_reporter.py b/core/status_reporter.py index 9119b9f..91a4808 100644 --- a/core/status_reporter.py +++ b/core/status_reporter.py @@ -58,13 +58,17 @@ class StatusReporter: self.status_data["cleanup"] = {"status": self.cleanup_timer.status, "last_ran": self.cleanup_timer.last_cleanup_time.replace( tzinfo=pytz.UTC).timestamp() if self.cleanup_timer.last_cleanup_time else 0} - self.status_data["webserver"] = {"status": self.web_server.status, - "last_api_access": self.web_server.last_api_access_time.replace( - tzinfo=pytz.UTC).timestamp() if self.web_server.last_api_access_time else 0, - "api_access_count": self.web_server.api_access_counter, - "last_page_access": self.web_server.last_page_access_time.replace( - tzinfo=pytz.UTC).timestamp() if self.web_server.last_page_access_time else 0, - "page_access_count": self.web_server.page_access_counter} + self.status_data["webserver"] = {"status": self.web_server.web_server_metrics["status"], + "last_api_access": self.web_server.web_server_metrics[ + "last_api_access_time"].replace( + tzinfo=pytz.UTC).timestamp() if self.web_server.web_server_metrics[ + "last_api_access_time"] else 0, + "api_access_count": self.web_server.web_server_metrics["api_access_counter"], + "last_page_access": self.web_server.web_server_metrics[ + "last_page_access_time"].replace( + tzinfo=pytz.UTC).timestamp() if self.web_server.web_server_metrics[ + "last_page_access_time"] else 0, + "page_access_count": self.web_server.web_server_metrics["page_access_counter"]} # Update Prometheus metrics memory_use_gauge.set(psutil.Process(os.getpid()).memory_info().rss * 1024) diff --git a/server/handlers/api/addspot.py b/server/handlers/api/addspot.py index d6601c8..b37b916 100644 --- a/server/handlers/api/addspot.py +++ b/server/handlers/api/addspot.py @@ -1,12 +1,15 @@ import json import logging import re +from datetime import datetime +import pytz import tornado from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE from core.constants import UNKNOWN_BAND from core.lookup_helper import lookup_helper +from core.prometheus_metrics_handler import api_requests_counter from core.sig_utils import get_ref_regex_for_sig from core.utils import serialize_everything from data.sig_ref import SIGRef @@ -15,11 +18,18 @@ from data.spot import Spot # API request handler for /api/v1/spot (POST) class APISpotHandler(tornado.web.RequestHandler): - def initialize(self, spots): + def initialize(self, spots, web_server_metrics): self.spots = spots + self.web_server_metrics = web_server_metrics def post(self): try: + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + # Reject if not allowed if not ALLOW_SPOTTING: self.set_status(401) diff --git a/server/handlers/api/alerts.py b/server/handlers/api/alerts.py index f0067e0..ed4c53b 100644 --- a/server/handlers/api/alerts.py +++ b/server/handlers/api/alerts.py @@ -5,16 +5,24 @@ from datetime import datetime import pytz import tornado +from core.prometheus_metrics_handler import api_requests_counter from core.utils import serialize_everything # API request handler for /api/v1/alerts class APIAlertsHandler(tornado.web.RequestHandler): - def initialize(self, alerts): + def initialize(self, alerts, web_server_metrics): self.alerts = alerts + self.web_server_metrics = web_server_metrics def get(self): try: + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, # reduce that to just the first entry, and convert bytes to string query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} @@ -39,6 +47,9 @@ class APIAlertsStreamHandler(tornado.web.RequestHandler): def get(self): # todo # try: + # # Metrics + # api_requests_counter.inc() + # # response.content_type = 'text/event-stream' # response.cache_control = 'no-cache' # yield 'retry: 1000\n\n' diff --git a/server/handlers/api/lookups.py b/server/handlers/api/lookups.py index b1e324c..6578b45 100644 --- a/server/handlers/api/lookups.py +++ b/server/handlers/api/lookups.py @@ -1,10 +1,13 @@ import json import logging import re +from datetime import datetime +import pytz import tornado from core.constants import SIGS +from core.prometheus_metrics_handler import api_requests_counter from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info from core.utils import serialize_everything from data.sig_ref import SIGRef @@ -13,8 +16,17 @@ from data.spot import Spot # API request handler for /api/v1/lookup/call class APILookupCallHandler(tornado.web.RequestHandler): + def initialize(self, web_server_metrics): + self.web_server_metrics = web_server_metrics + def get(self): try: + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, # reduce that to just the first entry, and convert bytes to string query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} @@ -63,8 +75,17 @@ class APILookupCallHandler(tornado.web.RequestHandler): # API request handler for /api/v1/lookup/sigref class APILookupSIGRefHandler(tornado.web.RequestHandler): + def initialize(self, web_server_metrics): + self.web_server_metrics = web_server_metrics + def get(self): try: + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, # reduce that to just the first entry, and convert bytes to string query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} diff --git a/server/handlers/api/options.py b/server/handlers/api/options.py index 6aaed90..a7a5084 100644 --- a/server/handlers/api/options.py +++ b/server/handlers/api/options.py @@ -1,18 +1,28 @@ import json +from datetime import datetime +import pytz import tornado from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING, WEB_UI_OPTIONS from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS +from core.prometheus_metrics_handler import api_requests_counter from core.utils import serialize_everything # API request handler for /api/v1/options class APIOptionsHandler(tornado.web.RequestHandler): - def initialize(self, status_data): + def initialize(self, status_data, web_server_metrics): self.status_data = status_data + self.web_server_metrics = web_server_metrics def get(self): + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + options = {"bands": BANDS, "modes": ALL_MODES, "mode_types": MODE_TYPES, diff --git a/server/handlers/api/spots.py b/server/handlers/api/spots.py index bb1d67f..b02ead0 100644 --- a/server/handlers/api/spots.py +++ b/server/handlers/api/spots.py @@ -5,16 +5,24 @@ from datetime import datetime, timedelta import pytz import tornado +from core.prometheus_metrics_handler import api_requests_counter from core.utils import serialize_everything # API request handler for /api/v1/spots class APISpotsHandler(tornado.web.RequestHandler): - def initialize(self, spots): + def initialize(self, spots, web_server_metrics): self.spots = spots + self.web_server_metrics = web_server_metrics def get(self): try: + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, # reduce that to just the first entry, and convert bytes to string query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} @@ -40,6 +48,9 @@ class APISpotsStreamHandler(tornado.web.RequestHandler): def get(self): # todo # try: + # # Metrics + # api_requests_counter.inc() + # # response.content_type = 'text/event-stream' # response.cache_control = 'no-cache' # yield 'retry: 1000\n\n' diff --git a/server/handlers/api/status.py b/server/handlers/api/status.py index 9a5219e..39808a4 100644 --- a/server/handlers/api/status.py +++ b/server/handlers/api/status.py @@ -1,16 +1,26 @@ import json +from datetime import datetime +import pytz import tornado +from core.prometheus_metrics_handler import api_requests_counter from core.utils import serialize_everything # API request handler for /api/v1/status class APIStatusHandler(tornado.web.RequestHandler): - def initialize(self, status_data): + def initialize(self, status_data, web_server_metrics): self.status_data = status_data + self.web_server_metrics = web_server_metrics def get(self): + # Metrics + self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["api_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + api_requests_counter.inc() + self.write(json.dumps(self.status_data, default=serialize_everything)) self.set_status(200) self.set_header("Cache-Control", "no-store") diff --git a/server/handlers/pagetemplate.py b/server/handlers/pagetemplate.py index 984a613..c1c9751 100644 --- a/server/handlers/pagetemplate.py +++ b/server/handlers/pagetemplate.py @@ -1,14 +1,26 @@ +from datetime import datetime + +import pytz import tornado from core.config import ALLOW_SPOTTING from core.constants import SOFTWARE_VERSION +from core.prometheus_metrics_handler import page_requests_counter # Handler for all HTML pages generated from templates class PageTemplateHandler(tornado.web.RequestHandler): - def initialize(self, template_name): + def initialize(self, template_name, web_server_metrics): self.template_name = template_name + self.web_server_metrics = web_server_metrics def get(self): + # Metrics + self.web_server_metrics["last_page_access_time"] = datetime.now(pytz.UTC) + self.web_server_metrics["page_access_counter"] += 1 + self.web_server_metrics["status"] = "OK" + page_requests_counter.inc() + # Load named template, and provide variables used in templates self.render(self.template_name + ".html", software_version=SOFTWARE_VERSION, allow_spotting=ALLOW_SPOTTING) + diff --git a/server/webserver.py b/server/webserver.py index 2416ac1..73e8a81 100644 --- a/server/webserver.py +++ b/server/webserver.py @@ -15,35 +15,25 @@ from server.handlers.pagetemplate import PageTemplateHandler # Provides the public-facing web server. -# TODO test lookups # TODO SSE API responses # TODO clean_up_sse_queues -# TODO page & API access counters - how to do from a subclass handler? e.g. -# self.last_api_access_time = datetime.now(pytz.UTC) -# self.api_access_counter += 1 -# api_requests_counter.inc() -# self.status = "OK" -# -# self.last_page_access_time = datetime.now(pytz.UTC) -# self.page_access_counter += 1 -# page_requests_counter.inc() -# self.status = "OK" - class WebServer: # Constructor def __init__(self, spots, alerts, status_data, port): - self.last_page_access_time = None - self.last_api_access_time = None - self.page_access_counter = 0 - self.api_access_counter = 0 self.spots = spots self.alerts = alerts self.sse_spot_queues = [] self.sse_alert_queues = [] self.status_data = status_data self.port = port - self.status = "Starting" self.shutdown_event = asyncio.Event() + self.web_server_metrics = { + "last_page_access_time": None, + "last_api_access_time": None, + "page_access_counter": 0, + "api_access_counter": 0, + "status": "Starting" + } # Start the web server def start(self): @@ -57,24 +47,24 @@ class WebServer: async def start_inner(self): app = tornado.web.Application([ # Routes for API calls - (r"/api/v1/spots", APISpotsHandler, {"spots": self.spots}), - (r"/api/v1/alerts", APIAlertsHandler, {"alerts": self.alerts}), + (r"/api/v1/spots", APISpotsHandler, {"spots": self.spots, "web_server_metrics": self.web_server_metrics}), + (r"/api/v1/alerts", APIAlertsHandler, {"alerts": self.alerts, "web_server_metrics": self.web_server_metrics}), (r"/api/v1/spots/stream", APISpotsStreamHandler), # todo provide queues? (r"/api/v1/alerts/stream", APIAlertsStreamHandler), # todo provide queues? - (r"/api/v1/options", APIOptionsHandler, {"status_data": self.status_data}), - (r"/api/v1/status", APIStatusHandler, {"status_data": self.status_data}), - (r"/api/v1/lookup/call", APILookupCallHandler), - (r"/api/v1/lookup/sigref", APILookupSIGRefHandler), - (r"/api/v1/spot", APISpotHandler, {"spots": self.spots}), + (r"/api/v1/options", APIOptionsHandler, {"status_data": self.status_data, "web_server_metrics": self.web_server_metrics}), + (r"/api/v1/status", APIStatusHandler, {"status_data": self.status_data, "web_server_metrics": self.web_server_metrics}), + (r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}), + (r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {"web_server_metrics": self.web_server_metrics}), + (r"/api/v1/spot", APISpotHandler, {"spots": self.spots, "web_server_metrics": self.web_server_metrics}), # Routes for templated pages - (r"/", PageTemplateHandler, {"template_name": "spots"}), - (r"/map", PageTemplateHandler, {"template_name": "map"}), - (r"/bands", PageTemplateHandler, {"template_name": "bands"}), - (r"/alerts", PageTemplateHandler, {"template_name": "alerts"}), - (r"/add-spot", PageTemplateHandler, {"template_name": "add_spot"}), - (r"/status", PageTemplateHandler, {"template_name": "status"}), - (r"/about", PageTemplateHandler, {"template_name": "about"}), - (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs"}), + (r"/", PageTemplateHandler, {"template_name": "spots", "web_server_metrics": self.web_server_metrics}), + (r"/map", PageTemplateHandler, {"template_name": "map", "web_server_metrics": self.web_server_metrics}), + (r"/bands", PageTemplateHandler, {"template_name": "bands", "web_server_metrics": self.web_server_metrics}), + (r"/alerts", PageTemplateHandler, {"template_name": "alerts", "web_server_metrics": self.web_server_metrics}), + (r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", "web_server_metrics": self.web_server_metrics}), + (r"/status", PageTemplateHandler, {"template_name": "status", "web_server_metrics": self.web_server_metrics}), + (r"/about", PageTemplateHandler, {"template_name": "about", "web_server_metrics": self.web_server_metrics}), + (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", "web_server_metrics": self.web_server_metrics}), # Route for Prometheus metrics (r"/metrics", PrometheusMetricsHandler), # Default route to serve from "webassets"