From e04ccc33889e4f5c87526eb5cb61cffd08c75b16 Mon Sep 17 00:00:00 2001
From: Benjamin Ledel <benjamin@schule-plus.com>
Date: Wed, 5 Mar 2025 16:53:48 +0100
Subject: [PATCH] * add prometheus counter

---
 src/backend/settings.py |  4 +-
 src/backend/urls.py     |  1 +
 src/xapi/views.py       | 89 +++++++++++++++++++++++++++++++----------
 3 files changed, 71 insertions(+), 23 deletions(-)

diff --git a/src/backend/settings.py b/src/backend/settings.py
index d62c12f..596e837 100644
--- a/src/backend/settings.py
+++ b/src/backend/settings.py
@@ -266,4 +266,6 @@ SHIB_ID_CONNECTOR_URL = env("SHIB_ID_CONNECTOR_URL",default="")
 SHIB_ID_CONNECTOR_APP_SECRET = env("SHIB_ID_CONNECTOR_APP_SECRET",default="")
 SHIB_ID_CONNECTOR_PROCESS_ID = env("SHIB_ID_CONNECTOR_PROCESS_ID",default="PI")
 SHIB_ID_CONNECTOR_LINK_TYPE = env("SHIB_ID_CONNECTOR_LINK_TYPE",default="")
-SHIB_ID_CONNECTOR_USE_FILE_MAPPING = env("SHIB_ID_CONNECTOR_USE_FILE_MAPPING",default=False)
\ No newline at end of file
+SHIB_ID_CONNECTOR_USE_FILE_MAPPING = env("SHIB_ID_CONNECTOR_USE_FILE_MAPPING",default=False)
+
+PROMETHEUS_METRIC_NAMESPACE = "polaris"
\ No newline at end of file
diff --git a/src/backend/urls.py b/src/backend/urls.py
index e883672..9a7e20b 100644
--- a/src/backend/urls.py
+++ b/src/backend/urls.py
@@ -48,6 +48,7 @@ def get_static_text(filename):
 urlpatterns = [
     path('robots.txt', get_static_text('static/robots_deny.txt' if settings.ALLOW_ROBOTS == "False" or settings.ALLOW_ROBOTS == False else "static/robots_allow.txt")),
     path("admin/", admin.site.urls),
+    path('', include('django_prometheus.urls')),
     path("api/v1/consents/", include("consents.urls")),
     path("api/v1/auth/", include("users.urls")),
     path("api/v1/provider/", include("providers.urls")),
diff --git a/src/xapi/views.py b/src/xapi/views.py
index 0e4013e..9bfe2b9 100644
--- a/src/xapi/views.py
+++ b/src/xapi/views.py
@@ -15,6 +15,7 @@ from jsonschema import ValidationError, validate
 from jsonschema.validators import validator_for
 from rest_framework import status
 from rest_framework.views import APIView
+from prometheus_client import Counter
 
 from backend.utils import lrs_db
 from consents.models import UserConsents
@@ -28,6 +29,17 @@ from .tasks import retry_forward_statements
 
 PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
 
+# Prometheus Counters
+STATEMENTS_PROCESSED = Counter(
+    "xapi_statements_processed_total", "Total number of xAPI statements processed"
+)
+STATEMENTS_ACCEPTED = Counter(
+    "xapi_statements_accepted_total", "Total number of xAPI statements accepted"
+)
+STATEMENTS_REJECTED = Counter(
+    "xapi_statements_rejected_total", "Total number of xAPI statements rejected"
+)
+
 with open(os.path.join(PROJECT_PATH, "static/xapi_statement.schema.json")) as f:
     schema = json.loads(f.read())
     cls = validator_for(schema)
@@ -285,13 +297,14 @@ def process_tan_statement(x_api_statement):
 
 class CreateStatement(APIView):
     """
-    xAPI create statements proxy. This endpoint filter xAPI statements based and user settings and passes only consented xAPI statements to the LRS.
+    xAPI create statements proxy. This endpoint filters xAPI statements based on user settings
+    and passes only consented xAPI statements to the LRS.
     """
 
     def post(self, request):
-        # provider authorization
+        # Provider authorization
         auth_header = request.headers.get("Authorization")
-        if not auth_header.startswith("Basic "):
+        if not auth_header or not auth_header.startswith("Basic "):
             return JsonResponse(
                 {
                     "message": "No provider authorization token supplied.",
@@ -300,6 +313,7 @@ class CreateStatement(APIView):
                 safe=False,
                 status=status.HTTP_401_UNAUTHORIZED,
             )
+
         auth_key = auth_header.split(" ")[1]
         try:
             provider_auth = ProviderAuthorization.objects.get(key=auth_key)
@@ -315,7 +329,7 @@ class CreateStatement(APIView):
 
         provider = provider_auth.provider
 
-        # load latest provider schema for essential verbs
+        # Load latest provider schema for essential verbs
         try:
             latest_schema = ProviderSchema.objects.get(
                 provider=provider, superseded_by__isnull=True
@@ -330,29 +344,52 @@ class CreateStatement(APIView):
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
 
-        # handle list of xAPI statements as well as single xAPI statement
+        # Handle list of xAPI statements as well as a single statement
         x_api_statements = (
             request.data if isinstance(request.data, list) else [request.data]
         )
 
-        # forward to other LRS without validation etc., if given
-        if latest_schema.additional_lrs and isinstance(latest_schema.additional_lrs, list) and len(latest_schema.additional_lrs) > 0:
+        # Track total processed statements
+        STATEMENTS_PROCESSED.inc(len(x_api_statements))
+
+        # Forward to additional LRS if configured
+        if (
+            latest_schema.additional_lrs
+            and isinstance(latest_schema.additional_lrs, list)
+            and len(latest_schema.additional_lrs) > 0
+        ):
             for additional_lrs in latest_schema.additional_lrs:
-                headers = {"Authorization": additional_lrs["token_type"] + " " + additional_lrs["token"]}
+                headers = {
+                    "Authorization": additional_lrs["token_type"]
+                    + " "
+                    + additional_lrs["token"]
+                }
                 try:
-                    res = requests.post(additional_lrs["url"], json=x_api_statements, headers=headers)
+                    res = requests.post(
+                        additional_lrs["url"], json=x_api_statements, headers=headers
+                    )
                     if res.status_code != 200:
                         raise RuntimeError("Returned status code other than 200")
                     if settings.DEBUG:
-                        print("Forwarded statement to ", additional_lrs["url"], ":", res.reason,
-                              "({})".format(res.status_code))
+                        print(
+                            "Forwarded statement to ",
+                            additional_lrs["url"],
+                            ":",
+                            res.reason,
+                            "({})".format(res.status_code),
+                        )
                 except Exception as e:
                     if settings.DEBUG:
                         print("Could not forward to ", additional_lrs["url"], ":", e)
-                    retry_forward_statements.delay(x_api_statements, additional_lrs["token_type"], additional_lrs["token"], additional_lrs["url"])
+                    retry_forward_statements.delay(
+                        x_api_statements,
+                        additional_lrs["token_type"],
+                        additional_lrs["token"],
+                        additional_lrs["url"],
+                    )
 
         if settings.SHOW_XAPI_STATEMENTS:
-           print(x_api_statements)
+            print(x_api_statements)
 
         try:
             result = [
@@ -367,15 +404,19 @@ class CreateStatement(APIView):
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
 
-        invalid_or_not_consented = (
-            len([e for e in result if e["valid"] == False or e["accepted"] == False])
-            > 0
-        )        
-        
+        # Count rejected and accepted statements
+        rejected_count = sum(
+            1 for e in result if e["valid"] is False or e["accepted"] is False
+        )
+        accepted_count = len(x_api_statements) - rejected_count
+
+        STATEMENTS_REJECTED.inc(rejected_count)
+        STATEMENTS_ACCEPTED.inc(accepted_count)
+
         if settings.SHOW_XAPI_STATEMENTS:
             print(result)
 
-        if invalid_or_not_consented:
+        if rejected_count > 0:
             return JsonResponse(
                 {
                     "message": "xAPI statements couldn't be stored in LRS",
@@ -384,9 +425,13 @@ class CreateStatement(APIView):
                 status=status.HTTP_400_BAD_REQUEST,
             )
         else:
-            # anonymize statements
-            x_api_statements = [anonymize_statement(statement) if result[i].get("needs_anonymization", False)
-                                else statement for i, statement in enumerate(x_api_statements)]
+            # Anonymize statements where needed
+            x_api_statements = [
+                anonymize_statement(statement)
+                if result[i].get("needs_anonymization", False)
+                else statement
+                for i, statement in enumerate(x_api_statements)
+            ]
             uuids = list(map(store_in_db, x_api_statements))
             return JsonResponse(
                 {
-- 
GitLab