From 60aa7a875bc35c9cb5fb7afdfede80fe77e6393a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Wed, 19 Jun 2024 13:37:11 +0200 Subject: [PATCH] feat(api): add OpenMetrics renderer for metrics This makes it compatible with Prometheus. Fixes #9285 --- docs/api.rst | 4 ++++ docs/changes.rst | 1 + weblate/api/renderers.py | 28 ++++++++++++++++++++++++++++ weblate/api/tests.py | 5 +++++ weblate/api/views.py | 4 ++++ 5 files changed, 42 insertions(+) create mode 100644 weblate/api/renderers.py diff --git a/docs/api.rst b/docs/api.rst index bdd4214dab3e..44ce2227dddc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2560,6 +2560,10 @@ Metrics Returns server metrics. + .. versionchanged:: 5.6.1 + + Metrics can now be exposed in OpenMetrics compatible format with ``?format=openmetrics``. + :>json int units: Number of units :>json int units_translated: Number of translated units :>json int users: Number of users diff --git a/docs/changes.rst b/docs/changes.rst index 6997ef1b2345..7f6e2a5f4629 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,6 +8,7 @@ Not yet released. **Improvements** * Docker container accepts :envvar:`WEBLATE_REMOVE_ADDONS` and :envvar:`WEBLATE_ADD_MACHINERY` to customize automatic suggestion services. +* Added OpenMetrics compatibility for :http:get:`/api/metrics/`. **Bug fixes** diff --git a/weblate/api/renderers.py b/weblate/api/renderers.py new file mode 100644 index 000000000000..4b4302a56471 --- /dev/null +++ b/weblate/api/renderers.py @@ -0,0 +1,28 @@ +# Copyright © Michal Čihař +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework.renderers import BaseRenderer + + +class OpenMetricsRenderer(BaseRenderer): + media_type = "application/openmetrics-text" + format = "openmetrics" + charset = "utf-8" + render_style = "text" + + def render(self, data, accepted_media_type=None, renderer_context=None): + result = [] + for key, value in data.items(): + if isinstance(value, str): + # Strings not supported + continue + if isinstance(value, int | float): + result.append(f"{key} {value}") + elif isinstance(value, dict): + # Celery queues + for queue, stat in value.items(): + result.append(f'{key}(queue="{queue}") {stat}') + + result.append("# EOF") + return "\n".join(result) diff --git a/weblate/api/tests.py b/weblate/api/tests.py index 8f0b3019a23e..f95eb56d9f8b 100644 --- a/weblate/api/tests.py +++ b/weblate/api/tests.py @@ -3908,6 +3908,11 @@ def test_metrics(self) -> None: response = self.client.get(reverse("api:metrics")) self.assertEqual(response.data["projects"], 1) + def test_metrics_openmetrics(self) -> None: + self.authenticate() + response = self.client.get(reverse("api:metrics"), {"format": "openmetrics"}) + self.assertContains(response, "# EOF") + def test_forbidden(self) -> None: response = self.client.get(reverse("api:metrics")) self.assertEqual(response.data["detail"].code, "not_authenticated") diff --git a/weblate/api/views.py b/weblate/api/views.py index 96b372a235d3..e904b4a22eb4 100644 --- a/weblate/api/views.py +++ b/weblate/api/views.py @@ -24,6 +24,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, UpdateModelMixin from rest_framework.permissions import IsAuthenticated +from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.settings import api_settings @@ -115,6 +116,8 @@ from weblate.utils.views import download_translation_file, zip_download from weblate.wladmin.models import ConfigurationError +from .renderers import OpenMetricsRenderer + REPO_OPERATIONS = { "push": ("vcs.push", "do_push", (), True), "pull": ("vcs.update", "do_update", (), True), @@ -1812,6 +1815,7 @@ class Metrics(APIView): """Metrics view for monitoring.""" permission_classes = (IsAuthenticated,) + renderer_classes = (JSONRenderer, BrowsableAPIRenderer, OpenMetricsRenderer) def get(self, request, format=None): # noqa: A002 stats = GlobalStats()