From a4f6dd7100adecd6d2c24135fe21d5fd1e135f51 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 22 Jan 2025 08:44:34 -0800 Subject: [PATCH] redis report --- app/main/views/platform_admin.py | 66 +++++++++++++++++++ app/main/views/sign_in.py | 11 ++-- .../views/platform-admin/reports.html | 3 + .../clients/redis/redis_client.py | 16 +++++ tests/app/main/views/test_tour.py | 4 +- 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index 6a121ecc2..04ef87a55 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -116,6 +116,72 @@ def download_all_users(): return response +@main.route("/platform-admin/get-redis-report") +@user_is_platform_admin +def get_redis_report(): + + memory_info = redis_client.info("memory") + memory_used = memory_info.get("used_memory_human", "N/A") + max_memory = memory_info.get("maxmemory_human", "N/A") + if max_memory == "0B": + max_memory = "No set limit" + mem_fragmentation = memory_info.get("mem_fragmentation_ratio", "N/A") + frag_quality = "Swapping (bad)" + if mem_fragmentation >= 1.0: + frag_quality = "Healthy" + if mem_fragmentation > 1.5: + frag_quality = "Problematic" + if mem_fragmentation > 2.0: + frag_quality = "Severe fragmentation" + + frag_note = "" + if mem_fragmentation > 2.0: + frag_note = "Use MEMORY PURGE.\nReplace multiple small keys with hashes.\nAvoid long keys.\nSet max_memory." + elif mem_fragmentation < 1.0: + frag_note = "Allocate more RAM.\nSet max_memory." + + keys = redis_client.keys("*") + key_details = [] + + for key in keys: + key_type = redis_client.type(key).decode("utf-8") + ttl = redis_client.ttl(key) + ttl_str = "No Expiry" if ttl == -1 else f"{ttl} seconds" + key_details.append( + {"Key": key.decode("utf-8"), "Type": key_type, "TTL": ttl_str} + ) + output = StringIO() + writer = csv.writer( + output, + ) + writer.writerow(["Redis Report"]) + writer.writerow([]) + + writer.writerow(["Memory"]) + writer.writerow(["", "Metric", "Value"]) + writer.writerow(["", "Memory Used", memory_used]) + writer.writerow(["", "Max Memory", max_memory]) + writer.writerow(["", "Memory Fragmentation Ratio", mem_fragmentation]) + writer.writerow(["", "Memory Fragmentation Quality", frag_quality]) + writer.writerow(["", "Memory Fragmentation Note", frag_note]) + writer.writerow([]) + + writer.writerow(["Keys Overview"]) + writer.writerow(["", "TTL", "Type", "Key"]) + for key_detail in key_details: + writer.writerow( + ["", key_detail["TTL"], key_detail["Type"], key_detail["Key"][0:50]] + ) + + csv_data = output.getvalue() + + # Create a direct download response with the CSV data and appropriate headers + response = Response(csv_data, content_type="text/csv; charset=utf-8") + response.headers["Content-Disposition"] = "attachment; filename=redis.csv" + + return response + + def is_over_threshold(number, total, threshold): percentage = number / total * 100 if total else 0 return percentage > threshold diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 004dce2ae..1cb163691 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -68,11 +68,12 @@ def _get_access_token(code): # pragma: no cover id_token = get_id_token(response_json) nonce = id_token["nonce"] nonce_key = f"login-nonce-{unquote(nonce)}" - stored_nonce = redis_client.get(nonce_key).decode("utf8") + if not os.getenv("NOTIFY_ENVIRONMENT") == "development": + stored_nonce = redis_client.get(nonce_key).decode("utf8") - if nonce != stored_nonce: - current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}") - abort(403) + if nonce != stored_nonce: + current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}") + abort(403) try: access_token = response_json["access_token"] @@ -112,7 +113,7 @@ def _do_login_dot_gov(): # $ pragma: no cover verify_key = f"login-verify_email-{unquote(state)}" verify_path = bool(redis_client.get(verify_key)) - if not verify_path: + if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development": state_key = f"login-state-{unquote(state)}" stored_state = unquote(redis_client.get(state_key).decode("utf8")) if state != stored_state: diff --git a/app/templates/views/platform-admin/reports.html b/app/templates/views/platform-admin/reports.html index 3b7b32d3a..23dccc41c 100644 --- a/app/templates/views/platform-admin/reports.html +++ b/app/templates/views/platform-admin/reports.html @@ -34,5 +34,8 @@

Download All Users

+

+ Get Redis Report +

{% endblock %} diff --git a/notifications_utils/clients/redis/redis_client.py b/notifications_utils/clients/redis/redis_client.py index 1723dd2c1..469cd7c8f 100644 --- a/notifications_utils/clients/redis/redis_client.py +++ b/notifications_utils/clients/redis/redis_client.py @@ -149,6 +149,22 @@ class RedisClient: except Exception as e: self.__handle_exception(e, raise_exception, "incr", key) + def info(self, key): + if self.active: + return self.redis_store.info(key) + + def keys(self, pattern): + if self.active: + return self.redis_store.keys(pattern) + + def type(self, key): + if self.active: + return self.redis_store.type(key) + + def ttl(self, key): + if self.active: + return self.redis_store.ttl(key) + def get(self, key, raise_exception=False): key = prepare_value(key) if self.active: diff --git a/tests/app/main/views/test_tour.py b/tests/app/main/views/test_tour.py index 9a2b8f053..3207123a9 100644 --- a/tests/app/main/views/test_tour.py +++ b/tests/app/main/views/test_tour.py @@ -174,9 +174,7 @@ def test_should_show_empty_text_box( # data-module=autofocus is set on a containing element so it # shouldn’t also be set on the textbox itself assert "data-module" not in textbox - assert ( - normalize_spaces(page.select_one("label[for=phone-number]").text) == "one" - ) + assert normalize_spaces(page.select_one("label[for=phone-number]").text) == "one" def test_should_prefill_answers_for_get_tour_step(