diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 4b005e51..6b449e4d 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -110,6 +110,7 @@ def __init__( self.unleash_app_name = app_name self.unleash_environment = environment self.unleash_instance_id = instance_id + self._connection_id = str(uuid.uuid4()) self.unleash_refresh_interval = refresh_interval self.unleash_request_timeout = request_timeout self.unleash_request_retries = request_retries @@ -183,6 +184,18 @@ def __init__( engine=self.engine, ) + @property + def unleash_refresh_interval_str_millis(self) -> str: + return str(self.unleash_refresh_interval * 1000) + + @property + def unleash_metrics_interval_str_millis(self) -> str: + return str(self.unleash_metrics_interval * 1000) + + @property + def connection_id(self): + return self._connection_id + def initialize_client(self, fetch_toggles: bool = True) -> None: """ Initializes client and starts communication with central unleash server(s). @@ -213,19 +226,25 @@ def initialize_client(self, fetch_toggles: bool = True) -> None: if not self.is_initialized: # pylint: disable=no-else-raise try: - headers = { + base_headers = { **self.unleash_custom_headers, - "unleash-connection-id": str(uuid.uuid4()), + "unleash-connection-id": self.connection_id, "unleash-appname": self.unleash_app_name, "unleash-sdk": f"{SDK_NAME}:{SDK_VERSION}", } + metrics_headers = { + **base_headers, + "unleash-interval": self.unleash_metrics_interval_str_millis, + } + # Setup metrics_args = { "url": self.unleash_url, "app_name": self.unleash_app_name, + "connection_id": self.connection_id, "instance_id": self.unleash_instance_id, - "headers": headers, + "headers": metrics_headers, "custom_options": self.unleash_custom_options, "request_timeout": self.unleash_request_timeout, "engine": self.engine, @@ -237,19 +256,25 @@ def initialize_client(self, fetch_toggles: bool = True) -> None: self.unleash_url, self.unleash_app_name, self.unleash_instance_id, + self.connection_id, self.unleash_metrics_interval, - headers, + base_headers, self.unleash_custom_options, self.strategy_mapping, self.unleash_request_timeout, ) if fetch_toggles: + fetch_headers = { + **base_headers, + "unleash-interval": self.unleash_refresh_interval_str_millis, + } + job_args = { "url": self.unleash_url, "app_name": self.unleash_app_name, "instance_id": self.unleash_instance_id, - "headers": headers, + "headers": fetch_headers, "custom_options": self.unleash_custom_options, "cache": self.cache, "engine": self.engine, @@ -277,7 +302,6 @@ def initialize_client(self, fetch_toggles: bool = True) -> None: executor=self.unleash_executor_name, kwargs=job_args, ) - if not self.unleash_disable_metrics: self.metric_job = self.unleash_scheduler.add_job( aggregate_and_send_metrics, diff --git a/UnleashClient/api/register.py b/UnleashClient/api/register.py index 24c0d6ed..18024489 100644 --- a/UnleashClient/api/register.py +++ b/UnleashClient/api/register.py @@ -21,6 +21,7 @@ def register_client( url: str, app_name: str, instance_id: str, + connection_id: str, metrics_interval: int, headers: dict, custom_options: dict, @@ -47,6 +48,7 @@ def register_client( registration_request = { "appName": app_name, "instanceId": instance_id, + "connectionId": connection_id, "sdkVersion": f"{SDK_NAME}:{SDK_VERSION}", "strategies": [*supported_strategies], "started": datetime.now(timezone.utc).isoformat(), diff --git a/UnleashClient/periodic_tasks/send_metrics.py b/UnleashClient/periodic_tasks/send_metrics.py index ef532caf..55fed20f 100644 --- a/UnleashClient/periodic_tasks/send_metrics.py +++ b/UnleashClient/periodic_tasks/send_metrics.py @@ -12,6 +12,7 @@ def aggregate_and_send_metrics( url: str, app_name: str, instance_id: str, + connection_id: str, headers: dict, custom_options: dict, request_timeout: int, @@ -22,6 +23,7 @@ def aggregate_and_send_metrics( metrics_request = { "appName": app_name, "instanceId": instance_id, + "connectionId": connection_id, "bucket": metrics_bucket, "platformName": python_implementation(), "platformVersion": python_version(), diff --git a/tests/unit_tests/api/test_metrics.py b/tests/unit_tests/api/test_metrics.py index 2e3d3567..35ff98d1 100644 --- a/tests/unit_tests/api/test_metrics.py +++ b/tests/unit_tests/api/test_metrics.py @@ -1,3 +1,5 @@ +import json + import responses from pytest import mark, param from requests import ConnectionError @@ -36,5 +38,9 @@ def test_send_metrics(payload, status, expected): URL, MOCK_METRICS_REQUEST, CUSTOM_HEADERS, CUSTOM_OPTIONS, REQUEST_TIMEOUT ) + request = json.loads(responses.calls[0].request.body) + assert len(responses.calls) == 1 assert expected(result) + + assert request["connectionId"] == MOCK_METRICS_REQUEST.get("connectionId") diff --git a/tests/unit_tests/api/test_register.py b/tests/unit_tests/api/test_register.py index 3cfc6e6d..972f56a7 100644 --- a/tests/unit_tests/api/test_register.py +++ b/tests/unit_tests/api/test_register.py @@ -6,6 +6,7 @@ from tests.utilities.testing_constants import ( APP_NAME, + CONNECTION_ID, CUSTOM_HEADERS, CUSTOM_OPTIONS, INSTANCE_ID, @@ -40,6 +41,7 @@ def test_register_client(payload, status, expected): URL, APP_NAME, INSTANCE_ID, + CONNECTION_ID, METRICS_INTERVAL, CUSTOM_HEADERS, CUSTOM_OPTIONS, @@ -59,6 +61,7 @@ def test_register_includes_metadata(): URL, APP_NAME, INSTANCE_ID, + CONNECTION_ID, METRICS_INTERVAL, CUSTOM_HEADERS, CUSTOM_OPTIONS, @@ -71,5 +74,6 @@ def test_register_includes_metadata(): assert request["yggdrasilVersion"] is not None assert request["specVersion"] == CLIENT_SPEC_VERSION + assert request["connectionId"] == CONNECTION_ID assert request["platformName"] is not None assert request["platformVersion"] is not None diff --git a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py index 8fd4807b..8621d949 100644 --- a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py +++ b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py @@ -5,6 +5,7 @@ from tests.utilities.testing_constants import ( APP_NAME, + CONNECTION_ID, CUSTOM_HEADERS, CUSTOM_OPTIONS, INSTANCE_ID, @@ -31,6 +32,7 @@ def test_no_metrics(): URL, APP_NAME, INSTANCE_ID, + CONNECTION_ID, CUSTOM_HEADERS, CUSTOM_OPTIONS, REQUEST_TIMEOUT, @@ -51,6 +53,7 @@ def test_metrics_metadata_is_sent(): URL, APP_NAME, INSTANCE_ID, + CONNECTION_ID, CUSTOM_HEADERS, CUSTOM_OPTIONS, REQUEST_TIMEOUT, diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py index c114eb4d..807245a1 100644 --- a/tests/unit_tests/test_client.py +++ b/tests/unit_tests/test_client.py @@ -1,4 +1,6 @@ +import json import time +import uuid import warnings from datetime import datetime, timezone from pathlib import Path @@ -1078,3 +1080,79 @@ def test_identification_headers_unique_connection_id(): "UNLEASH-CONNECTION-ID" ] assert connection_id_first_client != connection_id_second_client + + +@responses.activate +def test_identification_values_are_passed_in(): + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add( + responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200 + ) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + refresh_interval = 1 + metrics_interval = 2 + unleash_client = UnleashClient( + URL, + APP_NAME, + refresh_interval=refresh_interval, + metrics_interval=metrics_interval, + ) + + expected_refresh_interval = str(refresh_interval * 1000) + expected_metrics_interval = str(metrics_interval * 1000) + + unleash_client.initialize_client() + register_request = responses.calls[0].request + register_body = json.loads(register_request.body) + + assert "connectionId" in register_body, "Key missing: connectionId" + try: + uuid.UUID(register_body["connectionId"]) + except ValueError: + assert False, "Invalid UUID format in connectionId" + + assert ( + "UNLEASH-CONNECTION-ID" in register_request.headers + ), "Header missing: UNLEASH-CONNECTION-ID" + try: + uuid.UUID(register_request.headers["UNLEASH-CONNECTION-ID"]) + except ValueError: + assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID" + + unleash_client.is_enabled("registerMetricsFlag") + + features_request = responses.calls[1].request + + assert features_request.headers["UNLEASH-INTERVAL"] == expected_refresh_interval + + assert ( + "UNLEASH-CONNECTION-ID" in features_request.headers + ), "Header missing: UNLEASH-CONNECTION-ID" + + try: + uuid.UUID(features_request.headers["UNLEASH-CONNECTION-ID"]) + except ValueError: + assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID" + + time.sleep(3) + 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_request.headers["UNLEASH-INTERVAL"] == expected_metrics_interval + + assert "connectionId" in metrics_body, "Key missing: connectionId" + try: + uuid.UUID(metrics_body["connectionId"]) + except ValueError: + assert False, "Invalid UUID format in connectionId" + + assert ( + "UNLEASH-CONNECTION-ID" in metrics_request.headers + ), "Header missing: UNLEASH-CONNECTION-ID" + try: + uuid.UUID(metrics_request.headers["UNLEASH-CONNECTION-ID"]) + except ValueError: + assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID" diff --git a/tests/utilities/mocks/mock_metrics.py b/tests/utilities/mocks/mock_metrics.py index ceaa8ab3..66ab96e9 100644 --- a/tests/utilities/mocks/mock_metrics.py +++ b/tests/utilities/mocks/mock_metrics.py @@ -1,6 +1,7 @@ MOCK_METRICS_REQUEST = { "appName": "appName", "instanceId": "instanceId", + "connectionId": "connectionId", "bucket": { "start": "2016-11-03T07:16:43.572Z", "stop": "2016-11-03T07:16:53.572Z", diff --git a/tests/utilities/testing_constants.py b/tests/utilities/testing_constants.py index ca05b6a5..1ece2f40 100644 --- a/tests/utilities/testing_constants.py +++ b/tests/utilities/testing_constants.py @@ -4,6 +4,7 @@ APP_NAME = "pytest" ENVIRONMENT = "unit" INSTANCE_ID = "123" +CONNECTION_ID = "test-connection-id" REFRESH_INTERVAL = 3 REFRESH_JITTER = None METRICS_INTERVAL = 2