diff --git a/app/classes/models/server_stats.py b/app/classes/models/server_stats.py index 14f85ad3..ff871d30 100644 --- a/app/classes/models/server_stats.py +++ b/app/classes/models/server_stats.py @@ -3,11 +3,14 @@ import logging import datetime from datetime import timedelta +from prometheus_client import Gauge + from app.classes.models.servers import Servers, HelperServers from app.classes.shared.helpers import Helpers from app.classes.shared.main_models import DatabaseShortcuts from app.classes.shared.migration import MigrationManager + try: from peewee import ( SqliteDatabase, @@ -29,6 +32,13 @@ logger = logging.getLogger(__name__) peewee_logger = logging.getLogger("peewee") peewee_logger.setLevel(logging.INFO) +# REGISTRY Entries for Server Stats functions +ONLINE_PLAYERS = Gauge( + name="online_players", + documentation="The number of players online for a server", + labelnames=["server_id"], +) + # ********************************************************************************** # Servers Stats Class @@ -157,6 +167,8 @@ class HelperServerStats: self.database.connect(reuse_if_open=True) server_id = server_stats.get("id", 0) + ONLINE_PLAYERS.labels(f"{self.server_id}").set(server_stats.get("online")) + if server_id == 0: logger.warning("Stats saving failed with error: Server unknown (id = 0)") return diff --git a/app/classes/web/metrics_handler.py b/app/classes/web/metrics_handler.py new file mode 100644 index 00000000..41abfbf7 --- /dev/null +++ b/app/classes/web/metrics_handler.py @@ -0,0 +1,50 @@ +import logging +import typing as t + +from prometheus_client import REGISTRY, CollectorRegistry +from prometheus_client.exposition import _bake_output +from prometheus_client.exposition import parse_qs, urlparse + +from app.classes.web.base_api_handler import BaseApiHandler + +logger = logging.getLogger(__name__) + + +class BaseMetricsHandler(BaseApiHandler): + """HTTP handler that gives metrics from ``REGISTRY``.""" + + registry: CollectorRegistry = REGISTRY + + def get_registry(self) -> None: + # Prepare parameters + registry = self.registry + accept_header = self.request.headers.get("Accept") + accept_encoding_header = self.request.headers.get("Accept-Encoding") + params = parse_qs(urlparse(self.request.path).query) + # Bake output + status, headers, output = _bake_output( + registry, accept_header, accept_encoding_header, params, False + ) + # Return output + self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output) + + @classmethod + def factory(cls, registry: CollectorRegistry) -> type: + """Returns a dynamic MetricsHandler class tied + to the passed registry. + """ + # This implementation relies on MetricsHandler.registry + # (defined above and defaulted to REGISTRY). + + # As we have unicode_literals, we need to create a str() + # object for type(). + cls_name = str(cls.__name__) + MyMetricsHandler = type(cls_name, (cls, object), {"registry": registry}) + return MyMetricsHandler + + def finish_metrics(self, status: int, headers, data: t.Dict[str, t.Any]): + self.set_status(status) + self.set_header("Content-Type", "text/plain") + for header in headers: + self.set_header(*header) + self.finish(data) diff --git a/app/classes/web/routes/metrics/index.py b/app/classes/web/routes/metrics/index.py new file mode 100644 index 00000000..c08011a7 --- /dev/null +++ b/app/classes/web/routes/metrics/index.py @@ -0,0 +1,19 @@ +from prometheus_client import Info +from app.classes.web.metrics_handler import BaseMetricsHandler + +CRAFTY_INFO = Info("Crafty_Controller", "Infos of this Crafty Instance") + + +# Decorate function with metric. +class ApiOpenMetricsIndexHandler(BaseMetricsHandler): + def get(self): + auth_data = self.authenticate_user() + if not auth_data: + return + + version = f"{self.helper.get_version().get('major')}.{self.helper.get_version().get('minor')}.{self.helper.get_version().get('sub')}" + CRAFTY_INFO.info( + {"version": version, "docker": f"{self.helper.is_env_docker()}"} + ) + + self.get_registry() diff --git a/app/classes/web/routes/metrics/metrics_handlers.py b/app/classes/web/routes/metrics/metrics_handlers.py new file mode 100644 index 00000000..9c042862 --- /dev/null +++ b/app/classes/web/routes/metrics/metrics_handlers.py @@ -0,0 +1,18 @@ +from app.classes.web.routes.metrics.index import ApiOpenMetricsIndexHandler +from app.classes.web.routes.metrics.servers import ApiOpenMetricsServersHandler + + +def metrics_handlers(handler_args): + return [ + # OpenMetrics routes + ( + r"/metrics?", + ApiOpenMetricsIndexHandler, + handler_args, + ), + ( + r"/metrics/servers/(0-9)+?", + ApiOpenMetricsServersHandler, + handler_args, + ), + ] diff --git a/app/classes/web/routes/metrics/servers.py b/app/classes/web/routes/metrics/servers.py new file mode 100644 index 00000000..0e6e0860 --- /dev/null +++ b/app/classes/web/routes/metrics/servers.py @@ -0,0 +1,12 @@ +from prometheus_client import Histogram +from app.classes.web.metrics_handler import BaseMetricsHandler + + +# Decorate function with metric. +class ApiOpenMetricsServersHandler(BaseMetricsHandler): + def get(self): + auth_data = self.authenticate_user() + if not auth_data: + return + + self.get_registry() diff --git a/app/classes/web/tornado_handler.py b/app/classes/web/tornado_handler.py index d2b047d7..4bda6ad2 100644 --- a/app/classes/web/tornado_handler.py +++ b/app/classes/web/tornado_handler.py @@ -20,6 +20,7 @@ from app.classes.web.public_handler import PublicHandler from app.classes.web.panel_handler import PanelHandler from app.classes.web.default_handler import DefaultHandler from app.classes.web.routes.api.api_handlers import api_handlers +from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers from app.classes.web.server_handler import ServerHandler from app.classes.web.ajax_handler import AjaxHandler from app.classes.web.api_handler import ( @@ -169,6 +170,8 @@ class Webserver: (r"/api/v1/users/delete_user", DeleteUser, handler_args), # API Routes V2 *api_handlers(handler_args), + # API Routes OpenMetrics + *metrics_handlers(handler_args), # Using this one at the end # to catch all the other requests to Public Handler (r"/(.*)", PublicHandler, handler_args), diff --git a/requirements.txt b/requirements.txt index df3360a0..7b5a488f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ cryptography==41.0.1 libgravatar==1.0.0 peewee==3.13 pexpect==4.8 -psutil==5.9 +psutil==5.9.5 pyOpenSSL==23.2.0 pyjwt==2.4.0 PyYAML==6.0.1 @@ -19,3 +19,4 @@ tornado==6.3.2 tzlocal==4.0 jsonschema==4.5.1 orjson==3.8.12 +prometheus-client==0.17.1