Skip to content

Commit

Permalink
feat: add descriptive event title to dynatrace integration (#2424)
Browse files Browse the repository at this point in the history
* Refactor audit log integrations to receive audit log record

* WIP: add deployment name

* Add deployment name logic

* Linting fixes

* Fix tests

* Update test to cover all related object types

* Remove unnecessary additions to datadog test
  • Loading branch information
matthewelwell committed Jul 18, 2023
1 parent 61001ef commit f1dba53
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 89 deletions.
8 changes: 8 additions & 0 deletions api/audit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ def history_record(self):
klass = self.get_history_record_model_class(self.history_record_class_path)
return klass.objects.get(id=self.history_record_id)

@property
def environment_name(self) -> str:
return getattr(self.environment, "name", "unknown")

@property
def author_identifier(self) -> str:
return getattr(self.author, "email", "system")

@staticmethod
def get_history_record_model_class(
history_record_class_path: str,
Expand Down
8 changes: 1 addition & 7 deletions api/audit/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,7 @@ def signal_wrapper(sender, instance, **kwargs):


def _track_event_async(instance, integration_client):
event_data = integration_client.generate_event_data(
log=instance.log,
email=instance.author.email if instance.author else "",
environment_name=instance.environment.name.lower()
if instance.environment
else "",
)
event_data = integration_client.generate_event_data(audit_log_record=instance)

integration_client.track_event_async(event=event_data)

Expand Down
7 changes: 4 additions & 3 deletions api/integrations/common/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import typing
from abc import ABC, abstractmethod, abstractstaticmethod
from abc import ABC, abstractmethod

from util.util import postpone

Expand All @@ -18,8 +18,9 @@ def _track_event(self, event: dict) -> None:
def track_event_async(self, event: dict) -> None:
self._track_event(event)

@abstractstaticmethod
def generate_event_data(*args, **kwargs) -> None:
@staticmethod
@abstractmethod
def generate_event_data(*args, **kwargs) -> ...:
raise NotImplementedError()


Expand Down
7 changes: 6 additions & 1 deletion api/integrations/datadog/datadog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import requests

from audit.models import AuditLog
from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper

logger = logging.getLogger(__name__)
Expand All @@ -21,7 +22,11 @@ def __init__(self, base_url: str, api_key: str, session: requests.Session = None
self.session = session or requests.Session()

@staticmethod
def generate_event_data(log: str, email: str, environment_name: str) -> dict:
def generate_event_data(audit_log_record: AuditLog) -> dict:
log = audit_log_record.log
environment_name = audit_log_record.environment_name
email = audit_log_record.author_identifier

return {
"text": f"{log} by user {email}",
"title": "Flagsmith Feature Flag Event",
Expand Down
64 changes: 38 additions & 26 deletions api/integrations/datadog/tests/test_datadog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest

from audit.models import AuditLog
from environments.models import Environment
from integrations.datadog.datadog import EVENTS_API_URI, DataDogWrapper


Expand Down Expand Up @@ -42,59 +44,69 @@ def test_datadog_track_event(mocker):
)


def test_datadog_when_generate_event_data_with_correct_values_then_success():
def test_datadog_when_generate_event_data_with_correct_values_then_success(
django_user_model,
feature,
):
# Given
log = "some log data"
email = "tes@email.com"
env = "test"

author = django_user_model(email="test@email.com")
environment = Environment(name="test")

audit_log_record = AuditLog(log=log, author=author, environment=environment)

data_dog = DataDogWrapper(base_url="http://test.com", api_key="123key")

# When
event_data = data_dog.generate_event_data(
log=log, email=email, environment_name=env
)
event_data = data_dog.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user {email}"
expected_event_text = f"{log} by user {author.email}"

assert event_data["text"] == expected_event_text
assert len(event_data["tags"]) == 1
assert event_data["tags"][0] == "env:" + env
assert event_data["tags"][0] == f"env:{environment.name}"


def test_datadog_when_generate_event_data_with_missing_author_then_success(feature):
# Given
log = "some log data"

environment = Environment(name="test")

audit_log_record = AuditLog(log=log, environment=environment)

def test_datadog_when_generate_event_data_with_with_missing_values_then_success():
# Given no log or email data
log = None
email = None
env = "test"
data_dog = DataDogWrapper(base_url="http://test.com", api_key="123key")

# When
event_data = data_dog.generate_event_data(
log=log, email=email, environment_name=env
)
event_data = data_dog.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user {email}"
expected_event_text = f"{log} by user system"
assert event_data["text"] == expected_event_text
assert len(event_data["tags"]) == 1
assert event_data["tags"][0] == f"env:{env}"
assert event_data["tags"][0] == f"env:{environment.name}"


def test_datadog_when_generate_event_data_with_with_missing_env_then_success():
def test_datadog_when_generate_event_data_with_missing_env_then_success(
django_user_model,
feature,
):
# Given environment
log = "some log data"
email = "tes@email.com"
env = None

author = django_user_model(email="test@email.com")

audit_log_record = AuditLog(log=log, author=author)

data_dog = DataDogWrapper(base_url="http://test.com", api_key="123key")

# When
event_data = data_dog.generate_event_data(
log=log, email=email, environment_name=env
)
event_data = data_dog.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user {email}"
expected_event_text = f"{log} by user {author.email}"
assert event_data["text"] == expected_event_text
assert len(event_data["tags"]) == 1
assert event_data["tags"][0] == f"env:{env}"
assert event_data["tags"][0] == "env:unknown"
60 changes: 59 additions & 1 deletion api/integrations/dynatrace/dynatrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@

import requests

from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from features.models import Feature
from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper
from segments.models import Segment

logger = logging.getLogger(__name__)

EVENTS_API_URI = "api/v2/events/ingest"

# use 'Deployment' as a fallback to maintain current behaviour in the
# event that we cannot determine the correct name to return.
DEFAULT_DEPLOYMENT_NAME = "Deployment"


class DynatraceWrapper(AbstractBaseEventIntegrationWrapper):
def __init__(self, base_url: str, api_key: str, entity_selector: str):
Expand All @@ -30,14 +38,64 @@ def _headers(self) -> dict:
return {"Content-Type": "application/json"}

@staticmethod
def generate_event_data(log: str, email: str, environment_name: str) -> dict:
def generate_event_data(audit_log_record: AuditLog) -> dict:
log = audit_log_record.log
environment_name = audit_log_record.environment_name
email = audit_log_record.author_identifier

flag_properties = {
"event": f"{log} by user {email}",
"environment": environment_name,
"dt.event.deployment.name": _get_deployment_name(audit_log_record),
}

return {
"title": "Flagsmith flag change.",
"eventType": "CUSTOM_DEPLOYMENT",
"properties": flag_properties,
}


def _get_deployment_name(audit_log_record: AuditLog) -> str:
try:
related_object_type = RelatedObjectType[audit_log_record.related_object_type]

if related_object_type in (
RelatedObjectType.FEATURE,
RelatedObjectType.FEATURE_STATE,
):
return _get_deployment_name_for_feature(
audit_log_record.related_object_id, related_object_type
)
elif related_object_type == RelatedObjectType.SEGMENT:
return _get_deployment_name_for_segment(audit_log_record.related_object_id)
except KeyError:
pass

# use 'Deployment' as a fallback to maintain current behaviour in the
# event that we cannot determine the correct name to return.
return DEFAULT_DEPLOYMENT_NAME


def _get_deployment_name_for_feature(
object_id: int, object_type: RelatedObjectType
) -> str:
qs = Feature.objects.all_with_deleted()
if object_type == RelatedObjectType.FEATURE:
qs = qs.filter(id=object_id)
elif object_type == RelatedObjectType.FEATURE_STATE:
qs = qs.filter(feature_states__id=object_id).distinct()

if feature := qs.first():
return f"Flagsmith Deployment - Flag Changed: {feature.name}"

# use 'Deployment' as a fallback to maintain current behaviour in the
# event that we cannot determine the correct name to return.
return DEFAULT_DEPLOYMENT_NAME


def _get_deployment_name_for_segment(object_id: int) -> str:
if segment := Segment.objects.all_with_deleted().filter(id=object_id).first():
return f"Flagsmith Deployment - Segment Changed: {segment.name}"

return DEFAULT_DEPLOYMENT_NAME
104 changes: 87 additions & 17 deletions api/integrations/dynatrace/tests/test_dynatrace.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import pytest
from pytest_lazyfixture import lazy_fixture

from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from environments.models import Environment
from integrations.dynatrace.dynatrace import EVENTS_API_URI, DynatraceWrapper


Expand All @@ -17,46 +23,110 @@ def test_dynatrace_initialized_correctly():
assert dynatrace.url == expected_url


def test_dynatrace_when_generate_event_data_with_correct_values_then_success():
@pytest.mark.parametrize(
"related_object_type, related_object, expected_deployment_name",
(
(
RelatedObjectType.FEATURE.name,
lazy_fixture("feature"),
"Flagsmith Deployment - Flag Changed: Test Feature1",
),
(
RelatedObjectType.FEATURE_STATE.name,
lazy_fixture("feature_state"),
"Flagsmith Deployment - Flag Changed: Test Feature1",
),
(
RelatedObjectType.SEGMENT.name,
lazy_fixture("segment"),
"Flagsmith Deployment - Segment Changed: segment",
),
),
)
def test_dynatrace_when_generate_event_data_with_correct_values_then_success(
django_user_model, related_object_type, related_object, expected_deployment_name
):
# Given
log = "some log data"
email = "tes@email.com"
env = "test"

author = django_user_model(email="test@email.com")
environment = Environment(name="test")

audit_log_record = AuditLog(
log=log,
author=author,
environment=environment,
related_object_type=related_object_type,
related_object_id=related_object.id,
)

dynatrace = DynatraceWrapper(
base_url="http://test.com",
api_key="123key",
entity_selector="type(APPLICATION),entityName(docs)",
)

# When
event_data = dynatrace.generate_event_data(
log=log, email=email, environment_name=env
)
event_data = dynatrace.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user {email}"
expected_event_text = f"{log} by user {author.email}"

assert event_data["properties"]["event"] == expected_event_text
assert event_data["properties"]["environment"] == env
assert event_data["properties"]["environment"] == environment.name
assert (
event_data["properties"]["dt.event.deployment.name"] == expected_deployment_name
)


def test_dynatrace_when_generate_event_data_with_with_missing_values_then_success():
# Given no log or email data
log = None
email = None
env = "test"
def test_dynatrace_when_generate_event_data_with_missing_author_then_success():
# Given
log = "some log data"

environment = Environment(name="test")

audit_log_record = AuditLog(log=log, environment=environment)

dynatrace = DynatraceWrapper(
base_url="http://test.com",
api_key="123key",
entity_selector="type(APPLICATION),entityName(docs)",
)

# When
event_data = dynatrace.generate_event_data(
log=log, email=email, environment_name=env
event_data = dynatrace.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user system"
assert event_data["properties"]["event"] == expected_event_text
assert event_data["properties"]["environment"] == environment.name


def test_dynatrace_when_generate_event_data_with_missing_environment_then_success(
django_user_model, feature
):
# Given
log = "some log data"

author = django_user_model(email="test@example.com")

audit_log_record = AuditLog(
log=log,
author=author,
related_object_type=RelatedObjectType.FEATURE.name,
related_object_id=feature.id,
)

dynatrace = DynatraceWrapper(
base_url="http://test.com",
api_key="123key",
entity_selector="type(APPLICATION),entityName(docs)",
)

# When
event_data = dynatrace.generate_event_data(audit_log_record=audit_log_record)

# Then
expected_event_text = f"{log} by user {email}"
expected_event_text = f"{log} by user {author.email}"
assert event_data["properties"]["event"] == expected_event_text
assert event_data["properties"]["environment"] == env
assert event_data["properties"]["environment"] == "unknown"

3 comments on commit f1dba53

@vercel
Copy link

@vercel vercel bot commented on f1dba53 Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on f1dba53 Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on f1dba53 Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs-git-main-flagsmith.vercel.app
docs-flagsmith.vercel.app
docs.flagsmith.com
docs.bullet-train.io

Please sign in to comment.