Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds request id #63

Merged
merged 13 commits into from
Feb 18, 2025
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ updates:
time: "08:00"
day: "sunday"
target-branch: "main"
groups:
patches:
update-types:
- "patch"
reviewers:
- "juntossomosmais/loyalty"
- "juntossomosmais/loja-virtual"
Expand Down
12 changes: 12 additions & 0 deletions django_outbox_pattern/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import logging

from datetime import timedelta
from uuid import uuid4

from django import db
from django.core.cache import cache
from django.utils import timezone
from django.utils.module_loading import import_string
from request_id_django_log import local_threading
from stomp.utils import get_uuid

from django_outbox_pattern import settings
Expand All @@ -24,6 +26,15 @@ def _get_msg_id(headers):
return ret


def _get_or_create_correlation_id(headers: dict) -> str:
if "dop-correlation-id" in headers:
return headers["dop-correlation-id"]

correlation_id = str(uuid4())
logger.debug("A new dop-correlation-id was generated %s", correlation_id)
return correlation_id


class Consumer(Base):
def __init__(self, connection, username, passcode):
super().__init__(connection, username, passcode)
Expand All @@ -38,6 +49,7 @@ def __init__(self, connection, username, passcode):
self.set_listener(self.listener_name, self.listener_class(self))

def message_handler(self, body, headers):
abxsantos marked this conversation as resolved.
Show resolved Hide resolved
local_threading.request_id = _get_or_create_correlation_id(headers)
try:
body = json.loads(body)
except json.JSONDecodeError as exc:
Expand Down
9 changes: 9 additions & 0 deletions django_outbox_pattern/headers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import json

from uuid import uuid4

from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from django.utils.module_loading import import_string
from request_id_django_log.request_id import current_request_id
from request_id_django_log.settings import NO_REQUEST_ID

from django_outbox_pattern import settings


def generate_headers(message):
correlation_id = current_request_id()
if not correlation_id or correlation_id == NO_REQUEST_ID:
correlation_id = uuid4()

return {
"dop-msg-id": str(message.id),
"dop-msg-destination": message.destination,
"dop-msg-type": message.__class__.__name__,
"dop-msg-sent-time": timezone.now(),
"dop-correlation-id": correlation_id,
}


Expand Down
39 changes: 36 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-outbox-pattern"
version = "2.1.0"
version = "2.2.0"
description = "A django application to make it easier to use the transactional outbox pattern"
license = "MIT"
authors = ["Hugo Brilhante <hugobrilhante@gmail.com>"]
Expand Down Expand Up @@ -32,6 +32,7 @@ packages = [
python = ">=3.10,<4.0"
django = ">=5.0.8"
"stomp.py" = ">=8.0.1,<9"
request-id-django-log = "^0.2.0"

[tool.poetry.group.dev.dependencies]
psycopg2-binary = "^2.9.6"
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_consumer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from unittest.mock import Mock
from unittest.mock import patch
from uuid import uuid4

from django.db import transaction
from django.test import SimpleTestCase
from django.test import TransactionTestCase
from request_id_django_log import local_threading
from stomp.exception import StompException

from django_outbox_pattern.choices import StatusChoice
from django_outbox_pattern.consumers import _get_or_create_correlation_id
from django_outbox_pattern.factories import factory_consumer
from django_outbox_pattern.payloads import Payload

Expand Down Expand Up @@ -103,3 +107,30 @@ def test_consumer_start_with_correct_headers(self):
self.assertIn("x-queue-name", self.consumer.subscribe_headers)
self.assertIn("x-dead-letter-routing-key", self.consumer.subscribe_headers)
self.assertIn("x-dead-letter-exchange", self.consumer.subscribe_headers)

def test_consumer_message_handler_should_add_correlation_id_from_header_into_local_threading(self):
self.consumer.callback = lambda p: p.save()
self.consumer.message_handler('{"message": "my message"}', {"message-id": 1, "dop-correlation-id": "1234"})
self.assertEqual(self.consumer.received_class.objects.filter(status=StatusChoice.SUCCEEDED).count(), 1)
message = self.consumer.received_class.objects.filter(status=StatusChoice.SUCCEEDED).first()
self.assertEqual({"message": "my message"}, message.body)
self.assertEqual({"message-id": 1, "dop-correlation-id": "1234"}, message.headers)
self.assertEqual("1234", message.msg_id)
self.assertEqual(local_threading.request_id, message.headers["dop-correlation-id"])


class GetOrCreateCorrelationIdTest(SimpleTestCase):

def test_should_return_correlation_id_from_headers(self):
headers = {"dop-correlation-id": "1234"}
with patch("django_outbox_pattern.consumers.uuid4", wraps=uuid4) as uuid4_spy:
self.assertEqual("1234", _get_or_create_correlation_id(headers))
uuid4_spy.assert_not_called()

self.assertEqual("1234", _get_or_create_correlation_id(headers))

def test_should_create_a_new_correlation_id_given_header_without_correlation_id(self) -> None:
headers: dict = {}
with patch("django_outbox_pattern.consumers.uuid4", wraps=uuid4) as uuid4_spy:
self.assertIsNotNone(_get_or_create_correlation_id(headers))
uuid4_spy.assert_called_once()
21 changes: 21 additions & 0 deletions tests/unit/test_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from uuid import uuid4

from django.test import TransactionTestCase
from request_id_django_log import local_threading

from django_outbox_pattern.models import Published
from django_outbox_pattern.models import Received


Expand All @@ -15,3 +19,20 @@ def test_received_should_return_destination_value_when_have_headers(self):
def test_received_should_return_destination_empty_when_object_have_headers_but_without_key_destination(self):
received = Received(headers={"fake": "fake"})
self.assertEqual("", received.destination)


class PublishedTest(TransactionTestCase):

def test_published_should_add_correlation_id_header_from_current_request_id(self):
request_id = str(uuid4())
local_threading.request_id = request_id
published = Published.objects.create(destination="destination", body={"message": "Message test"})
self.assertEqual(published.headers["dop-correlation-id"], request_id)

def test_published_should_not_add_correlation_id_given_custom_header(self):
request_id = str(uuid4())
local_threading.request_id = request_id
published = Published.objects.create(
destination="destination", body={"message": "Message test"}, headers={"custom": "xpto-lalala"}
)
self.assertNotIn("dop-correlation-id", published.headers)
13 changes: 13 additions & 0 deletions tests/unit/test_producer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from unittest.mock import Mock
from unittest.mock import patch
from uuid import uuid4

from django.test import TestCase
from request_id_django_log import local_threading
from stomp.exception import StompException

from django_outbox_pattern import settings
Expand All @@ -23,6 +25,17 @@ def test_producer_send(self):
self.assertEqual(self.producer.connection.send.call_count, 1)
self.assertTrue(published.headers is not None)

def test_producer_send_should_add_correlation_id_header_from_current_request_id(self):
request_id = str(uuid4())
local_threading.request_id = request_id
published = Published.objects.create(destination="destination", body={"message": "Message test"})
self.producer.start()
self.producer.send(published)
self.producer.stop()
self.assertEqual(self.producer.connection.send.call_count, 1)
self.assertIsNotNone(published.headers)
self.assertEqual(published.headers["dop-correlation-id"], request_id)

def test_producer_send_with_header(self):
headers = {"key": "value"}
published = Published.objects.create(
Expand Down