Repository: akvo/akvo-rsr

Issue:
Run Google Cloud Profiler in GCP
We are seeing spikes of CPU usage in production and we can't tie them to anything at the moment as we don't have profiling activated. Since our production environment is on GCP, we can take advantage of [Cloud Profiler](https://cloud.google.com/profiler).

There's a guide for [Python](https://cloud.google.com/profiler/docs/profiling-python).


Discussion:

Resolving patch:
diff --git a/akvo/utils.py b/akvo/utils.py
--- a/akvo/utils.py
+++ b/akvo/utils.py
@@ -523,6 +523,18 @@ def maybe_decimal(value):
         return None
 
 
+BOOL_STRINGS = (
+    "1",
+    "true",
+    "yes",
+)
+
+
+def to_bool(obj) -> bool:
+    """Converts a given object to a boolean"""
+    return str(obj).lower() in BOOL_STRINGS
+
+
 def save_image(img, name, field_name):
     if isinstance(img.storage, GoogleCloudStorage):
         new_name = img.field.generate_filename(img.instance, name)
diff --git a/akvo/wsgi.py b/akvo/wsgi.py
--- a/akvo/wsgi.py
+++ b/akvo/wsgi.py
@@ -18,6 +18,7 @@
 framework.
 
 """
+import logging
 import os
 
 from django.core.wsgi import get_wsgi_application
@@ -32,3 +33,32 @@
 # Apply WSGI middleware here.
 # from helloworld.wsgi import HelloWorldApplication
 # application = HelloWorldApplication(application)
+
+# Django as a whole can now be imported after the apps have been populated
+from django.conf import settings
+from . import utils
+
+logger = logging.getLogger(__name__)
+# Use Google Cloud Profiler if enabled and installed
+if utils.to_bool(os.environ.get("CLOUD_PROFILER_ENABLE")):
+    # https://cloud.google.com/profiler/docs/profiling-python
+    try:
+        import googlecloudprofiler
+
+        deploy_commit_id = getattr(settings, "DEPLOY_COMMIT_ID", None)
+        if not deploy_commit_id:
+            raise ValueError("Unknown commit ID")
+
+        # We currently don't have a good way of determining which env RSR is running in
+        domain_name = settings.RSR_DOMAIN
+        service_version = f"{domain_name}-{deploy_commit_id}"
+
+        logger.info("Starting cloud profiler for version: %s", service_version)
+        googlecloudprofiler.start(
+            service='akvo-rsr',
+            service_version=service_version,
+        )
+    except ImportError:
+        logger.warning("Google Cloud Profiler enabled, but not installed")
+    except (ValueError, NotImplementedError) as exc:
+        print(exc)  # Handle errors here


Test patch:
diff --git a/akvo/rsr/tests/rsr_utils/test_utils.py b/akvo/rsr/tests/rsr_utils/test_utils.py
--- a/akvo/rsr/tests/rsr_utils/test_utils.py
+++ b/akvo/rsr/tests/rsr_utils/test_utils.py
@@ -9,12 +9,14 @@
 
 from akvo.rsr.models import User, Project, Country, Keyword, Sector
 from akvo.codelists.models import Sector as CodelistSector
-from akvo.utils import (rsr_send_mail_to_users, model_and_instance_based_filename,
-                        who_am_i, who_is_parent, to_gmt, rsr_show_keywords,
-                        custom_get_or_create_country, right_now_in_akvo,
-                        pagination, filter_query_string, codelist_name,
-                        codelist_choices, single_period_dates, get_placeholder_thumbnail,
-                        ensure_decimal, maybe_decimal)
+from akvo.utils import (
+    rsr_send_mail_to_users, model_and_instance_based_filename,
+    to_bool, who_am_i, who_is_parent, to_gmt, rsr_show_keywords,
+    custom_get_or_create_country, right_now_in_akvo,
+    pagination, filter_query_string, codelist_name,
+    codelist_choices, single_period_dates, get_placeholder_thumbnail,
+    ensure_decimal, maybe_decimal,
+)
 
 from django.core import mail
 from django.http.request import QueryDict
@@ -302,3 +304,19 @@ def test_maybe_decimal(self):
             (None, None),
         ]:
             self.assertEqual(expected, maybe_decimal(actual))
+
+    def test_to_bool(self):
+        for expected, actual in [
+            (True, "1"),
+            (True, 1),
+            (True, "yes"),
+            (True, "YES"),
+            (True, "true"),
+            (True, "True"),
+            (False, "false"),
+            (False, "False"),
+            (False, "something"),
+            (False, None),
+            (False, object()),
+        ]:
+            self.assertEqual(expected, to_bool(actual))