Repository: juntossomosmais/opentelemetry-instrumentation-django-stomp (language: python)

Issue:
feat(core): included wrappers and tests for opentelemetry instrumentation
## Infos

Create `DjangoStompInstrumentor` to create wrapper from django-stomp code and instrument publisher, consumer, ack and nack traces. more details in [README.md](https://github.com/juntossomosmais/opentelemetry-instrumentation-django-stomp/pull/1/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5)

## DjangoStompInstrumentor
- main instrument and uninstrument function to initialize `django-stomp` instrumentators;
- The function `DjangoStompInstrumentor.instrument()` execute `ConsumerInstrument.instrument()` and `PublisherInstrument.instrument()`;

## ConsumerInstrument
- consumer instrument and uninstrument function to create wrappers to trace "CONSUMER", "ACK" and "ACK";
- wrappers on 
```python
wrapt.wrap_function_wrapper(Listener, "on_message", wrapper_on_message)
wrapt.wrap_function_wrapper(Listener, "_create_new_worker_executor", wrapper_create_new_worker_executor)
wrapt.wrap_function_wrapper(StompConnection11, "nack", wrapper_nack)
wrapt.wrap_function_wrapper(StompConnection11, "ack", wrapper_ack)
```

## PublisherInstrument
- publisher instrument and uninstrument function to create wrappers to trace "CONSUMER", "ACK" and "ACK";
- wrappers on 
```python
wrapt.wrap_function_wrapper(Publisher, "_send_to_broker_without_retry_attempts", wrapper_publisher)
wrapt.wrap_function_wrapper(Publisher, "_send_to_broker", wrapper_publisher)
```


📁 Folder Structure with tests
```bash
opentelemetry_instrumentation_django_stomp
    └── opentelemetry_instrumentation_django_stomp
        ├── __init__.py
        ├── instrumentors
        │   ├── __init__.py
        │   ├── consumer_instrument.py
        │   └── publisher_instrument.py
        ├── package.py
        ├── utils
        │   ├── __init__.py
        │   ├── django_stomp_getter.py
        │   ├── shared_types.py
        │   ├── span.py
        │   └── traced_thread_pool_executor.py
        └── version.py
    └── tests
        ├── __init__.py
        ├── conftest.py
        ├── instrumentors
        │   ├── __init__.py
        │   ├── test_consumer_instrument.py
        │   └── test_publisher_instrument.py
        ├── resources
        │   └── custom-rabbitmq-conf
        │       ├── enabled_plugins
        │       └── rabbitmq.conf
        ├── support
        │   ├── __init__.py
        │   ├── helpers_tests.py
        │   └── otel_helpers.py
        └── utils
            ├── __init__.py
            ├── test_django_stomp_getter.py
            ├── test_span.py
            └── test_traced_thread_pool_executor.py
```


Discussion / hints:

Fix patch:
diff --git a/.env b/.env
new file mode 100644
index 0000000..9427980
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+STOMP_SERVER_HOST=broker-rabbitmq
+STOMP_SERVER_PORT=61613
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2b60eb8..99a3641 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pycqa/flake8
-    rev: 6.0.0
+    rev: 5.0.4
     hooks:
       - id: flake8
         additional_dependencies: [
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..104d053
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,25 @@
+FROM python:3.7-slim
+
+# Git is required for pre-commit
+RUN apt update
+RUN apt install -y git
+
+# Creates application directory
+WORKDIR /app
+
+# Creates an appuser and change the ownership of the application's folder
+RUN useradd appuser && chown appuser ./
+
+# Installs poetry and pip
+RUN pip install --upgrade pip && \
+    pip install poetry && \
+    poetry config virtualenvs.create false --local
+
+# Copy dependency definition to cache
+COPY --chown=appuser poetry.lock pyproject.toml ./
+
+# Installs projects dependencies as a separate layer
+RUN poetry install --no-root
+
+# Copies and chowns for the userapp on a single layer
+COPY --chown=appuser . ./
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..eb3e4f3
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,57 @@
+version: "3.9"
+
+# https://docs.docker.com/compose/compose-file/compose-file-v3/#extension-fields
+x-build: &dockerfile-dev-build
+  context: .
+  dockerfile: Dockerfile.dev
+
+services:
+
+  integration-tests:
+    build: *dockerfile-dev-build
+    volumes:
+      - .:/app
+    depends_on:
+      - broker-rabbitmq
+    command:
+      [
+        "./scripts/wait-for-it.sh",
+        "${STOMP_SERVER_HOST}:${STOMP_SERVER_PORT}",
+        "-t",
+        "120",
+        "--",
+        "./scripts/start-tests.sh",
+      ]
+    networks:
+      - instrumentationdjangostomp
+
+  lint-formatter:
+    build: *dockerfile-dev-build
+    volumes:
+      - .:/app
+    command: ["./scripts/start-formatter-lint.sh"]
+    networks:
+      - instrumentationdjangostomp
+
+  broker-activemq:
+    image: rmohr/activemq:latest
+    ports:
+      - 8161:8161
+      - 61613:61613
+    volumes:
+      - ./tests/resources/custom-activemq-conf:/opt/apache-activemq-5.15.6/conf
+    networks:
+      - instrumentationdjangostomp
+
+  broker-rabbitmq:
+    image: rabbitmq:3-management
+    ports:
+      - 61613:61613
+      - 15672:15672
+    volumes:
+      - ./tests/resources/custom-rabbitmq-conf:/etc/rabbitmq/
+    networks:
+      - instrumentationdjangostomp
+
+networks:
+  instrumentationdjangostomp:
\ No newline at end of file
diff --git a/opentelemetry_instrumentation_django_stomp/__init__.py b/opentelemetry_instrumentation_django_stomp/__init__.py
new file mode 100644
index 0000000..25f7ab6
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/__init__.py
@@ -0,0 +1,114 @@
+"""
+This library supports the `django-stomp` library, it can be enabled by
+using ``DjangoStompInstrumentor``.
+
+*****************************************
+USAGE
+-----
+In project manage.py you can include the example code below
+
+.. code-block:: python
+    from opentelemetry_instrumentation_django_stomp import DjangoStompInstrumentor
+
+    def publisher_hook(span: Span, body: Dict, headers: Dict):
+        # Custom code
+        pass
+
+    def consumer_hook(span: Span, body: Dict, headers: Dict):
+        # Custom code
+        pass
+
+    provider = TracerProvider()
+    trace.set_tracer_provider(provider)
+    trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
+
+    DjangoStompInstrumentor().instrument(
+        trace_provider=trace,
+        publisher_hook=publisher_hook,
+        consumer_hook=consumer_hook,
+    )
+
+*****************************************
+PUBLISHER
+-----
+With the django-stomp we can publish a message to broker using `publisher.send` and the instrumentor
+can include a span with telemetry data in this function utilization.
+
+.. code-block:: python
+   publisher = build_publisher(f"publisher-unique-name-{uuid4()}")
+    publisher.send(
+        queue=DESTINATION,
+        body={"a": "random","body": "message},
+    )
+
+*****************************************
+CONSUMER
+-----
+With the django-stomp we create a simple consumer using pubsub command and the instrumentor
+can include a span with telemetry data in this function utilization.
+
+.. code-block:: python
+   python manage.py pubsub QUEUE_NAME callback_function_to_consume_message
+"""
+import threading
+import typing
+
+from django.conf import settings
+from opentelemetry import trace
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.trace import TracerProvider
+
+from .instrumentors.consumer_instrument import ConsumerInstrument
+from .instrumentors.publisher_instrument import PublisherInstrument
+from .package import _instruments
+from .utils.shared_types import CallbackHookT
+from .version import __version__
+
+_CTX_KEY = "__otel_django_stomp_span"
+
+local_threading = threading.local()
+
+
+class DjangoStompInstrumentor(BaseInstrumentor):
+    def instrumentation_dependencies(self) -> typing.Collection[str]:
+        """
+        Function to check compatibility with dependencies package(django-stomp)
+        """
+        return _instruments
+
+    def _uninstrument(self, **kwargs):
+        """
+        Function to unwrap publisher and consumer functions from django-stomp
+        """
+        if hasattr(self, "__opentelemetry_tracer_provider"):
+            delattr(self, "__opentelemetry_tracer_provider")
+        ConsumerInstrument().uninstrument()
+        PublisherInstrument().uninstrument()
+
+    def _instrument(self, **kwargs) -> None:
+        """
+        Instrument function to initialize wrappers in publisher and consumer functions from django-stomp.
+
+        Args:
+            kwargs (typing.Dict[str, typing.Any]):
+                trace_provider (Optional[TracerProvider]): The tracer provider to use in open-telemetry spans.
+                publisher_hook (CallbackHookT): The callable function to call before original function call, use
+                this to override or enrich the span created in main project.
+                consumer_hook (CallbackHookT): The callable function to call before original function call, use
+                this to override or enrich the span created in main project.
+
+        Returns:
+        """
+        instrument_django_stomp = getattr(settings, "OTEL_PYTHON_DJANGO_STOMP_INSTRUMENT", True)
+        if not instrument_django_stomp:
+            return None
+
+        tracer_provider: typing.Optional[TracerProvider] = kwargs.get("tracer_provider", None)
+        publisher_hook: CallbackHookT = kwargs.get("publisher_hook", None)
+        consumer_hook: CallbackHookT = kwargs.get("consumer_hook", None)
+
+        self.__setattr__("__opentelemetry_tracer_provider", tracer_provider)
+        tracer = trace.get_tracer(__name__, __version__, tracer_provider)
+
+        ConsumerInstrument().instrument(tracer=tracer, callback_hook=consumer_hook)
+        PublisherInstrument().instrument(tracer=tracer, callback_hook=publisher_hook)
diff --git a/opentelemetry_instrumentation_django_stomp/instrumentors/__init__.py b/opentelemetry_instrumentation_django_stomp/instrumentors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/opentelemetry_instrumentation_django_stomp/instrumentors/consumer_instrument.py b/opentelemetry_instrumentation_django_stomp/instrumentors/consumer_instrument.py
new file mode 100644
index 0000000..2cfd020
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/instrumentors/consumer_instrument.py
@@ -0,0 +1,92 @@
+import logging
+import typing
+
+import wrapt
+
+from django_stomp.services.consumer import Listener
+from django_stomp.settings import STOMP_PROCESS_MSG_WORKERS
+from opentelemetry import context
+from opentelemetry import propagate
+from opentelemetry import trace
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.sdk.trace import Tracer
+from opentelemetry.semconv.trace import MessagingOperationValues
+from opentelemetry.trace import SpanKind
+from stomp.connect import StompConnection11
+
+from opentelemetry_instrumentation_django_stomp.utils.django_stomp_getter import DjangoStompGetter
+from opentelemetry_instrumentation_django_stomp.utils.shared_types import CallbackHookT
+from opentelemetry_instrumentation_django_stomp.utils.span import enrich_span_with_host_data
+from opentelemetry_instrumentation_django_stomp.utils.span import get_span
+from opentelemetry_instrumentation_django_stomp.utils.traced_thread_pool_executor import TracedThreadPoolExecutor
+
+_django_stomp_getter = DjangoStompGetter()
+
+_logger = logging.getLogger(__name__)
+
+
+class ConsumerInstrument:
+    @staticmethod
+    def instrument(tracer: Tracer, callback_hook: CallbackHookT = None):
+        """Instrumentor function to create span and instrument consumer"""
+
+        def wrapper_on_message(wrapped, instance, args, kwargs):
+            frame = args[0]
+            headers, body = frame.headers, frame.body
+
+            ctx = propagate.extract(headers, getter=_django_stomp_getter)
+            if not ctx:
+                ctx = context.get_current()
+            token = context.attach(ctx)
+
+            span = get_span(
+                tracer=tracer,
+                destination=headers.get("tshoot-destination"),
+                span_kind=SpanKind.CONSUMER,
+                headers=headers,
+                body=body,
+                span_name="CONSUMER",
+                operation=str(MessagingOperationValues.RECEIVE.value),
+            )
+
+            try:
+                with trace.use_span(span, end_on_exit=True):
+                    if callback_hook:
+                        try:
+                            callback_hook(span, body, headers)
+                        except Exception as hook_exception:  # pylint: disable=W0703
+                            _logger.exception(hook_exception)
+                    return wrapped(*args, **kwargs)
+            finally:
+                context.detach(token)
+
+        def wrapper_create_new_worker_executor(wrapped, instance, *args, **kwargs):
+            return TracedThreadPoolExecutor(
+                tracer=trace.get_tracer(__name__),
+                max_workers=STOMP_PROCESS_MSG_WORKERS,
+                thread_name_prefix=instance._subscription_id,
+            )
+
+        def common_ack_or_nack_span(span_name: str, wrapped_function: typing.Callable):
+            span = tracer.start_span(name=span_name, kind=SpanKind.CONSUMER)
+            enrich_span_with_host_data(span)
+            with trace.use_span(span, end_on_exit=True):
+                return wrapped_function
+
+        def wrapper_nack(wrapped, instance, args, kwargs):
+            return common_ack_or_nack_span("NACK", wrapped(*args, **kwargs))
+
+        def wrapper_ack(wrapped, instance, args, kwargs):
+            return common_ack_or_nack_span("ACK", wrapped(*args, **kwargs))
+
+        wrapt.wrap_function_wrapper(Listener, "on_message", wrapper_on_message)
+        wrapt.wrap_function_wrapper(Listener, "_create_new_worker_executor", wrapper_create_new_worker_executor)
+        wrapt.wrap_function_wrapper(StompConnection11, "nack", wrapper_nack)
+        wrapt.wrap_function_wrapper(StompConnection11, "ack", wrapper_ack)
+
+    @staticmethod
+    def uninstrument():
+        unwrap(Listener, "on_message")
+        unwrap(Listener, "_create_new_worker_executor")
+        unwrap(StompConnection11, "nack")
+        unwrap(StompConnection11, "ack")
diff --git a/opentelemetry_instrumentation_django_stomp/instrumentors/publisher_instrument.py b/opentelemetry_instrumentation_django_stomp/instrumentors/publisher_instrument.py
new file mode 100644
index 0000000..89ffca8
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/instrumentors/publisher_instrument.py
@@ -0,0 +1,53 @@
+import logging
+
+import wrapt
+
+from django_stomp.services.producer import Publisher
+from opentelemetry import propagate
+from opentelemetry import trace
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.sdk.trace import Tracer
+from opentelemetry.trace import SpanKind
+
+from opentelemetry_instrumentation_django_stomp.utils.shared_types import CallbackHookT
+from opentelemetry_instrumentation_django_stomp.utils.span import get_span
+
+_logger = logging.getLogger(__name__)
+
+
+class PublisherInstrument:
+    @staticmethod
+    def instrument(tracer: Tracer, callback_hook: CallbackHookT = None):
+        """Instrumentor to create span and instrument publisher"""
+
+        def wrapper_publisher(wrapped, instance, args, kwargs):
+            payload = args[0]
+            headers, body = payload.get("headers"), payload.get("body")
+
+            span = get_span(
+                tracer=tracer,
+                destination=headers.get("tshoot-destination"),
+                span_kind=SpanKind.PRODUCER,
+                headers=headers,
+                body=body,
+                span_name="PUBLISHER",
+            )
+
+            with trace.use_span(span, end_on_exit=True):
+                if span.is_recording():
+                    propagate.inject(headers)
+                    if callback_hook:
+                        try:
+                            callback_hook(span, body, headers)
+                        except Exception as hook_exception:  # pylint: disable=W0703
+                            _logger.exception(hook_exception)
+                return wrapped(*args, **kwargs)
+
+        wrapt.wrap_function_wrapper(Publisher, "_send_to_broker_without_retry_attempts", wrapper_publisher)
+        wrapt.wrap_function_wrapper(Publisher, "_send_to_broker", wrapper_publisher)
+
+    @staticmethod
+    def uninstrument():
+        """Uninstrument publisher functions from django-stomp"""
+        unwrap(Publisher, "_send_to_broker_without_retry_attempts")
+        unwrap(Publisher, "_send_to_broker")
diff --git a/opentelemetry_instrumentation_django_stomp/package.py b/opentelemetry_instrumentation_django_stomp/package.py
new file mode 100644
index 0000000..7563491
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/package.py
@@ -0,0 +1,3 @@
+from typing import Collection
+
+_instruments: Collection[str] = ("django-stomp >= 5.0.0",)
diff --git a/opentelemetry_instrumentation_django_stomp/utils/__init__.py b/opentelemetry_instrumentation_django_stomp/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/opentelemetry_instrumentation_django_stomp/utils/django_stomp_getter.py b/opentelemetry_instrumentation_django_stomp/utils/django_stomp_getter.py
new file mode 100644
index 0000000..af08046
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/utils/django_stomp_getter.py
@@ -0,0 +1,15 @@
+import typing
+
+from opentelemetry.propagators.textmap import CarrierT
+from opentelemetry.propagators.textmap import Getter
+
+
+class DjangoStompGetter(Getter[CarrierT]):
+    """Propagators class to get trace-parent header from a message from messaging broker"""
+
+    def get(self, carrier: CarrierT, key: str) -> typing.Optional[typing.List[str]]:
+        value = carrier.get(key, None)
+        return [value] if value is not None else None
+
+    def keys(self, carrier: CarrierT) -> typing.List[str]:
+        return []
diff --git a/opentelemetry_instrumentation_django_stomp/utils/shared_types.py b/opentelemetry_instrumentation_django_stomp/utils/shared_types.py
new file mode 100644
index 0000000..28ad7dc
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/utils/shared_types.py
@@ -0,0 +1,5 @@
+import typing
+
+from opentelemetry.trace.span import Span
+
+CallbackHookT = typing.Optional[typing.Callable[[Span, typing.Dict, typing.Dict], None]]
diff --git a/opentelemetry_instrumentation_django_stomp/utils/span.py b/opentelemetry_instrumentation_django_stomp/utils/span.py
new file mode 100644
index 0000000..8509e42
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/utils/span.py
@@ -0,0 +1,61 @@
+import json
+import sys
+import typing
+
+from django.conf import settings
+from opentelemetry.sdk.trace import Span
+from opentelemetry.sdk.trace import Tracer
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import SpanKind
+
+
+def enrich_span_with_host_data(span: Span):
+    """Helper function add broker SpanAttributes"""
+    system = getattr(settings, "STOMP_SYSTEM", None) or "rabbitmq"
+    attributes = {
+        SpanAttributes.NET_PEER_NAME: settings.STOMP_SERVER_HOST,
+        SpanAttributes.NET_PEER_PORT: settings.STOMP_SERVER_PORT,
+        SpanAttributes.MESSAGING_SYSTEM: system,
+    }
+    span.set_attributes(attributes)
+
+
+def enrich_span(
+    span: Span,
+    operation: typing.Optional[str],
+    destination: str,
+    headers: typing.Dict,
+    body: typing.Dict,
+) -> None:
+    """Helper function add SpanAttributes"""
+    attributes = {
+        SpanAttributes.MESSAGING_DESTINATION: destination,
+        SpanAttributes.MESSAGING_CONVERSATION_ID: str(headers.get("correlation-id")),
+        SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: sys.getsizeof(json.dumps(body)),
+    }
+    if operation is not None:
+        attributes.update({SpanAttributes.MESSAGING_OPERATION: operation})
+    span.set_attributes(attributes)
+    enrich_span_with_host_data(span)
+
+
+def get_span(
+    tracer: Tracer,
+    destination: str,
+    span_kind: SpanKind,
+    headers: typing.Dict,
+    body: typing.Dict,
+    span_name: str,
+    operation: typing.Optional[str] = None,
+) -> Span:
+    """Helper function to mount span and call function to set SpanAttributes"""
+    span = tracer.start_span(name=span_name, kind=span_kind)
+    if span.is_recording():
+        enrich_span(
+            span=span,
+            operation=operation,
+            destination=destination,
+            headers=headers,
+            body=body,
+        )
+    return span
diff --git a/opentelemetry_instrumentation_django_stomp/utils/traced_thread_pool_executor.py b/opentelemetry_instrumentation_django_stomp/utils/traced_thread_pool_executor.py
new file mode 100644
index 0000000..22bbff1
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/utils/traced_thread_pool_executor.py
@@ -0,0 +1,29 @@
+import typing
+
+from concurrent.futures import ThreadPoolExecutor
+
+from opentelemetry import context as otel_context
+from opentelemetry.sdk.trace import Tracer
+
+
+def with_otel_context(context: otel_context.Context, fn: typing.Callable):
+    otel_context.attach(context)
+    return fn()
+
+
+class TracedThreadPoolExecutor(ThreadPoolExecutor):
+    """Implementation of :class:`ThreadPoolExecutor` that will pass context into sub tasks."""
+
+    def __init__(self, tracer: Tracer, *args, **kwargs):
+        self.tracer = tracer
+        super().__init__(*args, **kwargs)
+
+    def submit(self, fn, *args, **kwargs):
+        """Submit a new task to the thread pool."""
+        context = otel_context.get_current()
+        if context:
+            return super().submit(
+                lambda: with_otel_context(context, lambda: fn(*args, **kwargs)),
+            )
+        else:
+            return super().submit(lambda: fn(*args, **kwargs))
diff --git a/opentelemetry_instrumentation_django_stomp/version.py b/opentelemetry_instrumentation_django_stomp/version.py
new file mode 100644
index 0000000..3dc1f76
--- /dev/null
+++ b/opentelemetry_instrumentation_django_stomp/version.py
@@ -0,0 +1,1 @@
+__version__ = "0.1.0"
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..56c8448
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1091 @@
+[[package]]
+name = "asgiref"
+version = "3.5.2"
+description = "ASGI specs, helper code, and adapters"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
+
+[[package]]
+name = "async-generator"
+version = "1.10"
+description = "Async generators and context managers for Python 3.5+"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
+
+[[package]]
+name = "autoflake"
+version = "1.7.8"
+description = "Removes unused imports and unused variables"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pyflakes = ">=1.1.0,<3"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "black"
+version = "22.10.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "coverage"
+version = "6.5.0"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "deprecated"
+version = "1.2.13"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"]
+
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "django"
+version = "3.2.16"
+description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+asgiref = ">=3.3.2,<4"
+pytz = "*"
+sqlparse = ">=0.2.2"
+
+[package.extras]
+argon2 = ["argon2-cffi (>=19.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "django-stomp"
+version = "5.1.1"
+description = "A simple implementation of STOMP with Django"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+request-id-django-log = "0.1.1"
+"stomp.py" = ">=7.0,<8.0"
+tenacity = ">=8.0,<9.0"
+
+[[package]]
+name = "docopt"
+version = "0.6.2"
+description = "Pythonic argument parser, that will make you smile"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "exceptiongroup"
+version = "1.0.4"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "execnet"
+version = "1.9.0"
+description = "execnet: rapid multi-Python deployment"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+testing = ["pre-commit"]
+
+[[package]]
+name = "filelock"
+version = "3.8.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
+testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "flake8"
+version = "5.0.4"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""}
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.9.0,<2.10.0"
+pyflakes = ">=2.5.0,<2.6.0"
+
+[[package]]
+name = "flake8-bugbear"
+version = "22.10.27"
+description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+flake8 = ">=3.0.0"
+
+[package.extras]
+dev = ["tox", "coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"]
+
+[[package]]
+name = "icdiff"
+version = "2.0.5"
+description = "improved colored diff"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "identify"
+version = "2.5.9"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "importlib-metadata"
+version = "4.2.0"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.10.1"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1,<4.0"
+
+[package.extras]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+plugins = ["setuptools"]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "mypy"
+version = "0.991"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "nodeenv"
+version = "1.7.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.14.0"
+description = "OpenTelemetry Python API"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+deprecated = ">=1.2.6"
+
+[[package]]
+name = "opentelemetry-instrumentation"
+version = "0.35b0"
+description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+opentelemetry-api = ">=1.4,<2.0"
+wrapt = ">=1.0.0,<2.0.0"
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.14.0"
+description = "OpenTelemetry Python SDK"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+opentelemetry-api = "1.14.0"
+opentelemetry-semantic-conventions = "0.35b0"
+typing-extensions = ">=3.7.4"
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.35b0"
+description = "OpenTelemetry Semantic Conventions"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "outcome"
+version = "1.2.0"
+description = "Capture the outcome of Python function calls."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pathspec"
+version = "0.10.2"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "platformdirs"
+version = "2.5.4"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"]
+test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pprintpp"
+version = "0.4.0"
+description = "A drop-in replacement for pprint that's actually pretty"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pre-commit"
+version = "2.20.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pycodestyle"
+version = "2.9.1"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyflakes"
+version = "2.5.0"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pygments"
+version = "2.13.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "dev"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["railroad-diagrams", "jinja2"]
+
+[[package]]
+name = "pytest"
+version = "7.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-clarity"
+version = "1.0.1"
+description = "A plugin providing an alternative, colourful diff output for failing assertions."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pprintpp = ">=0.4.0"
+pytest = ">=3.5.0"
+rich = ">=8.0.0"
+
+[[package]]
+name = "pytest-cov"
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pytest-django"
+version = "4.5.2"
+description = "A Django plugin for pytest."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+pytest = ">=5.4.0"
+
+[package.extras]
+docs = ["sphinx", "sphinx-rtd-theme"]
+testing = ["django", "django-configurations (>=2.0)"]
+
+[[package]]
+name = "pytest-env"
+version = "0.8.1"
+description = "py.test plugin that allows you to add environment variables."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=7.1.3"
+
+[package.extras]
+test = ["coverage (>=6.5)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pytest-icdiff"
+version = "0.6"
+description = "use icdiff for better error messages in pytest assertions"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+icdiff = "*"
+pprintpp = "*"
+pytest = "*"
+
+[[package]]
+name = "pytest-mock"
+version = "3.10.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "tox", "pytest-asyncio"]
+
+[[package]]
+name = "pytest-trio"
+version = "0.8.0"
+description = "Pytest plugin for trio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+outcome = ">=1.1.0"
+pytest = ">=7.2.0"
+trio = ">=0.22.0"
+
+[[package]]
+name = "pytest-xdist"
+version = "3.0.2"
+description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+execnet = ">=1.1"
+pytest = ">=6.2.0"
+
+[package.extras]
+psutil = ["psutil (>=3.0)"]
+setproctitle = ["setproctitle"]
+testing = ["filelock"]
+
+[[package]]
+name = "pytz"
+version = "2022.6"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "request-id-django-log"
+version = "0.1.1"
+description = "A simple way to implement request_id in Django"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "rich"
+version = "12.6.0"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "dev"
+optional = false
+python-versions = ">=3.6.3,<4.0.0"
+
+[package.dependencies]
+commonmark = ">=0.9.0,<0.10.0"
+pygments = ">=2.6.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "sqlparse"
+version = "0.4.3"
+description = "A non-validating SQL parser."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "stomp.py"
+version = "7.0.0"
+description = "Python STOMP client, supporting versions 1.0, 1.1 and 1.2 of the protocol"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+docopt = ">=0.6.2,<0.7.0"
+
+[[package]]
+name = "tenacity"
+version = "8.1.0"
+description = "Retry code until it succeeds"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+doc = ["reno", "sphinx", "tornado (>=4.5)"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tox"
+version = "3.27.1"
+description = "tox is a generic virtualenv management and test command line tool"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""}
+filelock = ">=3.0.0"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+packaging = ">=14"
+pluggy = ">=0.12.0"
+py = ">=1.4.17"
+six = ">=1.14.0"
+tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}
+virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
+
+[package.extras]
+docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
+testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"]
+
+[[package]]
+name = "trio"
+version = "0.22.0"
+description = "A friendly Python library for async concurrency and I/O"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+async-generator = ">=1.9"
+attrs = ">=19.2.0"
+cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
+exceptiongroup = {version = ">=1.0.0rc9", markers = "python_version < \"3.11\""}
+idna = "*"
+outcome = "*"
+sniffio = "*"
+sortedcontainers = "*"
+
+[[package]]
+name = "typed-ast"
+version = "1.5.4"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "types-requests"
+version = "2.28.11.5"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+types-urllib3 = "<1.27"
+
+[[package]]
+name = "types-urllib3"
+version = "1.26.25.4"
+description = "Typing stubs for urllib3"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "virtualenv"
+version = "20.16.2"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+distlib = ">=0.3.1,<1"
+filelock = ">=3.2,<4"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+platformdirs = ">=2,<3"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"]
+
+[[package]]
+name = "wrapt"
+version = "1.14.1"
+description = "Module for decorators, wrappers and monkey patching."
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "zipp"
+version = "3.11.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.7"
+content-hash = "d8773d6f8a69626be7347797a71f5179e2565fe63a8259a203d34214a020fef1"
+
+[metadata.files]
+asgiref = []
+async-generator = [
+    {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
+    {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"},
+]
+attrs = []
+autoflake = []
+black = []
+cffi = []
+cfgv = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+click = []
+colorama = []
+commonmark = [
+    {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+    {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+coverage = []
+deprecated = []
+distlib = []
+django = []
+django-stomp = []
+docopt = [
+    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
+]
+exceptiongroup = []
+execnet = [
+    {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
+    {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
+]
+filelock = []
+flake8 = []
+flake8-bugbear = []
+icdiff = []
+identify = []
+idna = []
+importlib-metadata = [
+    {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
+    {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isort = [
+    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
+    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
+]
+mccabe = []
+mypy = []
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+nodeenv = []
+opentelemetry-api = []
+opentelemetry-instrumentation = []
+opentelemetry-sdk = []
+opentelemetry-semantic-conventions = []
+outcome = []
+packaging = [
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pathspec = []
+platformdirs = []
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pprintpp = [
+    {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"},
+    {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"},
+]
+pre-commit = []
+py = [
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pycodestyle = []
+pycparser = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+pyflakes = []
+pygments = []
+pyparsing = []
+pytest = []
+pytest-clarity = [
+    {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"},
+]
+pytest-cov = [
+    {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
+    {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
+]
+pytest-django = [
+    {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
+    {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
+]
+pytest-env = []
+pytest-icdiff = []
+pytest-mock = []
+pytest-trio = []
+pytest-xdist = []
+pytz = []
+pyyaml = [
+    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+request-id-django-log = [
+    {file = "request_id_django_log-0.1.1-py3-none-any.whl", hash = "sha256:b202535be8de5f9568686344f71a19b899ec021f216f61d1313de871959021b1"},
+    {file = "request_id_django_log-0.1.1.tar.gz", hash = "sha256:63e636dc73c529e98fb4588eb79ee1b2778ca5b0d6abb7f2221167a8c94c5eef"},
+]
+rich = []
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+sniffio = []
+sortedcontainers = [
+    {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
+    {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
+]
+sqlparse = []
+"stomp.py" = [
+    {file = "stomp.py-7.0.0-py3-none-any.whl", hash = "sha256:6e1d93f2b2a7c63301f3e09e7ffa82ea80affec59164cd8c9b7807af4fe0e732"},
+    {file = "stomp.py-7.0.0.tar.gz", hash = "sha256:fb301f338292b1b95089c6f1d3a38a9dd8353a5ff3f921e389dfa5f9413b5a8a"},
+]
+tenacity = []
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tox = []
+trio = []
+typed-ast = []
+types-requests = []
+types-urllib3 = []
+typing-extensions = []
+virtualenv = []
+wrapt = []
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..dd298fe
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,71 @@
+[tool.poetry]
+name = "opentelemetry-instrumentation-django-stomp"
+version = "0.1.0"
+description = "Opentelemetry instrumentor for django-stomp package"
+authors = ["Juntos Somos Mais <labs@juntossomosmais.com.br>"]
+readme = "README.md"
+license = "MIT"
+include = [
+    "LICENSE",
+]
+homepage = "https://github.com/juntossomosmais/opentelemetry-instrumentation-django-stomp"
+repository = "https://github.com/juntossomosmais/opentelemetry-instrumentation-django-stomp"
+keywords=["django-stomp", "stomp", "django", "opentelemetry", "instrumentation"]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+Django = "*"
+django-stomp = "^5.0.0"
+opentelemetry-sdk = "^1.14.0"
+opentelemetry-api = "^1.14.0"
+opentelemetry-instrumentation = "^0.35b0"
+
+[tool.poetry.dev-dependencies]
+### Tests Runner
+tox = "^3.24.5"
+trio = "^0.22.0"
+pytest-trio = "^0.8.0"
+### Pytest: Essentials
+pytest = "^7"
+pytest-mock = "^3"
+pytest-django = "^4"
+pytest-cov = "^3"
+pytest-env = "^0"
+pytest-xdist = "*"
+### Pytest: Add-ons
+pytest-icdiff = "^0"
+pytest-clarity = "^1"
+### Format, lint, static type checker, among others
+black = "*"
+mypy = "^0"
+isort = "*"
+flake8 = "*"
+flake8-bugbear = "*"
+autoflake = "^1"
+pre-commit = "^2"
+types-requests = "*"
+
+[tool.black]
+line-length = 120
+target_version = ["py37"]
+include = '\.pyi?$'
+exclude = '''
+(
+  /(
+      \.eggs
+    | \.git
+    | \.hg
+    | \.mypy_cache
+    | \.tox
+    | \.venv
+    | _build
+    | buck-out
+    | build
+    | dist
+  )/
+)
+'''
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/scripts/start-formatter-lint.sh b/scripts/start-formatter-lint.sh
new file mode 100755
index 0000000..d0f45f4
--- /dev/null
+++ b/scripts/start-formatter-lint.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+pre-commit run --all-files
\ No newline at end of file
diff --git a/scripts/wait-for-it.sh b/scripts/wait-for-it.sh
new file mode 100755
index 0000000..5b3a6f9
--- /dev/null
+++ b/scripts/wait-for-it.sh
@@ -0,0 +1,177 @@
+#!/usr/bin/env bash
+#   Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename $0)
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $TIMEOUT -gt 0 ]]; then
+        echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+    else
+        echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+    fi
+    start_ts=$(date +%s)
+    while :
+    do
+        if [[ $ISBUSY -eq 1 ]]; then
+            nc -z $HOST $PORT
+            result=$?
+        else
+            (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+            result=$?
+        fi
+        if [[ $result -eq 0 ]]; then
+            end_ts=$(date +%s)
+            echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $QUIET -eq 1 ]]; then
+        timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    else
+        timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    fi
+    PID=$!
+    trap "kill -INT -$PID" INT
+    wait $PID
+    RESULT=$?
+    if [[ $RESULT -ne 0 ]]; then
+        echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+    fi
+    return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        hostport=(${1//:/ })
+        HOST=${hostport[0]}
+        PORT=${hostport[1]}
+        shift 1
+        ;;
+        --child)
+        CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        STRICT=1
+        shift 1
+        ;;
+        -h)
+        HOST="$2"
+        if [[ $HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        PORT="$2"
+        if [[ $PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        TIMEOUT="$2"
+        if [[ $TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        CLI=("$@")
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+# check to see if timeout is from busybox?
+# check to see if timeout is from busybox?
+TIMEOUT_PATH=$(realpath $(which timeout))
+if [[ $TIMEOUT_PATH =~ "busybox" ]]; then
+        ISBUSY=1
+        BUSYTIMEFLAG="-t"
+else
+        ISBUSY=0
+        BUSYTIMEFLAG=""
+fi
+
+if [[ $CHILD -gt 0 ]]; then
+    wait_for
+    RESULT=$?
+    exit $RESULT
+else
+    if [[ $TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        RESULT=$?
+    else
+        wait_for
+        RESULT=$?
+    fi
+fi
+
+if [[ $CLI != "" ]]; then
+    if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+        echoerr "$cmdname: strict mode, refusing to execute subprocess"
+        exit $RESULT
+    fi
+    exec "${CLI[@]}"
+else
+    exit $RESULT
+fi
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2228115
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,12 @@
+[tox]
+skipsdist=True
+envlist = py37
+
+[testenv]
+deps = pipenv
+commands =
+    pipenv install --dev --ignore-pipfile
+    pytest tests -vv --doctest-modules --cov=. --junitxml=tests-reports/junit.xml --cov-report=xml:tests-reports/coverage.xml --cov-report=html:tests-reports/html --cov-report=term
+    rm -rf {envdir}
+whitelist_externals =
+    rm
\ No newline at end of file


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..180a70e
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,13 @@
+[pytest]
+trio_mode = true
+env =
+    D:STOMP_SERVER_HOST=broker-rabbitmq
+    D:STOMP_SERVER_PORT=61613
+    D:STOMP_SERVER_USER=
+    D:STOMP_SERVER_PASSWORD=
+    D:STOMP_USE_SSL=False
+    D:STOMP_LISTENER_CLIENT_ID=your-app-name
+    D:STOMP_CORRELATION_ID_REQUIRED=True
+    D:STOMP_PROCESS_MSG_ON_BACKGROUND=False
+    D:STOMP_OUTGOING_HEARTBEAT=0
+    D:STOMP_INCOMING_HEARTBEAT=0
\ No newline at end of file
diff --git a/scripts/start-tests.sh b/scripts/start-tests.sh
new file mode 100755
index 0000000..37b4628
--- /dev/null
+++ b/scripts/start-tests.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+# https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin
+# -e  Exit immediately if a command exits with a non-zero status.
+# -x Print commands and their arguments as they are executed.
+set -e
+
+tox
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..101ab64
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,43 @@
+import os
+import uuid
+
+import pytest
+
+from django.conf import settings
+
+
+@pytest.fixture
+def mock_payload_size(mocker):
+    """Mock function to get size in bytes of payload queue"""
+    get_sizeof_value = 1
+    mocker.patch("sys.getsizeof").return_value = get_sizeof_value
+    return get_sizeof_value
+
+
+def pytest_configure():
+    settings.configure(
+        INSTALLED_APPS=["opentelemetry_instrumentation_django_stomp", "django_stomp"],
+        STOMP_SERVER_HOST=os.getenv("STOMP_SERVER_HOST"),
+        STOMP_SERVER_PORT=os.getenv("STOMP_SERVER_PORT"),
+        STOMP_SERVER_STANDBY_HOST=os.getenv("STOMP_SERVER_STANDBY_HOST"),
+        STOMP_SERVER_STANDBY_PORT=os.getenv("STOMP_SERVER_STANDBY_PORT"),
+        STOMP_SERVER_USER=os.getenv("STOMP_SERVER_USER"),
+        STOMP_SERVER_PASSWORD=os.getenv("STOMP_SERVER_PASSWORD"),
+        STOMP_USE_SSL=os.getenv("STOMP_USE_SSL"),
+        STOMP_LISTENER_CLIENT_ID=os.getenv("STOMP_LISTENER_CLIENT_ID"),
+        STOMP_CORRELATION_ID_REQUIRED=os.getenv("STOMP_CORRELATION_ID_REQUIRED"),
+        STOMP_PROCESS_MSG_ON_BACKGROUND=os.getenv("STOMP_PROCESS_MSG_ON_BACKGROUND"),
+        STOMP_OUTGOING_HEARTBEAT=os.getenv("STOMP_OUTGOING_HEARTBEAT"),
+        STOMP_INCOMING_HEARTBEAT=os.getenv("STOMP_INCOMING_HEARTBEAT"),
+        DATABASES={
+            "default": {
+                "ENGINE": os.getenv("DB_ENGINE", "django.db.backends.sqlite3"),
+                "NAME": os.getenv("DB_DATABASE", f"test_db-{uuid.uuid4()}"),
+                "USER": os.getenv("DB_USER"),
+                "HOST": os.getenv("DB_HOST"),
+                "PORT": os.getenv("DB_PORT"),
+                "PASSWORD": os.getenv("DB_PASSWORD"),
+                "TEST": {"NAME": os.getenv("DB_DATABASE", f"test_db-{uuid.uuid4()}")},
+            }
+        },
+    )
diff --git a/tests/instrumentors/__init__.py b/tests/instrumentors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/instrumentors/test_consumer_instrument.py b/tests/instrumentors/test_consumer_instrument.py
new file mode 100644
index 0000000..a48f161
--- /dev/null
+++ b/tests/instrumentors/test_consumer_instrument.py
@@ -0,0 +1,166 @@
+import json
+import logging
+
+from uuid import uuid4
+
+from django.conf import settings
+from django_stomp.builder import build_listener
+from django_stomp.builder import build_publisher
+from django_stomp.services.consumer import StompFrame
+from opentelemetry.semconv.trace import MessagingOperationValues
+from opentelemetry.semconv.trace import SpanAttributes
+
+from tests.support.helpers_tests import CustomFakeException
+from tests.support.helpers_tests import get_latest_message_from_destination_using_test_listener
+from tests.support.otel_helpers import TestBase
+
+
+class TestConsumerBase(TestBase):
+    hook_callback = None
+    consumer_id = None
+    queue_consumer_name = None
+    correlation_id = None
+    fake_payload_body = None
+    fake_payload_headers = None
+    span_host_attributes = None
+    listener = None
+    fake_frame = None
+
+    def setup_class(self):
+        self.consumer_hook = self.hook_callback
+        super().setup_class(self)
+
+    def setup_method(self):
+        # Arrange
+        self.consumer_id = f"some-destination-{uuid4()}"
+        self.queue_consumer_name = f"/queue/test-publisher-queue-{uuid4()}"
+        self.correlation_id = f"{uuid4()}"
+        self.fake_payload_body = {"fake": "body"}
+        self.fake_payload_headers = {
+            "tshoot-destination": self.queue_consumer_name,
+            "correlation-id": self.correlation_id,
+            "message-id": str(uuid4()),
+        }
+        self.span_host_attributes = {
+            SpanAttributes.NET_PEER_NAME: settings.STOMP_SERVER_HOST,
+            SpanAttributes.NET_PEER_PORT: settings.STOMP_SERVER_PORT,
+            SpanAttributes.MESSAGING_SYSTEM: "rabbitmq",
+        }
+        self.listener = build_listener(self.consumer_id, should_process_msg_on_background=True)
+        self.fake_frame = StompFrame(
+            cmd="FAKE_CMD", headers=self.fake_payload_headers, body=json.dumps(self.fake_payload_body)
+        )
+
+    def expected_span_attributes(self, mock_payload_size):
+        return {
+            SpanAttributes.MESSAGING_DESTINATION: self.queue_consumer_name,
+            SpanAttributes.MESSAGING_OPERATION: str(MessagingOperationValues.RECEIVE.value),
+            SpanAttributes.MESSAGING_CONVERSATION_ID: self.correlation_id,
+            SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: mock_payload_size,
+            **self.span_host_attributes,
+        }
+
+
+class TestConsumerInstrument(TestConsumerBase):
+    @staticmethod
+    def hook_callback(span, body, headers):
+        assert span.name == "CONSUMER"
+
+    def test_should_create_child_span_consumer_when_traceparent_header_exists_in_on_message_payload(
+        self, mock_payload_size
+    ):
+        # Arrange
+        publisher = build_publisher(f"test-publisher-{uuid4()}")
+        publisher.send(queue=self.queue_consumer_name, body=self.fake_payload_body, headers=self.fake_payload_headers)
+
+        received_message = get_latest_message_from_destination_using_test_listener(self.queue_consumer_name)
+        received_message_headers, received_message_body = received_message
+
+        fake_frame_with_trace_parent_header = StompFrame(
+            cmd="FAKE_CMD", headers=received_message_headers, body=json.dumps(received_message_body)
+        )
+
+        common_span_attributes = {
+            SpanAttributes.MESSAGING_DESTINATION: self.queue_consumer_name,
+            SpanAttributes.MESSAGING_CONVERSATION_ID: self.correlation_id,
+            SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: mock_payload_size,
+            **self.span_host_attributes,
+        }
+
+        expected_consumer_span_attributes = {
+            SpanAttributes.MESSAGING_OPERATION: str(MessagingOperationValues.RECEIVE.value),
+            **common_span_attributes,
+        }
+
+        # Act
+        self.listener.on_message(fake_frame_with_trace_parent_header)
+        self.listener.shutdown_worker_pool()
+
+        # Assert
+        finished_spans = self.get_finished_spans()
+        finished_consumer_span = finished_spans.by_name("CONSUMER")
+        finished_publisher_span = finished_spans.by_name("PUBLISHER")
+
+        assert len(finished_spans) == 2
+        assert dict(finished_consumer_span.attributes) == expected_consumer_span_attributes
+        assert dict(finished_publisher_span.attributes) == common_span_attributes
+        # finished_consumer_span is a child of finished_publisher_span
+        assert finished_consumer_span.parent.span_id == finished_publisher_span.context.span_id
+        assert finished_consumer_span.parent.trace_id == finished_publisher_span.context.trace_id
+        assert json.loads(received_message_body) == self.fake_payload_body
+
+    def test_should_create_consumer_span_in_on_message_function(self, mock_payload_size):
+        # Act
+        self.listener.on_message(self.fake_frame)
+        self.listener.shutdown_worker_pool()
+
+        # Assert
+        finished_consumer_span = self.get_finished_spans().by_name("CONSUMER")
+        assert dict(finished_consumer_span.attributes) == self.expected_span_attributes(mock_payload_size)
+
+    def test_should_create_ack_span_on_ack_listener_action(self, mocker):
+        # Arrange
+        mocker.patch(
+            "django_stomp.services.consumer.connect.StompConnection11.send_frame"
+        )  # to not raise StompConnectionException
+
+        # Act
+        self.listener._connection.ack(id="x", subscription=self.listener.__dict__["_subscription_id"])
+        ack_finished_span = self.get_finished_spans().by_name("ACK")
+
+        # Assert
+        assert dict(ack_finished_span.attributes) == self.span_host_attributes
+
+    def test_should_create_nack_span_on_nack_listener_action(self, mocker):
+        # Arrange
+        mocker.patch(
+            "django_stomp.services.consumer.connect.StompConnection11.send_frame"
+        )  # to not raise StompConnectionException
+
+        # Act
+        self.listener._connection.nack(id="x", subscription=self.listener.__dict__["_subscription_id"])
+        nack_finished_span = self.get_finished_spans().by_name("NACK")
+
+        # Assert
+        assert dict(nack_finished_span.attributes) == self.span_host_attributes
+
+
+class TestConsumerInstrumentHookRaises(TestConsumerBase):
+    @staticmethod
+    def hook_callback(span, body, headers):
+        assert span.name == "CONSUMER"
+        raise CustomFakeException("fake exception")
+
+    def test_should_log_exception_if_it_was_raised_inside_hook_function(self, mock_payload_size, caplog):
+        # Arrange
+        caplog.set_level(logging.WARNING)
+
+        # Act
+        self.listener.on_message(self.fake_frame)
+        self.listener.shutdown_worker_pool()
+
+        # Assert
+        finished_consumer_span = self.get_finished_spans().by_name("CONSUMER")
+        assert dict(finished_consumer_span.attributes) == self.expected_span_attributes(mock_payload_size)
+        assert len(caplog.records) == 1
+        assert caplog.records[0].message == "fake exception"
diff --git a/tests/instrumentors/test_publisher_instrument.py b/tests/instrumentors/test_publisher_instrument.py
new file mode 100644
index 0000000..a7ba5fb
--- /dev/null
+++ b/tests/instrumentors/test_publisher_instrument.py
@@ -0,0 +1,121 @@
+import json
+import logging
+
+from uuid import uuid4
+
+from django.conf import settings as django_settings
+from django_stomp.builder import build_publisher
+from opentelemetry import trace
+from opentelemetry.semconv.trace import SpanAttributes
+
+from opentelemetry_instrumentation_django_stomp import DjangoStompInstrumentor
+from tests.support.helpers_tests import CustomFakeException
+from tests.support.helpers_tests import get_latest_message_from_destination_using_test_listener
+from tests.support.otel_helpers import TestBase
+from tests.support.otel_helpers import get_traceparent_from_span
+
+
+class PublisherInstrumentBase(TestBase):
+    hook_callback = None
+    test_queue_name = None
+    correlation_id = None
+    fake_payload_body = None
+    publisher = None
+
+    def setup_class(self):
+        self.publisher_hook = self.hook_callback
+        super().setup_class(self)
+
+    def setup_method(self):
+        # Arrange
+        self.test_queue_name = f"/queue/test-publisher-queue-{uuid4()}"
+        self.correlation_id = f"{uuid4()}"
+        self.fake_payload_body = {"fake": "body"}
+        self.publisher = build_publisher(f"test-publisher-{uuid4()}")
+
+    def expected_span_attributes(self, mock_payload_size):
+        return {
+            SpanAttributes.MESSAGING_CONVERSATION_ID: self.correlation_id,
+            SpanAttributes.MESSAGING_DESTINATION: self.test_queue_name,
+            SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: mock_payload_size,
+            SpanAttributes.NET_PEER_NAME: django_settings.STOMP_SERVER_HOST,
+            SpanAttributes.NET_PEER_PORT: django_settings.STOMP_SERVER_PORT,
+            SpanAttributes.MESSAGING_SYSTEM: "rabbitmq",
+        }
+
+
+class TestPublisherInstrument(PublisherInstrumentBase):
+    @staticmethod
+    def hook_callback(span, body, headers):
+        assert headers.get("traceparent", None) == get_traceparent_from_span(span)
+
+    def test_should_match_traceparent_header_message_equals_to_traceparent_context_span(self, mock_payload_size):
+        # Act
+        self.publisher.send(
+            queue=self.test_queue_name, body={"fake": "body"}, headers={"correlation-id": self.correlation_id}
+        )
+
+        # Assert
+        # getting message without create a consumer span
+        received_message = get_latest_message_from_destination_using_test_listener(self.test_queue_name)
+        received_message_headers, received_message_body = received_message
+        finished_spans = self.get_finished_spans()
+        publisher_span = finished_spans.by_name("PUBLISHER")
+
+        assert json.loads(received_message_body) == self.fake_payload_body
+        assert received_message_headers.get("traceparent") == get_traceparent_from_span(publisher_span)
+        assert dict(publisher_span.attributes) == self.expected_span_attributes(mock_payload_size)
+
+
+class TestPublisherInstrumentHookRaises(PublisherInstrumentBase):
+    @staticmethod
+    def hook_callback(span, body, headers):
+        assert headers.get("traceparent", None) == get_traceparent_from_span(span)
+        raise CustomFakeException("fake exception")
+
+    def test_should_log_exception_if_it_was_raised_inside_hook_function(self, mock_payload_size, caplog):
+        # Arrange
+        caplog.set_level(logging.WARNING)
+
+        # Act
+        self.publisher.send(
+            queue=self.test_queue_name, body={"fake": "body"}, headers={"correlation-id": self.correlation_id}
+        )
+
+        # Assert
+        # getting message without create a consumer span
+        received_message = get_latest_message_from_destination_using_test_listener(self.test_queue_name)
+        received_message_headers, received_message_body = received_message
+        finished_spans = self.get_finished_spans()
+        publisher_span = finished_spans.by_name("PUBLISHER")
+
+        assert json.loads(received_message_body) == self.fake_payload_body
+        assert received_message_headers.get("traceparent") == get_traceparent_from_span(publisher_span)
+        assert dict(publisher_span.attributes) == self.expected_span_attributes(mock_payload_size)
+        assert len(caplog.records) == 1
+        assert caplog.records[0].message == "fake exception"
+
+
+class TestPublisherInstrumentSupress(PublisherInstrumentBase):
+    def setup_class(self):
+        result = self.create_tracer_provider()
+        self.tracer_provider, self.memory_exporter = result
+        trace.set_tracer_provider(self.tracer_provider)
+
+    def test_should_not_generate_span_if_suppress_key_is_in_context(self, settings):
+        # Act
+        settings.OTEL_PYTHON_DJANGO_STOMP_INSTRUMENT = False
+        DjangoStompInstrumentor().instrument()
+        publisher = build_publisher(f"test-publisher-{uuid4()}")
+        publisher.send(
+            queue=self.test_queue_name, body={"fake": "body"}, headers={"correlation-id": self.correlation_id}
+        )
+
+        # Assert
+        # getting message without create a consumer span
+        received_message = get_latest_message_from_destination_using_test_listener(self.test_queue_name)
+        received_message_headers, received_message_body = received_message
+        finished_spans = self.get_finished_spans()
+        assert len(finished_spans) == 0
+        assert json.loads(received_message_body) == self.fake_payload_body
+        assert received_message_headers.get("traceparent") is None
diff --git a/tests/resources/custom-rabbitmq-conf/enabled_plugins b/tests/resources/custom-rabbitmq-conf/enabled_plugins
new file mode 100644
index 0000000..162dc3b
--- /dev/null
+++ b/tests/resources/custom-rabbitmq-conf/enabled_plugins
@@ -0,0 +1,1 @@
+[rabbitmq_federation_management,rabbitmq_management,rabbitmq_mqtt,rabbitmq_stomp,rabbitmq_shovel,rabbitmq_shovel_management].
\ No newline at end of file
diff --git a/tests/resources/custom-rabbitmq-conf/rabbitmq.conf b/tests/resources/custom-rabbitmq-conf/rabbitmq.conf
new file mode 100644
index 0000000..ece904c
--- /dev/null
+++ b/tests/resources/custom-rabbitmq-conf/rabbitmq.conf
@@ -0,0 +1,14 @@
+loopback_users.guest = false
+listeners.tcp.default = 5672
+
+management.tcp.port = 15672
+
+# Reduce retention policy for faster publishing of stats
+management.sample_retention_policies.global.minute = 1
+management.sample_retention_policies.global.hour = 60
+management.sample_retention_policies.global.day = 1200
+
+management.sample_retention_policies.basic.minute = 1
+management.sample_retention_policies.basic.hour = 60
+
+management.sample_retention_policies.detailed.10 = 1
\ No newline at end of file
diff --git a/tests/support/__init__.py b/tests/support/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/support/helpers_tests.py b/tests/support/helpers_tests.py
new file mode 100644
index 0000000..6ae880f
--- /dev/null
+++ b/tests/support/helpers_tests.py
@@ -0,0 +1,24 @@
+import typing
+
+from django_stomp.builder import build_listener
+
+
+class CustomFakeException(Exception):
+    pass
+
+
+def get_latest_message_from_destination_using_test_listener(destination: str) -> typing.Dict:
+    """
+    Gets the latest message using the test listener utility. It does not ack the message
+    on the destination queue.
+    [!]: It makes a test hang forever if a message never arrives at the destination.
+    """
+    evaluation_consumer = build_listener(destination, is_testing=True)
+    test_listener = evaluation_consumer._test_listener
+    evaluation_consumer.start(wait_forever=False)
+
+    # may wait forever if the msg never arrives
+    test_listener.wait_for_message()
+    received_message = test_listener.get_latest_message()
+
+    return received_message
diff --git a/tests/support/otel_helpers.py b/tests/support/otel_helpers.py
new file mode 100644
index 0000000..ae5fe21
--- /dev/null
+++ b/tests/support/otel_helpers.py
@@ -0,0 +1,89 @@
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace import export
+from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
+from opentelemetry.trace import format_span_id
+from opentelemetry.trace import format_trace_id
+from opentelemetry.util._once import Once
+
+from opentelemetry_instrumentation_django_stomp import DjangoStompInstrumentor
+
+
+def get_traceparent_from_span(span):
+    """Helper function to get traceparent for propagator, used to create header on publish message"""
+    trace_id_formatted = format_trace_id(span.context.trace_id)
+    span_id_formatted = format_span_id(span.context.span_id)
+    trace_flags = span.context.trace_flags
+    return f"00-{trace_id_formatted}-{span_id_formatted}-{trace_flags:02x}"
+
+
+class FinishedTestSpans(list):
+    """Helper class to find finished spans in tests to make assertions"""
+
+    def __init__(self, test, spans):
+        super().__init__(spans)
+        self.test = test
+
+    def by_name(self, name):
+        for span in self:
+            if span.name == name:
+                return span
+        self.test.fail(f"Did not find span with name {name}")
+        return None
+
+    def by_attr(self, key, value):
+        for span in self:
+            if span.attributes.get(key) == value:
+                return span
+        self.test.fail(f"Did not find span with attrs {key}={value}")
+        return None
+
+
+class TestBase:
+    """Base test class with setup_method and teardown_method for telemetry parameters"""
+
+    tracer_provider = None
+    memory_exporter = None
+    consumer_hook = None
+    publisher_hook = None
+
+    def setup_class(self):
+        result = self.create_tracer_provider()
+        self.tracer_provider, self.memory_exporter = result
+        trace.set_tracer_provider(self.tracer_provider)
+        DjangoStompInstrumentor().instrument(publisher_hook=self.publisher_hook, consumer_hook=self.consumer_hook)
+
+    def teardown_method(self):
+        self.memory_exporter.clear()
+
+    def teardown_class(self):
+        self.reset_trace_globals()
+        DjangoStompInstrumentor().uninstrument()
+
+    def get_finished_spans(self):
+        return FinishedTestSpans(self, self.memory_exporter.get_finished_spans())
+
+    @staticmethod
+    def reset_trace_globals() -> None:
+        """WARNING: only use this for tests."""
+        trace._TRACER_PROVIDER_SET_ONCE = Once()
+        trace._TRACER_PROVIDER = None
+        trace._PROXY_TRACER_PROVIDER = trace.ProxyTracerProvider()
+
+    @staticmethod
+    def create_tracer_provider(**kwargs):
+        """Helper to create a configured tracer provider.
+        Creates and configures a `TracerProvider` with a
+        `SimpleSpanProcessor` and a `InMemorySpanExporter`.
+        All the parameters passed are forwarded to the TracerProvider
+        constructor.
+        Returns:
+            A list with the tracer provider in the first element and the
+            in-memory span exporter in the second.
+        """
+        tracer_provider = TracerProvider(**kwargs)
+        memory_exporter = InMemorySpanExporter()
+        span_processor = export.SimpleSpanProcessor(memory_exporter)
+        tracer_provider.add_span_processor(span_processor)
+
+        return tracer_provider, memory_exporter
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/utils/test_django_stomp_getter.py b/tests/utils/test_django_stomp_getter.py
new file mode 100644
index 0000000..df118da
--- /dev/null
+++ b/tests/utils/test_django_stomp_getter.py
@@ -0,0 +1,36 @@
+import pytest
+
+from opentelemetry.propagators.textmap import CarrierT
+
+from opentelemetry_instrumentation_django_stomp.utils.django_stomp_getter import DjangoStompGetter
+
+
+class TestDjangoStompGetter:
+    @pytest.fixture(autouse=True)
+    def setup(self):
+        # Arrange
+        self.fake_object_key = "fake_key"
+        self.fake_object_value = "fake_value"
+        self.fake_carrier: CarrierT = {self.fake_object_key: self.fake_object_value}
+        self.django_stomp_getter = DjangoStompGetter()
+
+    def test_should_return_value_in_list_if_value_exists_in_carrier(self):
+        # Act
+        getter_result = self.django_stomp_getter.get(self.fake_carrier, self.fake_object_key)
+
+        # Assert
+        assert type(getter_result) == list
+        assert len(getter_result) == len(self.fake_carrier)
+        assert getter_result[0] == self.fake_object_value
+
+    def test_should_return_none_if_value_not_exists_in_carrier(self):
+        # Act
+        getter_result = self.django_stomp_getter.get(self.fake_carrier, "other_key")
+        # Assert
+        assert getter_result is None
+
+    def test_should_return_empty_array_in_keys_method_call(self):
+        # Act
+        getter_result = self.django_stomp_getter.keys(self.fake_carrier)
+        # Assert
+        assert getter_result == []
diff --git a/tests/utils/test_span.py b/tests/utils/test_span.py
new file mode 100644
index 0000000..bab3847
--- /dev/null
+++ b/tests/utils/test_span.py
@@ -0,0 +1,93 @@
+from unittest import mock
+
+import pytest
+
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import SpanKind
+
+from opentelemetry_instrumentation_django_stomp.utils.span import get_span
+
+
+class TestSpan:
+    @pytest.mark.parametrize(
+        "test_params",
+        [
+            {
+                "fake_broker_host": "fake_host",
+                "fake_broker_port": "fake_port",
+                "fake_broker_system": "fake_system",
+                "operation": "fake_operation",
+                "destination": "fake_destination",
+                "headers": {"correlation-id": "fake_value"},
+                "body": {"fake_key": "fake_value"},
+                "span_kind": SpanKind.INTERNAL,
+                "span_name": "fake_span_name",
+            },
+            {
+                "fake_broker_host": "fake_host",
+                "fake_broker_port": "fake_port",
+                "operation": "fake_operation",
+                "destination": "fake_destination",
+                "headers": {"correlation-id": "fake_value"},
+                "body": {"fake_key": "fake_value"},
+                "span_kind": SpanKind.INTERNAL,
+                "span_name": "fake_span_name",
+            },
+            {
+                "fake_broker_host": "fake_host",
+                "fake_broker_port": "fake_port",
+                "fake_broker_system": None,
+                "operation": "fake_operation",
+                "destination": "fake_destination",
+                "headers": {"correlation-id": "fake_value"},
+                "body": {"fake_key": "fake_value"},
+                "span_kind": SpanKind.CONSUMER,
+                "span_name": "fake_span_name",
+            },
+        ],
+    )
+    def test_should_enrich_span_with_host_data_based_in_env_configurations_and_parameters(
+        self, test_params, settings, mock_payload_size
+    ):
+        # Arrange
+        fake_broker_host = test_params["fake_broker_host"]
+        fake_broker_port = test_params["fake_broker_port"]
+        fake_broker_system = test_params.get("fake_broker_system", False)
+
+        settings.STOMP_SERVER_HOST = fake_broker_host
+        settings.STOMP_SERVER_PORT = fake_broker_port
+        if fake_broker_system:
+            settings.STOMP_SYSTEM = fake_broker_system
+
+        expected_span_attributes_host = {
+            SpanAttributes.NET_PEER_NAME: fake_broker_host,
+            SpanAttributes.NET_PEER_PORT: fake_broker_port,
+            SpanAttributes.MESSAGING_SYSTEM: fake_broker_system or "rabbitmq",
+        }
+
+        expected_span_attributes_message = {
+            SpanAttributes.MESSAGING_DESTINATION: test_params["destination"],
+            SpanAttributes.MESSAGING_OPERATION: test_params["operation"],
+            SpanAttributes.MESSAGING_CONVERSATION_ID: test_params["headers"].get("correlation-id"),
+            SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: mock_payload_size,
+        }
+        mocked_span = mock.MagicMock()
+
+        mocked_tracer = mock.MagicMock()
+        mocked_tracer.start_span.return_value = mocked_span
+
+        # Act
+        get_span(
+            tracer=mocked_tracer,
+            operation=test_params["operation"],
+            destination=test_params["destination"],
+            headers=test_params["headers"],
+            body=test_params["body"],
+            span_name=test_params["span_name"],
+            span_kind=test_params["span_kind"],
+        )
+
+        # Assert
+        assert mocked_span.is_recording.call_count == 1
+        mocked_span.set_attributes.assert_any_call(expected_span_attributes_host)
+        mocked_span.set_attributes.assert_any_call(expected_span_attributes_message)
diff --git a/tests/utils/test_traced_thread_pool_executor.py b/tests/utils/test_traced_thread_pool_executor.py
new file mode 100644
index 0000000..d4352ec
--- /dev/null
+++ b/tests/utils/test_traced_thread_pool_executor.py
@@ -0,0 +1,50 @@
+import logging
+
+from opentelemetry import trace
+
+from opentelemetry_instrumentation_django_stomp.utils.traced_thread_pool_executor import TracedThreadPoolExecutor
+from tests.support.otel_helpers import TestBase
+
+_logger = logging.getLogger(__name__)
+
+
+class TestTracedThreadPoolExecutor(TestBase):
+    @staticmethod
+    def dummy_function(*args, **kwargs):
+        """Fake function to run in submit on TracedThreadPoolExecutor"""
+        tracer = trace.get_tracer_provider().get_tracer(__name__)
+        with tracer.start_as_current_span("dummy_function", end_on_exit=True):
+            _logger.info("dummy_function executed")
+
+    def test_traced_pool_executor_propagate_span_to_thread(self):
+        # Arrange
+        traced_pool_executor = TracedThreadPoolExecutor(
+            tracer=trace.get_tracer(__name__), thread_name_prefix="test_pool"
+        )
+
+        # Act
+        tracer = trace.get_tracer_provider().get_tracer(__name__)
+        with tracer.start_as_current_span("TRACED_THREAD_POOL_EXECUTOR", end_on_exit=True):
+            traced_pool_executor.submit(self.dummy_function, {"simple": "parameter"})
+
+        # Assert
+        finished_spans = self.get_finished_spans()
+        finished_thread_span = finished_spans.by_name("TRACED_THREAD_POOL_EXECUTOR")
+        finished_function_span = finished_spans.by_name("dummy_function")
+        assert finished_function_span.parent.trace_id == finished_thread_span.context.trace_id
+        assert finished_function_span.parent.span_id == finished_thread_span.context.span_id
+        assert len(finished_spans) == 2
+
+    def test_traced_pool_executor_without_propagate_span_to_thread(self):
+        # Arrange
+        traced_pool_executor = TracedThreadPoolExecutor(
+            tracer=trace.get_tracer(__name__), thread_name_prefix="test_pool"
+        )
+
+        # Act
+        traced_pool_executor.submit(self.dummy_function, {"simple": "parameter"})
+
+        # Assert
+        finished_spans = self.get_finished_spans()
+        assert len(finished_spans) == 1
+        assert finished_spans.by_name("dummy_function")