diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07d2073..3ca3bd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,7 @@ repos: 'flake8-annotations', # Enforces type annotation ] args: ['--enable-extensions=G'] + files: r'^django_guid' - repo: https://github.com/asottile/pyupgrade rev: v3.2.2 hooks: @@ -46,3 +47,4 @@ repos: rev: v0.991 hooks: - id: mypy + files: r'^django_guid' diff --git a/demoproj/asgi.py b/demoproj/asgi.py deleted file mode 100644 index 4ed0d94..0000000 --- a/demoproj/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for demoproj_asgi project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') - -application = get_asgi_application() diff --git a/demoproj/services/async_services.py b/demoproj/services/async_services.py deleted file mode 100644 index efc41cd..0000000 --- a/demoproj/services/async_services.py +++ /dev/null @@ -1,16 +0,0 @@ -import asyncio -import logging - -logger = logging.getLogger(__name__) - - -async def useless_function() -> bool: - """ - Useless function to demonstrate a function log message. - :return: True - """ - logger.info('Going to sleep for a sec') - await asyncio.sleep(1) - - logger.warning('Warning, I am awake!') - return True diff --git a/demoproj/services/sync_services.py b/demoproj/services/sync_services.py deleted file mode 100644 index a781ede..0000000 --- a/demoproj/services/sync_services.py +++ /dev/null @@ -1,12 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - - -def useless_function() -> bool: - """ - Useless function to demonstrate a function log message. - :return: True - """ - logger.warning('Some warning in a function') - return True diff --git a/demoproj/urls.py b/demoproj/urls.py deleted file mode 100644 index 05bdd00..0000000 --- a/demoproj/urls.py +++ /dev/null @@ -1,28 +0,0 @@ -"""demo URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.urls import path - -from demoproj.views.sync_views import index_view, no_guid, rest_view -from demoproj.views.async_views import index_view as asgi_index_view -from demoproj.views.async_views import django_guid_api_usage - -urlpatterns = [ - path('', index_view, name='index'), - path('api', rest_view, name='drf'), - path('no-guid', no_guid, name='no_guid'), - path('asgi', asgi_index_view, name='asgi_index'), - path('api-usage', django_guid_api_usage, name='django_guid_api_usage'), -] diff --git a/demoproj/views/async_views.py b/demoproj/views/async_views.py deleted file mode 100644 index d927189..0000000 --- a/demoproj/views/async_views.py +++ /dev/null @@ -1,38 +0,0 @@ -import logging -import asyncio -from django.http import JsonResponse - -from demoproj.services.async_services import useless_function -from django_guid import get_guid, set_guid, clear_guid -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from django.http import HttpRequest - -logger = logging.getLogger(__name__) - - -async def index_view(request: 'HttpRequest') -> JsonResponse: - """ - Example view that logs a log and calls a function that logs a log. - - :param request: HttpRequest - :return: JsonResponse - """ - logger.info('This log message should have a GUID') - task_one = asyncio.create_task(useless_function()) - task_two = asyncio.create_task(useless_function()) - results = await asyncio.gather(task_one, task_two) - return JsonResponse({'detail': f'It worked! Useless function response is {results}'}) - - -async def django_guid_api_usage(request: 'HttpRequest') -> JsonResponse: - """ - Uses each API function - """ - logger.info('Current GUID: %s', get_guid()) - set_guid('another guid') - logger.info('Current GUID: %s', get_guid()) - clear_guid() - logger.info('Current GUID: %s', get_guid()) - return JsonResponse({'detail': ':)'}) diff --git a/demoproj/views/sync_views.py b/demoproj/views/sync_views.py deleted file mode 100644 index c7b79d7..0000000 --- a/demoproj/views/sync_views.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging - -from django.http import JsonResponse -from rest_framework.decorators import api_view -from rest_framework.response import Response - -from demoproj.services.sync_services import useless_function -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from django.http import HttpRequest - from rest_framework.request import Request - -logger = logging.getLogger(__name__) - - -def index_view(request: 'HttpRequest') -> JsonResponse: - """ - Example view that logs a log and calls a function that logs a log. - - :param request: HttpRequest - :return: JsonResponse - """ - logger.info('This log message should have a GUID') - useless_response = useless_function() - return JsonResponse({'detail': f'It worked! Useless function response is {useless_response}'}) - - -def no_guid(request: 'HttpRequest') -> JsonResponse: - """ - Example view with a URL in the IGNORE_URLs list - no GUID will be in these logs - """ - logger.info('This log message should NOT have a GUID - the URL is in IGNORE_URLS') - useless_response = useless_function() - return JsonResponse({'detail': f'It worked also! Useless function response is {useless_response}'}) - - -@api_view(('GET',)) -def rest_view(request: 'Request') -> Response: - """ - Example DRF view that logs a log and calls a function that logs a log. - - :param request: Request - :return: Response - """ - logger.info('This is a DRF view log, and should have a GUID.') - useless_response = useless_function() - return Response(data={'detail': f'It worked! Useless function response is {useless_response}'}) diff --git a/demoproj/wsgi.py b/demoproj/wsgi.py deleted file mode 100644 index 7dade72..0000000 --- a/demoproj/wsgi.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') - -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. - -application = get_wsgi_application() diff --git a/examples/async-views/README.md b/examples/async-views/README.md new file mode 100644 index 0000000..3c22d89 --- /dev/null +++ b/examples/async-views/README.md @@ -0,0 +1,10 @@ +# Async views + +Example code for setting up correlation IDs for async views. + +To demo, run + +- `python manage.py runserver 8080`, or +- `uvicorn src.asgi:application` + +and go to http://127.0.0.1:8080. diff --git a/examples/async-views/manage.py b/examples/async-views/manage.py new file mode 100644 index 0000000..8d1e06c --- /dev/null +++ b/examples/async-views/manage.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import os +import sys + + +def main() -> None: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/demoproj/__init__.py b/examples/async-views/src/__init__.py similarity index 100% rename from demoproj/__init__.py rename to examples/async-views/src/__init__.py diff --git a/examples/async-views/src/asgi.py b/examples/async-views/src/asgi.py new file mode 100644 index 0000000..78f6ac0 --- /dev/null +++ b/examples/async-views/src/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + +application = get_asgi_application() diff --git a/examples/async-views/src/services.py b/examples/async-views/src/services.py new file mode 100644 index 0000000..a9dd3a9 --- /dev/null +++ b/examples/async-views/src/services.py @@ -0,0 +1,12 @@ +import asyncio +import logging +from random import randint + +logger = logging.getLogger(__name__) + + +async def some_async_function() -> int: + logger.info('Doing i/o bound work') + await asyncio.sleep(1) + logger.warning('Finished i/o bound work') + return randint(0, 10) diff --git a/examples/async-views/src/settings.py b/examples/async-views/src/settings.py new file mode 100644 index 0000000..d8a28c0 --- /dev/null +++ b/examples/async-views/src/settings.py @@ -0,0 +1,116 @@ +import os +from typing import List + +# fmt: off + +# No values are required +DJANGO_GUID = { + 'GUID_HEADER_NAME': 'X-Request-ID', + 'VALIDATE_GUID': False, +} + +# Log filter setup is required +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID + }, + 'formatters': { + # Example of a basic log format without correlation ID filter + 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, + + # Example format with correlation ID + 'correlation_id': { + 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s' # <-- Add the %(correlation_id)s + }, + }, + 'handlers': { + 'correlation_id_handler': { + 'class': 'logging.StreamHandler', + 'formatter': 'correlation_id', + 'filters': ['correlation_id'], # <-- Add this filter here + }, + }, + 'loggers': { + 'django': { + 'handlers': ['correlation_id_handler'], # <-- Add the handler in your loggers + 'level': 'INFO' + }, + 'src': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG' + }, + 'django_guid': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG', + 'propagate': True, + }, + } +} + +# fmt: on + + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +SECRET_KEY = 'secret' + +DEBUG = True + +ALLOWED_HOSTS: List[str] = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'django_guid', +] + +MIDDLEWARE = [ + 'django_guid.middleware.guid_middleware', # <-- Add middleware at the top of your middlewares + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'src.urls' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': ':memory:', + } +} +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = '/static/' diff --git a/examples/async-views/src/urls.py b/examples/async-views/src/urls.py new file mode 100644 index 0000000..daf5f21 --- /dev/null +++ b/examples/async-views/src/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from . import views + +urlpatterns = [path('', views.index_view, name='index')] diff --git a/examples/async-views/src/views.py b/examples/async-views/src/views.py new file mode 100644 index 0000000..44dc8c4 --- /dev/null +++ b/examples/async-views/src/views.py @@ -0,0 +1,20 @@ +import asyncio +import logging +from typing import TYPE_CHECKING + +from django.http import JsonResponse + +from .services import some_async_function + +if TYPE_CHECKING: + from django.http import HttpRequest + +logger = logging.getLogger(__name__) + + +async def index_view(request: 'HttpRequest') -> JsonResponse: + logger.info('Fetching counts asynchronously') + counts = await asyncio.gather( + asyncio.create_task(some_async_function()), asyncio.create_task(some_async_function()) + ) + return JsonResponse({'sum': sum(counts)}) diff --git a/examples/celery/README.md b/examples/celery/README.md new file mode 100644 index 0000000..8940a33 --- /dev/null +++ b/examples/celery/README.md @@ -0,0 +1,31 @@ +# Celery integration + +Example code for setting up correlation IDs and Celery IDs. + +To demo, run + +- `docker compose up -d`, or run redis locally on port 6378 (or change the example broker URL in settings.py) +- Run a celery worker with `celery -A src worker` +- Queue a task by running: + ``` + python manage.py shell< None: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/demoproj/services/__init__.py b/examples/celery/src/__init__.py similarity index 100% rename from demoproj/services/__init__.py rename to examples/celery/src/__init__.py diff --git a/demoproj/celery.py b/examples/celery/src/celery.py similarity index 82% rename from demoproj/celery.py rename to examples/celery/src/celery.py index c282124..724e632 100644 --- a/demoproj/celery.py +++ b/examples/celery/src/celery.py @@ -9,7 +9,7 @@ # Windows configuration to make celery run ok on Windows os.environ.setdefault('FORKED_BY_MULTIPROCESSING', '1') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') app = Celery('django_guid') app.config_from_object('django.conf:settings', namespace='CELERY') @@ -21,7 +21,7 @@ def debug_task() -> None: """ This is just an example task. """ - logger.info('Debug task 1') + logger.info('debug task 1') second_debug_task.delay() second_debug_task.delay() @@ -31,7 +31,7 @@ def second_debug_task() -> None: """ This is just an example task. """ - logger.info('Debug task 2') + logger.info('debug task 2') third_debug_task.delay() fourth_debug_task.delay() @@ -41,7 +41,7 @@ def third_debug_task() -> None: """ This is just an example task. """ - logger.info('Debug task 3') + logger.info('debug task 3') fourth_debug_task.delay() fourth_debug_task.delay() @@ -51,4 +51,4 @@ def fourth_debug_task() -> None: """ This is just an example task. """ - logger.info('Debug task 4') + logger.info('debug task 4') diff --git a/demoproj/settings.py b/examples/celery/src/settings.py similarity index 65% rename from demoproj/settings.py rename to examples/celery/src/settings.py index 2aba16c..76c214a 100644 --- a/demoproj/settings.py +++ b/examples/celery/src/settings.py @@ -1,21 +1,82 @@ -""" -Django settings for demoproj project. +import os +from typing import List -Generated by 'django-admin startproject' using Django 3.0.1. +from celery.schedules import crontab -For more information on this file, see -https://docs.djangoproject.com/en/3.0/topics/settings/ +from django_guid.integrations import CeleryIntegration -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.0/ref/settings/ -""" +# fmt: off -import os -from typing import List +# No values are required +DJANGO_GUID = { + 'INTEGRATIONS': [ + CeleryIntegration( + use_django_logging=True, + log_parent=True, + uuid_length=10 + ) + ] +} -from celery.schedules import crontab +CELERY_BROKER_URL = 'redis://:@localhost:6378' +CELERY_RESULT_BACKEND = CELERY_BROKER_URL +CELERY_BEAT_SCHEDULE = { + 'test': { + 'task': 'src.celery.debug_task', + 'schedule': crontab(minute='*/1'), + }, +} + +# Log filter setup is required +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID + 'celery_tracing': {'()': 'django_guid.integrations.celery.log_filters.CeleryTracing'}, # <-- Add celery IDs + }, + 'formatters': { + # Example of a basic log format without correlation ID filter + 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, + + # Example format with correlation ID + 'correlation_id': { + 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s' # <-- Add the %(correlation_id)s + }, + + # Format with correlation ID plus a celery process' parent ID and a unique current ID that will + # become the parent ID of any child processes that are created (most likely you won't want to + # display these values in your formatter, but include them just as a filter) + 'celery_depth_format': { + 'format': '%(levelname)s [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s - %(message)s' + }, + }, + 'handlers': { + 'correlation_id_handler': { + 'class': 'logging.StreamHandler', + 'formatter': 'correlation_id', + 'filters': ['correlation_id'], # <-- Add this filter here + }, + 'celery_depth_handler': { + 'class': 'logging.StreamHandler', + 'formatter': 'celery_depth_format', + 'filters': ['correlation_id', 'celery_tracing'], + }, + }, + 'loggers': { + 'django': { + 'handlers': ['correlation_id_handler'], # <-- Add the handler in your loggers + 'level': 'INFO' + }, + 'src': { + 'handlers': ['celery_depth_handler'], + 'level': 'DEBUG' + }, + } +} + +# fmt: on -from django_guid.integrations import CeleryIntegration, SentryIntegration BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -49,7 +110,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'demoproj.urls' +ROOT_URLCONF = 'src.urls' DATABASES = { 'default': { @@ -58,19 +119,6 @@ 'NAME': ':memory:', } } - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -STATIC_URL = '/static/' - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -86,94 +134,9 @@ }, }, ] - -# fmt: off - -# OBS: No setting in Django GUID is required. These are example settings. -DJANGO_GUID = { - 'GUID_HEADER_NAME': 'Correlation-ID', - 'VALIDATE_GUID': True, - 'INTEGRATIONS': [ - CeleryIntegration( - use_django_logging=True, - log_parent=True, - uuid_length=10 - ), - SentryIntegration() - ], - 'IGNORE_URLS': ['no-guid'], -} - -# Set up logging for the project -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID - 'celery_tracing': {'()': 'django_guid.integrations.celery.log_filters.CeleryTracing'}, # <-- Add celery IDs - }, - 'formatters': { - # Basic log format without django-guid filters - 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, - - # Format with correlation ID output to the console - 'correlation_id_format': {'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s'}, - - # Format with correlation ID plus a celery process' parent ID and a unique current ID that will - # become the parent ID of any child processes that are created (most likely you won't want to - # display these values in your formatter, but include them just as a filter) - 'celery_depth_format': { - 'format': '%(levelname)s [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s - %(message)s' - }, - }, - 'handlers': { - 'correlation_id_handler': { - 'class': 'logging.StreamHandler', - 'formatter': 'correlation_id_format', - # Here we include the filters on the handler - this means our IDs are included in the logger extra data - # and *can* be displayed in our log message if specified in the formatter - but it will be - # included in the logs whether shown in the message or not. - 'filters': ['correlation_id', 'celery_tracing'], - }, - 'celery_depth_handler': { - 'class': 'logging.StreamHandler', - 'formatter': 'celery_depth_format', - 'filters': ['correlation_id', 'celery_tracing'], - }, - }, - 'loggers': { - 'django': { - 'handlers': ['correlation_id_handler'], - 'level': 'INFO' - }, - 'demoproj': { - 'handlers': ['correlation_id_handler'], - 'level': 'DEBUG' - }, - 'django_guid': { - 'handlers': ['correlation_id_handler'], - 'level': 'DEBUG', - 'propagate': True, - }, - 'django_guid.celery': { - 'handlers': ['celery_depth_handler'], - 'level': 'DEBUG', - 'propagate': False, - }, - 'celery': { - 'handlers': ['celery_depth_handler'], - 'level': 'INFO', - }, - } -} - -# fmt: on - -CELERY_BROKER_URL = 'redis://:@localhost:6378' -CELERY_RESULT_BACKEND = CELERY_BROKER_URL -CELERY_BEAT_SCHEDULE = { - 'test': { - 'task': 'demoproj.celery.debug_task', - 'schedule': crontab(minute='*/1'), - }, -} +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = '/static/' diff --git a/examples/celery/src/urls.py b/examples/celery/src/urls.py new file mode 100644 index 0000000..637600f --- /dev/null +++ b/examples/celery/src/urls.py @@ -0,0 +1 @@ +urlpatterns = [] diff --git a/examples/ignore-url/README.md b/examples/ignore-url/README.md new file mode 100644 index 0000000..e8492b9 --- /dev/null +++ b/examples/ignore-url/README.md @@ -0,0 +1,10 @@ +# Sync views + +Example code for setting up correlation IDs for sync views. + +To demo, run + +- `python manage.py runserver 8080`, or +- `wsgi src.wsgi` + +and go to http://127.0.0.1:8080/ignored diff --git a/examples/ignore-url/manage.py b/examples/ignore-url/manage.py new file mode 100644 index 0000000..8d1e06c --- /dev/null +++ b/examples/ignore-url/manage.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import os +import sys + + +def main() -> None: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/demoproj/views/__init__.py b/examples/ignore-url/src/__init__.py similarity index 100% rename from demoproj/views/__init__.py rename to examples/ignore-url/src/__init__.py diff --git a/examples/ignore-url/src/services.py b/examples/ignore-url/src/services.py new file mode 100644 index 0000000..e0024bc --- /dev/null +++ b/examples/ignore-url/src/services.py @@ -0,0 +1,7 @@ +import logging + +logger = logging.getLogger(__name__) + + +def some_sync_function() -> None: + logger.info('Doing some work') diff --git a/examples/ignore-url/src/settings.py b/examples/ignore-url/src/settings.py new file mode 100644 index 0000000..ca10f79 --- /dev/null +++ b/examples/ignore-url/src/settings.py @@ -0,0 +1,115 @@ +import os +from typing import List + +# fmt: off + +# No values are required +DJANGO_GUID = { + 'IGNORE_URLS': ['ignored'], +} + +# Log filter setup is required +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID + }, + 'formatters': { + # Example of a basic log format without correlation ID filter + 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, + + # Example format with correlation ID + 'correlation_id': { + 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s' # <-- Add the %(correlation_id)s + }, + }, + 'handlers': { + 'correlation_id_handler': { + 'class': 'logging.StreamHandler', + 'formatter': 'correlation_id', + 'filters': ['correlation_id'], # <-- Add this filter here + }, + }, + 'loggers': { + 'django': { + 'handlers': ['correlation_id_handler'], # <-- Add the handler in your loggers + 'level': 'INFO' + }, + 'src': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG' + }, + 'django_guid': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG', + 'propagate': True, + }, + } +} + +# fmt: on + + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +SECRET_KEY = 'secret' + +DEBUG = True + +ALLOWED_HOSTS: List[str] = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'django_guid', +] + +MIDDLEWARE = [ + 'django_guid.middleware.guid_middleware', # <-- Add middleware at the top of your middlewares + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'src.urls' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': ':memory:', + } +} +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = '/static/' diff --git a/examples/ignore-url/src/urls.py b/examples/ignore-url/src/urls.py new file mode 100644 index 0000000..820af90 --- /dev/null +++ b/examples/ignore-url/src/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('ignored', views.ignored_view, name='ignored'), +] diff --git a/examples/ignore-url/src/views.py b/examples/ignore-url/src/views.py new file mode 100644 index 0000000..1bfb2c4 --- /dev/null +++ b/examples/ignore-url/src/views.py @@ -0,0 +1,18 @@ +import logging +from typing import TYPE_CHECKING + +from django.http import JsonResponse + +from django_guid.context import guid +from .services import some_sync_function + +if TYPE_CHECKING: + from django.http import HttpRequest + +logger = logging.getLogger(__name__) + + +def ignored_view(request: 'HttpRequest') -> JsonResponse: + logger.info('This log message should NOT have a correlation ID - the URL is in IGNORE_URLS') + some_sync_function() + return JsonResponse({'correlationId': f'{guid.get()}'}) diff --git a/examples/ignore-url/src/wsgi.py b/examples/ignore-url/src/wsgi.py new file mode 100644 index 0000000..9a0b048 --- /dev/null +++ b/examples/ignore-url/src/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + +application = get_wsgi_application() diff --git a/examples/sentry-and-celery/README.md b/examples/sentry-and-celery/README.md new file mode 100644 index 0000000..3013721 --- /dev/null +++ b/examples/sentry-and-celery/README.md @@ -0,0 +1,17 @@ +# Sentry and Celery-integration + +To enable the Sentry integration in a Celery context, set `sentry_integration=True` +when instantiating the `CeleryIntegration` class. + +Beyond that, take a look at the `celery` example for how to set up the celery integration in more detail. + +```python +from django_guid.integrations import SentryIntegration, CeleryIntegration + +DJANGO_GUID = { + 'INTEGRATIONS': [ + SentryIntegration(), + CeleryIntegration(sentry_integration=True) + ] +} +``` diff --git a/examples/sentry/README.md b/examples/sentry/README.md new file mode 100644 index 0000000..c95336f --- /dev/null +++ b/examples/sentry/README.md @@ -0,0 +1,16 @@ +# Sentry integration + +Shows how to add the Sentry integration. There's not an easy +way to demonstrate how this works, since you'd be dependent +on Sentry to demonstrate. As a consequence, this just includes +the configuration itself: + +```python +from django_guid.integrations import SentryIntegration + +DJANGO_GUID = { + 'INTEGRATIONS': [ + SentryIntegration() + ] +} +``` diff --git a/examples/sync-views/README.md b/examples/sync-views/README.md new file mode 100644 index 0000000..85c6056 --- /dev/null +++ b/examples/sync-views/README.md @@ -0,0 +1,10 @@ +# Sync views + +Example code for setting up correlation IDs for sync views. + +To demo, run + +- `python manage.py runserver 8080`, or +- `wsgi src.wsgi` + +and go to http://127.0.0.1:8080 diff --git a/examples/sync-views/manage.py b/examples/sync-views/manage.py new file mode 100644 index 0000000..8d1e06c --- /dev/null +++ b/examples/sync-views/manage.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import os +import sys + + +def main() -> None: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/examples/sync-views/src/__init__.py b/examples/sync-views/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/sync-views/src/services.py b/examples/sync-views/src/services.py new file mode 100644 index 0000000..e0024bc --- /dev/null +++ b/examples/sync-views/src/services.py @@ -0,0 +1,7 @@ +import logging + +logger = logging.getLogger(__name__) + + +def some_sync_function() -> None: + logger.info('Doing some work') diff --git a/examples/sync-views/src/settings.py b/examples/sync-views/src/settings.py new file mode 100644 index 0000000..891f7d0 --- /dev/null +++ b/examples/sync-views/src/settings.py @@ -0,0 +1,116 @@ +import os +from typing import List + +# fmt: off + +# No values are required +DJANGO_GUID = { + 'GUID_HEADER_NAME': 'X-Correlation-ID', + 'VALIDATE_GUID': True, +} + +# Log filter setup is required +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID + }, + 'formatters': { + # Example of a basic log format without correlation ID filter + 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, + + # Example format with correlation ID + 'correlation_id': { + 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s' # <-- Add the %(correlation_id)s + }, + }, + 'handlers': { + 'correlation_id_handler': { + 'class': 'logging.StreamHandler', + 'formatter': 'correlation_id', + 'filters': ['correlation_id'], # <-- Add this filter here + }, + }, + 'loggers': { + 'django': { + 'handlers': ['correlation_id_handler'], # <-- Add the handler in your loggers + 'level': 'INFO' + }, + 'src': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG' + }, + 'django_guid': { + 'handlers': ['correlation_id_handler'], + 'level': 'DEBUG', + 'propagate': True, + }, + } +} + +# fmt: on + + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +SECRET_KEY = 'secret' + +DEBUG = True + +ALLOWED_HOSTS: List[str] = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'django_guid', +] + +MIDDLEWARE = [ + 'django_guid.middleware.guid_middleware', # <-- Add middleware at the top of your middlewares + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'src.urls' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': ':memory:', + } +} +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = '/static/' diff --git a/examples/sync-views/src/urls.py b/examples/sync-views/src/urls.py new file mode 100644 index 0000000..29dccdd --- /dev/null +++ b/examples/sync-views/src/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.index_view), +] diff --git a/examples/sync-views/src/views.py b/examples/sync-views/src/views.py new file mode 100644 index 0000000..d3e5d42 --- /dev/null +++ b/examples/sync-views/src/views.py @@ -0,0 +1,18 @@ +import logging +from typing import TYPE_CHECKING + +from django.http import JsonResponse + +from django_guid.context import guid +from .services import some_sync_function + +if TYPE_CHECKING: + from django.http import HttpRequest + +logger = logging.getLogger(__name__) + + +def index_view(request: 'HttpRequest') -> JsonResponse: + logger.info('This log message should have a correlation ID') + some_sync_function() + return JsonResponse({'correlationId': f'{guid.get()}'}) diff --git a/examples/sync-views/src/wsgi.py b/examples/sync-views/src/wsgi.py new file mode 100644 index 0000000..9a0b048 --- /dev/null +++ b/examples/sync-views/src/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') + +application = get_wsgi_application()