diff --git a/api/core/workflows_services.py b/api/core/workflows_services.py index 49b17bfdea23..2547681e56a2 100644 --- a/api/core/workflows_services.py +++ b/api/core/workflows_services.py @@ -14,7 +14,7 @@ from features.workflows.core.models import ChangeRequest from users.models import FFAdminUser -logger = structlog.get_logger() +logger = structlog.get_logger("workflows") class ChangeRequestCommitService: @@ -34,6 +34,15 @@ def commit(self, committed_by: "FFAdminUser") -> None: self.change_request.committed_at = timezone.now() self.change_request.committed_by = committed_by + + if environment := self.change_request.environment: + logger.info( + "change_request.committed", + organisation__id=environment.project.organisation_id, + environment__id=environment.id, + feature_states__count=self.change_request.feature_states.count(), + ) + self.change_request.save() def _publish_feature_states(self) -> None: diff --git a/api/integrations/github/views.py b/api/integrations/github/views.py index bb984b6ee537..8eba281387a7 100644 --- a/api/integrations/github/views.py +++ b/api/integrations/github/views.py @@ -6,6 +6,7 @@ from urllib.parse import urlparse import requests +import structlog from django.conf import settings from django.db.utils import IntegrityError from rest_framework import status, viewsets @@ -54,6 +55,7 @@ from projects.code_references.services import get_code_references_for_feature_flag logger = logging.getLogger(__name__) +code_references_logger = structlog.get_logger("code_references") def github_auth_required(func): # type: ignore[no-untyped-def] @@ -383,6 +385,12 @@ def create_cleanup_issue(request, organisation_pk: int) -> Response: # type: ig except IntegrityError: pass + code_references_logger.info( + "cleanup_issues.created", + organisation__id=organisation_pk, + issues_created__count=len(summaries), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/projects/code_references/views.py b/api/projects/code_references/views.py index 8a892f38b041..b2d244fe9aac 100644 --- a/api/projects/code_references/views.py +++ b/api/projects/code_references/views.py @@ -1,5 +1,6 @@ from typing import Any +import structlog from django.shortcuts import get_object_or_404 from drf_spectacular.utils import extend_schema from rest_framework import generics, response @@ -19,6 +20,8 @@ FeatureFlagCodeReferencesRepositorySummary, ) +logger = structlog.get_logger("code_references") + class FeatureFlagCodeReferencesScanCreateAPIView( generics.CreateAPIView[FeatureFlagCodeReferencesScan] @@ -33,7 +36,14 @@ class FeatureFlagCodeReferencesScanCreateAPIView( def perform_create( # type: ignore[override] self, serializer: FeatureFlagCodeReferencesScanSerializer ) -> None: - serializer.save(project_id=self.kwargs["project_pk"]) + instance = serializer.save(project_id=self.kwargs["project_pk"]) + feature_names = {ref["feature_name"] for ref in instance.code_references} + logger.info( + "scan.created", + organisation__id=instance.project.organisation_id, + code_references__count=len(instance.code_references), + feature__count=len(feature_names), + ) @extend_schema( diff --git a/api/tests/unit/features/workflows/core/test_unit_workflows_models.py b/api/tests/unit/features/workflows/core/test_unit_workflows_models.py index 501ec89d6185..c46a2f2713f0 100644 --- a/api/tests/unit/features/workflows/core/test_unit_workflows_models.py +++ b/api/tests/unit/features/workflows/core/test_unit_workflows_models.py @@ -9,6 +9,7 @@ from flag_engine.segments.constants import EQUAL, PERCENTAGE_SPLIT from freezegun.api import FrozenDateTimeFactory from pytest_mock import MockerFixture +from pytest_structlog import StructuredLogCapture from audit.constants import ( CHANGE_REQUEST_APPROVED_MESSAGE, @@ -172,6 +173,28 @@ def test_change_request_commit__not_scheduled__sets_committed_at_and_version( # assert change_request_no_required_approvals.feature_states.first().live_from == now +def test_change_request_commit__valid_request__emits_structlog_event( + change_request_no_required_approvals: ChangeRequest, + log: StructuredLogCapture, +) -> None: + # Given + user = FFAdminUser.objects.create(email="committer@example.com") + + # When + change_request_no_required_approvals.commit(committed_by=user) + + # Then + environment = change_request_no_required_approvals.environment + assert environment is not None + assert { + "event": "change_request.committed", + "level": "info", + "organisation__id": environment.project.organisation_id, + "environment__id": environment.id, + "feature_states__count": change_request_no_required_approvals.feature_states.count(), + } in log.events + + def test_change_request_create__valid_environment__creates_audit_log( # type: ignore[no-untyped-def] environment, admin_user ): diff --git a/api/tests/unit/integrations/github/test_unit_github_cleanup_issue.py b/api/tests/unit/integrations/github/test_unit_github_cleanup_issue.py index 2515cde303c3..509ee096c4a1 100644 --- a/api/tests/unit/integrations/github/test_unit_github_cleanup_issue.py +++ b/api/tests/unit/integrations/github/test_unit_github_cleanup_issue.py @@ -6,6 +6,7 @@ import responses from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture +from pytest_structlog import StructuredLogCapture from rest_framework import status from rest_framework.test import APIClient @@ -78,6 +79,7 @@ def test_create_cleanup_issue__valid_request__returns_204( admin_client_new: APIClient, organisation: Organisation, feature: Feature, + log: StructuredLogCapture, ) -> None: # Given github_issue_response_1: dict[str, Any] = { @@ -135,6 +137,15 @@ def test_create_cleanup_issue__valid_request__returns_204( request_body_2 = json.loads(responses.calls[1].request.body) assert "lib/flags.py#L7" in request_body_2["body"] + assert log.events == [ + { + "event": "cleanup_issues.created", + "level": "info", + "organisation__id": organisation.id, + "issues_created__count": 2, + }, + ] + @responses.activate @pytest.mark.usefixtures("set_github_pat", "mock_code_references") diff --git a/api/tests/unit/projects/code_references/test_unit_projects_code_references_views.py b/api/tests/unit/projects/code_references/test_unit_projects_code_references_views.py index 55b408d7ec73..70bd7980a9e7 100644 --- a/api/tests/unit/projects/code_references/test_unit_projects_code_references_views.py +++ b/api/tests/unit/projects/code_references/test_unit_projects_code_references_views.py @@ -1,4 +1,5 @@ import freezegun +from pytest_structlog import StructuredLogCapture from rest_framework.test import APIClient from features.models import Feature @@ -10,6 +11,7 @@ def test_create_code_reference__valid_payload__returns_201_with_accepted_references( admin_client_new: APIClient, project: Project, + log: StructuredLogCapture, ) -> None: # Given / When response = admin_client_new.post( @@ -63,6 +65,16 @@ def test_create_code_reference__valid_payload__returns_201_with_accepted_referen }, ] + assert log.events == [ + { + "event": "scan.created", + "level": "info", + "organisation__id": project.organisation_id, + "code_references__count": 3, + "feature__count": 2, + }, + ] + def test_create_code_reference__not_authenticated__returns_401( client: APIClient, diff --git a/infrastructure/aws/staging/ecs-task-definition-task-processor.json b/infrastructure/aws/staging/ecs-task-definition-task-processor.json index 0b1f6dffe7d7..2a12b8bf1352 100644 --- a/infrastructure/aws/staging/ecs-task-definition-task-processor.json +++ b/infrastructure/aws/staging/ecs-task-definition-task-processor.json @@ -171,6 +171,10 @@ { "name": "LOG_LEVEL", "value": "INFO" + }, + { + "name": "LOG_FORMAT", + "value": "json" } ], "secrets": [