From 1ddd12ef4db128330d36ede9697474249b3f469b Mon Sep 17 00:00:00 2001
From: Jan David Mol <mol@astron.nl>
Date: Wed, 31 Jul 2024 06:14:39 +0000
Subject: [PATCH] Add django prometheus plugin

---
 Docker/lofar-ci/Dockerfile_ci_tmss            |  2 +-
 SAS/TMSS/backend/src/CMakeLists.txt           |  1 +
 SAS/TMSS/backend/src/tmss/settings.py         |  9 ++++--
 .../backend/src/tmss/tmssapp/models/common.py | 29 ++++++++++++++++++-
 SAS/TMSS/backend/src/tmss/urls.py             |  3 +-
 5 files changed, 39 insertions(+), 5 deletions(-)

diff --git a/Docker/lofar-ci/Dockerfile_ci_tmss b/Docker/lofar-ci/Dockerfile_ci_tmss
index baeacab91c6..536de340546 100644
--- a/Docker/lofar-ci/Dockerfile_ci_tmss
+++ b/Docker/lofar-ci/Dockerfile_ci_tmss
@@ -30,7 +30,7 @@ COPY SAS/TMSS/backend/services/tuna/requirements.txt tmss_tuna.txt
 RUN pip3 install "setuptools>=62.6" "wheel>=0.40" "pip>=23.1.2"
 RUN pip3 install astroplan cachetools comet coreapi coverage cx_Oracle cython django-auth-ldap \
     django-debug-toolbar django-filter django-json-widget django-jsoneditor django-jsonforms \
-    django-material django-property-filter django-viewflow djangorestframework-xml \
+    django-material django-property-filter django-prometheus django-viewflow djangorestframework-xml \
     drf-flex-fields drf-yasg fabric flask flex gevent graphviz gunicorn h5py isodate \
     jsonschema kombu ldap3 lxml Markdown matplotlib mozilla-django-oidc \
     mysql-connector-python numpy orjson packaging psycopg2 pygcn pymysql python-dateutil \
diff --git a/SAS/TMSS/backend/src/CMakeLists.txt b/SAS/TMSS/backend/src/CMakeLists.txt
index 66279e7a412..5cc606a9831 100644
--- a/SAS/TMSS/backend/src/CMakeLists.txt
+++ b/SAS/TMSS/backend/src/CMakeLists.txt
@@ -11,6 +11,7 @@ find_python_module(django REQUIRED)
 find_python_module(rest_framework REQUIRED)     # pip3 install djangorestframework
 find_python_module(ldap REQUIRED)
 find_python_module(django_filters REQUIRED)     # pip3 install django-filter
+find_python_module(django_prometheus REQUIRED)  # pip3 install django-prometheus
 find_python_module(django_property_filter REQUIRED)  # pip3 install django-property-filter
 find_python_module(django_auth_ldap REQUIRED)   # sudo apt-get install python3-django-python3-ldap python3-django-auth-ldap
 find_python_module(django_jsonforms REQUIRED)   # pip3 install django-jsonforms
diff --git a/SAS/TMSS/backend/src/tmss/settings.py b/SAS/TMSS/backend/src/tmss/settings.py
index 696e889fdb6..1dda9915872 100644
--- a/SAS/TMSS/backend/src/tmss/settings.py
+++ b/SAS/TMSS/backend/src/tmss/settings.py
@@ -97,7 +97,8 @@ INSTALLED_APPS = [
     'jsoneditor',
     'drf_yasg',
     'django_filters',
-    'django_property_filter'
+    'django_property_filter',
+    'django_prometheus',
     ]
 
 try:
@@ -111,6 +112,8 @@ except ImportError as e:
     logger.warning(str(e))
 
 MIDDLEWARE = [
+    'django_prometheus.middleware.PrometheusBeforeMiddleware',
+
     'django.middleware.gzip.GZipMiddleware',
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -118,7 +121,9 @@ MIDDLEWARE = [
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware'
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+
+    'django_prometheus.middleware.PrometheusAfterMiddleware',
 ]
 
 def show_debug_toolbar(*args, **kwargs):
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
index 19a1b2f6dc7..d3bb47a627e 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
@@ -11,6 +11,7 @@ from django.db.models import Model, CharField, DateTimeField, IntegerField, Uniq
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.contrib.postgres.indexes import GinIndex
 from django.db import transaction
+from django_prometheus.models import model_inserts, model_updates, model_deletes
 from lofar.common.json_utils import validate_json_against_its_schema, validate_json_against_schema, add_defaults_to_json_object_for_schema, get_default_json_object_for_schema, raise_on_self_refs, resolved_remote_refs, ref_resolved_url, get_refs, remove_from_schema_cache
 from lofar.sas.tmss.tmss.exceptions import SchemaValidationException, ObsoleteValidationException, ObsoleteTemplateException
 from django.urls import reverse as reverse_url
@@ -36,6 +37,32 @@ class RefreshFromDbInvalidatesCachedPropertiesMixin:
                     self.__dict__.pop(key, None)
 
 
+class PrometheusModelOperationsMixin:
+    """Returns a mixin for models to export counters for lifecycle operations.
+
+    Mimics django_prometheus.models.ExportModelOperationsMixin but auto-derives
+    the model name from the class."""
+
+    def __init_subclass__(cls):
+        # Force create the labels for this model in the counters. This
+        # is not necessary but it avoids gaps in the aggregated data.
+        model_inserts.labels(cls.__name__)
+        model_updates.labels(cls.__name__)
+        model_deletes.labels(cls.__name__)
+
+    def _do_insert(self, *args, **kwargs):
+        model_inserts.labels(self.__class__.__name__).inc()
+        return super()._do_insert(*args, **kwargs)
+
+    def _do_update(self, *args, **kwargs):
+        model_updates.labels(self.__class__.__name__).inc()
+        return super()._do_update(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        model_deletes.labels(self.__class__.__name__).inc()
+        return super().delete(*args, **kwargs)
+
+
 class ProjectPropertyMixin:
     @cached_property
     def project(self): # -> Project:
@@ -57,7 +84,7 @@ class ProjectPropertyMixin:
                 return None
 
 
-class BasicCommon(Model):
+class BasicCommon(PrometheusModelOperationsMixin, Model):
     # todo: we cannot use foreign keys in the array here, so we have to keep the Tags table up to date by trigger or so.
     # todo: we could switch to a manytomany field instead?
     tags = ArrayField(CharField(max_length=128), size=8, blank=True, help_text='User-defined search keywords for object.', default=list)
diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py
index 346e884c185..95090c40355 100644
--- a/SAS/TMSS/backend/src/tmss/urls.py
+++ b/SAS/TMSS/backend/src/tmss/urls.py
@@ -275,6 +275,7 @@ urlpatterns = [url(r'^api$', RedirectView.as_view(url='/api/')),
                 url(r'^past_schedule', views.public_past_schedule),
                 url(r'', include(frontend_urls)),
                 url(r'^.*', include(frontend_urlpatterns)),
+                path('', include('django_prometheus.urls')),
 ]
 
 
@@ -306,4 +307,4 @@ if isViewflowEnabled():
 
 
 # Custom error handlers
-handler500 = 'lofar.sas.tmss.tmss.tmssapp.views.response500'
\ No newline at end of file
+handler500 = 'lofar.sas.tmss.tmss.tmssapp.views.response500'
-- 
GitLab