From 1911d74e76912fa4f78b62c47e4a890de8473bef Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 24 Apr 2026 12:21:16 +0100 Subject: [PATCH 1/2] feat: added usage notifications structlog --- api/organisations/task_helpers.py | 25 ++++++++++++---- .../test_unit_organisations_tasks.py | 16 ++++++---- .../observability/_events-catalogue.md | 30 +++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/api/organisations/task_helpers.py b/api/organisations/task_helpers.py index ee00b648ec94..7470d54dbf77 100644 --- a/api/organisations/task_helpers.py +++ b/api/organisations/task_helpers.py @@ -1,6 +1,6 @@ -import logging from datetime import timedelta +import structlog from dateutil.relativedelta import relativedelta from django.conf import settings from django.core.mail import send_mail @@ -21,7 +21,7 @@ from .constants import API_USAGE_ALERT_THRESHOLDS -logger = logging.getLogger(__name__) +logger = structlog.get_logger("api_usage") def send_api_flags_blocked_notification(organisation: Organisation) -> None: @@ -115,10 +115,9 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - billing_starts_at = subscription_cache.current_billing_term_starts_at if billing_starts_at is None: - # Since the calling code is a list of many organisations - # log the error and return without raising an exception. logger.error( - f"Paid organisation {organisation.id} is missing billing_starts_at datetime" + "notification.missing_billing_starts_at", + organisation__id=organisation.id, ) return @@ -151,6 +150,16 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - matched_threshold = threshold + logger.info( + "notification.evaluated", + organisation__id=organisation.id, + api_usage=api_usage, + allowed_api_calls=allowed_api_calls, + api_usage_percent=api_usage_percent, + period_starts_at=period_starts_at.isoformat(), + matched_threshold=matched_threshold, + ) + # Didn't match even the lowest threshold, so no notification. if matched_threshold is None: return @@ -163,6 +172,12 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - # Already sent the max notification level so don't resend. return + logger.info( + "notification.sent", + organisation__id=organisation.id, + matched_threshold=matched_threshold, + ) + _send_api_usage_notification(organisation, matched_threshold) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index c99ad8ae028e..d953ed2845e3 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -10,6 +10,7 @@ from freezegun.api import FrozenDateTimeFactory from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture +from pytest_structlog import StructuredLogCapture from core.helpers import get_current_site_url from organisations.chargebee.metadata import ChargebeeObjMetadata @@ -290,7 +291,7 @@ def test_send_org_subscription_cancelled_alert__valid_organisation__sends_cancel def test_handle_api_usage_notification_for_organisation__billing_starts_at_is_none__logs_warning( organisation: Organisation, - caplog: pytest.LogCaptureFixture, + log: StructuredLogCapture, mocker: MockerFixture, ) -> None: # Given @@ -313,14 +314,18 @@ def test_handle_api_usage_notification_for_organisation__billing_starts_at_is_no # Then api_usage_mock.assert_not_called() - assert caplog.messages == [ - f"Paid organisation {organisation.id} is missing billing_starts_at datetime" + assert log.events == [ + { + "level": "error", + "event": "notification.missing_billing_starts_at", + "organisation__id": organisation.id, + } ] def test_handle_api_usage_notification_for_organisation__cancellation_date_is_set__skips_notification( organisation: Organisation, - caplog: pytest.LogCaptureFixture, + log: StructuredLogCapture, mocker: MockerFixture, ) -> None: # Given @@ -346,8 +351,7 @@ def test_handle_api_usage_notification_for_organisation__cancellation_date_is_se # Then assert OrganisationAPIUsageNotification.objects.count() == 0 - # Check to ensure that error messages haven't been set. - assert caplog.messages == [] + assert not any(e["level"] == "error" for e in log.events) def test_handle_api_usage_notification_for_organisation__billing_starts_at_over_12_months_ago__uses_12_month_offset( diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index a2d3ba898f7b..2a4548b1b710 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -1,4 +1,34 @@ +### `api_usage.notification.evaluated` + +Logged at `info` from: + - `api/organisations/task_helpers.py:153` + +Attributes: + - `allowed_api_calls` + - `api_usage` + - `api_usage_percent` + - `matched_threshold` + - `organisation.id` + - `period_starts_at` + +### `api_usage.notification.missing_billing_starts_at` + +Logged at `error` from: + - `api/organisations/task_helpers.py:118` + +Attributes: + - `organisation.id` + +### `api_usage.notification.sent` + +Logged at `info` from: + - `api/organisations/task_helpers.py:175` + +Attributes: + - `matched_threshold` + - `organisation.id` + ### `app_analytics.no_analytics_database_configured` Logged at `warning` from: From b1cc2867860b849e0ebec312246b0b01dd283e37 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 24 Apr 2026 12:26:18 +0100 Subject: [PATCH 2/2] feat: added period ends at --- api/organisations/task_helpers.py | 1 + .../deployment-self-hosting/observability/_events-catalogue.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/organisations/task_helpers.py b/api/organisations/task_helpers.py index 7470d54dbf77..0062633322dc 100644 --- a/api/organisations/task_helpers.py +++ b/api/organisations/task_helpers.py @@ -157,6 +157,7 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - allowed_api_calls=allowed_api_calls, api_usage_percent=api_usage_percent, period_starts_at=period_starts_at.isoformat(), + period_ends_at=now.isoformat(), matched_threshold=matched_threshold, ) diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 2a4548b1b710..1914f0f0f61f 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -10,6 +10,7 @@ Attributes: - `api_usage_percent` - `matched_threshold` - `organisation.id` + - `period_ends_at` - `period_starts_at` ### `api_usage.notification.missing_billing_starts_at` @@ -23,7 +24,7 @@ Attributes: ### `api_usage.notification.sent` Logged at `info` from: - - `api/organisations/task_helpers.py:175` + - `api/organisations/task_helpers.py:176` Attributes: - `matched_threshold`