From 9768f976c5781ab82c446d5d71535490738ec571 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Thu, 30 Oct 2025 13:32:10 +0000 Subject: [PATCH] Add prometheus metrics endpoint. Closes #67 --- core/prometheus_metrics_handler.py | 40 ++++++++++++++++++++++++++++++ core/status_reporter.py | 10 +++++++- requirements.txt | 3 ++- server/webserver.py | 16 ++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 core/prometheus_metrics_handler.py diff --git a/core/prometheus_metrics_handler.py b/core/prometheus_metrics_handler.py new file mode 100644 index 0000000..230ae1b --- /dev/null +++ b/core/prometheus_metrics_handler.py @@ -0,0 +1,40 @@ +from bottle import response +from prometheus_client import CollectorRegistry, generate_latest, CONTENT_TYPE_LATEST, Counter, disable_created_metrics, \ + Gauge + +disable_created_metrics() +# Prometheus metrics registry +registry = CollectorRegistry() + +page_requests_counter = Counter( + "page_requests", + "Total number of page requests received", + registry=registry, +) +api_requests_counter = Counter( + "api_requests", + "Total number of API requests received", + registry=registry +) +spots_gauge = Gauge( + "spots", + "Number of spots currently in the software", + registry=registry +) +alerts_gauge = Gauge( + "alerts", + "Number of alerts currently in the software", + registry=registry +) +memory_use_gauge = Gauge( + "memory_usage_bytes", + "Current memory usage of the software in bytes", + registry=registry +) + + +# Get a Prometheus metrics response for Bottle +def get_metrics(): + response.content_type = CONTENT_TYPE_LATEST + response.status = 200 + return generate_latest(registry) diff --git a/core/status_reporter.py b/core/status_reporter.py index acd23e2..9119b9f 100644 --- a/core/status_reporter.py +++ b/core/status_reporter.py @@ -7,6 +7,7 @@ import pytz from core.config import SERVER_OWNER_CALLSIGN from core.constants import SOFTWARE_VERSION +from core.prometheus_metrics_handler import memory_use_gauge, spots_gauge, alerts_gauge # Provides a timed update of the application's status data. @@ -60,8 +61,15 @@ class StatusReporter: 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} + tzinfo=pytz.UTC).timestamp() if self.web_server.last_page_access_time else 0, + "page_access_count": self.web_server.page_access_counter} + + # Update Prometheus metrics + memory_use_gauge.set(psutil.Process(os.getpid()).memory_info().rss * 1024) + spots_gauge.set(len(self.spots)) + alerts_gauge.set(len(self.alerts)) self.run_timer = Timer(self.run_interval, self.run) self.run_timer.start() diff --git a/requirements.txt b/requirements.txt index a1f3216..1dff514 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ diskcache~=5.6.3 psutil~=7.1.0 requests-sse~=0.5.2 rss-parser~=2.1.1 -pyproj~=3.7.2 \ No newline at end of file +pyproj~=3.7.2 +prometheus_client~=0.23.1 \ No newline at end of file diff --git a/server/webserver.py b/server/webserver.py index 0b53c28..f57159f 100644 --- a/server/webserver.py +++ b/server/webserver.py @@ -6,9 +6,11 @@ from threading import Thread import bottle import pytz from bottle import run, request, response, template +from prometheus_client import CONTENT_TYPE_LATEST, generate_latest from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION +from core.prometheus_metrics_handler import page_requests_counter, registry, get_metrics, api_requests_counter from data.spot import Spot @@ -19,6 +21,8 @@ class WebServer: 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.status_data = status_data @@ -44,6 +48,8 @@ class WebServer: bottle.get("/status")(lambda: self.serve_template('webpage_status')) bottle.get("/about")(lambda: self.serve_template('webpage_about')) bottle.get("/apidocs")(lambda: self.serve_template('webpage_apidocs')) + # Route for Prometheus metrics + bottle.get("/metrics")(lambda: self.serve_prometheus_metrics()) # Default route to serve from "webassets" bottle.get("/")(self.serve_static_file) @@ -92,6 +98,8 @@ class WebServer: # Serve a JSON API endpoint def serve_api(self, data): self.last_api_access_time = datetime.now(pytz.UTC) + self.api_access_counter += 1 + api_requests_counter.inc() self.status = "OK" response.content_type = 'application/json' response.set_header('Cache-Control', 'no-store') @@ -100,6 +108,8 @@ class WebServer: # Accept a spot def accept_spot(self): self.last_api_access_time = datetime.now(pytz.UTC) + self.api_access_counter += 1 + api_requests_counter.inc() self.status = "OK" try: @@ -153,6 +163,8 @@ class WebServer: # Serve a templated page def serve_template(self, template_name): self.last_page_access_time = datetime.now(pytz.UTC) + self.page_access_counter += 1 + page_requests_counter.inc() self.status = "OK" return template(template_name) @@ -160,6 +172,10 @@ class WebServer: def serve_static_file(self, filepath): return bottle.static_file(filepath, root="webassets") + # Serve Prometheus metrics + def serve_prometheus_metrics(self): + return get_metrics() + # Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in # the main "spots" GET call. def get_spot_list_with_filters(self):