Repository: Unleash/unleash-client-python
Pull request #351 (2025-05-30 09:49:38)

Flush metrics on destroy
# Description

UnleashClient's `destroy()` function was just unregistering the metrics job and not flushing metrics.  This can lead to up to N seconds where N = metrics job interval of metrics to get lost.  

Changes:
- If metrics are enabled, `destroy()` will try and flush metrics after metrics job is unscheduled (to avoid dupes).
- Light refactoring to share code between initialize/destroy functions.
- Add error code for when API calls fail to make troubleshooting easier.

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

- [x] Unit tests
- [x] Spec Tests
- [x] Integration tests / Manual Tests

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules



Patch:
diff --git a/Makefile b/Makefile
index 9ac4509..2dae731 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,7 @@ install-docs: install
 #-----------------------------------------------------------------------
 fmt:
 	black . && \
-	ruff UnleashClient tests --fix
+	ruff check UnleashClient tests --fix
 
 lint:
 	black . --check && \
diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py
index 6b449e4..266d3b4 100644
--- a/UnleashClient/__init__.py
+++ b/UnleashClient/__init__.py
@@ -146,6 +146,8 @@ class UnleashClient:
         self.cache.mset({METRIC_LAST_SENT_TIME: datetime.now(timezone.utc), ETAG: ""})
         self.unleash_bootstrapped = self.cache.bootstrapped
 
+        self.metrics_headers: dict = {}
+
         # Scheduler bootstrapping
         # - Figure out the Unleash executor name.
         if scheduler and scheduler_executor:
@@ -233,7 +235,7 @@ class UnleashClient:
                     "unleash-sdk": f"{SDK_NAME}:{SDK_VERSION}",
                 }
 
-                metrics_headers = {
+                self.metrics_headers = {
                     **base_headers,
                     "unleash-interval": self.unleash_metrics_interval_str_millis,
                 }
@@ -244,7 +246,7 @@ class UnleashClient:
                     "app_name": self.unleash_app_name,
                     "connection_id": self.connection_id,
                     "instance_id": self.unleash_instance_id,
-                    "headers": metrics_headers,
+                    "headers": self.metrics_headers,
                     "custom_options": self.unleash_custom_options,
                     "request_timeout": self.unleash_request_timeout,
                     "engine": self.engine,
@@ -357,6 +359,19 @@ class UnleashClient:
         self.fl_job.remove()
         if self.metric_job:
             self.metric_job.remove()
+
+            # Flush metrics before shutting down.
+            aggregate_and_send_metrics(
+                url=self.unleash_url,
+                app_name=self.unleash_app_name,
+                connection_id=self.connection_id,
+                instance_id=self.unleash_instance_id,
+                headers=self.metrics_headers,
+                custom_options=self.unleash_custom_options,
+                request_timeout=self.unleash_request_timeout,
+                engine=self.engine,
+            )
+
         self.unleash_scheduler.shutdown()
         self.cache.destroy()
 
diff --git a/UnleashClient/api/features.py b/UnleashClient/api/features.py
index 176091a..0cf97e4 100644
--- a/UnleashClient/api/features.py
+++ b/UnleashClient/api/features.py
@@ -72,7 +72,8 @@ def get_feature_toggles(
         if resp.status_code not in [200, 304]:
             log_resp_info(resp)
             LOGGER.warning(
-                "Unleash Client feature fetch failed due to unexpected HTTP status code."
+                "Unleash Client feature fetch failed due to unexpected HTTP status code: %s",
+                resp.status_code,
             )
             raise Exception(
                 "Unleash Client feature fetch failed!"
diff --git a/UnleashClient/api/metrics.py b/UnleashClient/api/metrics.py
index 0d9e75c..4a94c4d 100644
--- a/UnleashClient/api/metrics.py
+++ b/UnleashClient/api/metrics.py
@@ -41,7 +41,10 @@ def send_metrics(
 
         if resp.status_code != 202:
             log_resp_info(resp)
-            LOGGER.warning("Unleash Client metrics submission failed.")
+            LOGGER.warning(
+                "Unleash Client metrics submission due to unexpected HTTP status code: %s",
+                resp.status_code,
+            )
             return False
 
         LOGGER.info("Unleash Client metrics successfully sent!")
diff --git a/UnleashClient/api/register.py b/UnleashClient/api/register.py
index 1802448..f59b856 100644
--- a/UnleashClient/api/register.py
+++ b/UnleashClient/api/register.py
@@ -74,7 +74,8 @@ def register_client(
         if resp.status_code != 202:
             log_resp_info(resp)
             LOGGER.warning(
-                "Unleash Client registration failed due to unexpected HTTP status code."
+                "Unleash Client registration failed due to unexpected HTTP status code: %s",
+                resp.status_code,
             )
             return False
 
diff --git a/requirements.txt b/requirements.txt
index 127f507..b2bd7c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,7 +18,7 @@ mypy
 pylint
 pytest
 pytest-cov
-pytest-html==4.0.0rc4
+pytest-html
 pytest-mock
 pytest-xdist
 responses


Test patch:
diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py
index 807245a..8748ba4 100644
--- a/tests/unit_tests/test_client.py
+++ b/tests/unit_tests/test_client.py
@@ -394,6 +394,14 @@ def test_uc_context_manager(unleash_client_nodestroy):
 
     with unleash_client_nodestroy as unleash_client:
         assert unleash_client.is_initialized
+        assert unleash_client.is_enabled("testFlag")
+
+    # Context Manager use case is usualy short-lived so even with a METRICS_INTERVAL of 2 seconds metrics can get lost.  Verify that metrics are sent on destroy.
+    metrics_request = [
+        call for call in responses.calls if METRICS_URL in call.request.url
+    ][0].request
+    metrics_body = json.loads(metrics_request.body)
+    assert metrics_body["bucket"]["toggles"]["testFlag"]["yes"] == 1
 
 
 @responses.activate