Skip to content

Commit

Permalink
feat(api): add OpenMetrics renderer for metrics
Browse files Browse the repository at this point in the history
This makes it compatible with Prometheus.

Fixes #9285
  • Loading branch information
nijel committed Jun 19, 2024
1 parent c721aa5 commit 60aa7a8
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
28 changes: 28 additions & 0 deletions weblate/api/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# 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)
5 changes: 5 additions & 0 deletions weblate/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions weblate/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 60aa7a8

Please sign in to comment.