pax_global_header 0000666 0000000 0000000 00000000064 14546363661 0014530 g ustar 00root root 0000000 0000000 52 comment=7928d05f236c757b726c743e96d517d6015e2170
python-django-guid-3.4.1/ 0000775 0000000 0000000 00000000000 14546363661 0015244 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/.github/ 0000775 0000000 0000000 00000000000 14546363661 0016604 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14546363661 0020767 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000000430 14546363661 0023456 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior
**Full stack trace**
Don't leave anything out
python-django-guid-3.4.1/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000000162 14546363661 0024513 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: suggestion
assignees: ''
---
python-django-guid-3.4.1/.github/ISSUE_TEMPLATE/other--questions--remarks---.md 0000664 0000000 0000000 00000000161 14546363661 0026463 0 ustar 00root root 0000000 0000000 ---
name: Other (Questions, remarks..)
about: Questions, remarks..
title: ''
labels: question
assignees: ''
---
python-django-guid-3.4.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14546363661 0020641 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/.github/workflows/publish_to_pypi.yml 0000664 0000000 0000000 00000001003 14546363661 0024567 0 ustar 00root root 0000000 0000000 name: Publish django-guid to PyPI 📦
on:
release:
types: [ published ]
jobs:
build-and-publish:
name: Build and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: python -m pip install --upgrade pip poetry
- name: Build and publish
run: |
poetry config pypi-token.pypi ${{ secrets.pypi_password }}
poetry publish --build --no-interaction
python-django-guid-3.4.1/.github/workflows/test.yml 0000664 0000000 0000000 00000004406 14546363661 0022347 0 ustar 00root root 0000000 0000000 name: test
on:
pull_request:
push:
branches:
- master
jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11.0"
- run: pip install pre-commit
- uses: actions/cache@v3
id: pre-commit-cache
with:
path: ~/.cache/pre-commit
key: key-0
- run: pre-commit run --all-files
env:
SKIP: rst
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.8.18", "3.9.18", "3.10.13", "3.11.7", "3.12.1"]
django-version: [ "3.2", "4.2", "5.0"]
exclude:
# Django v3 does not support Python >3.10
- django-version: 3.2
python-version: 3.11.7
- django-version: 3.2
python-version: 3.12.1
# Django v5 drops Python <3.10 support
- django-version: 5.0
python-version: 3.8.18
- django-version: 5.0
python-version: 3.9.18
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python-version }}"
- uses: actions/cache@v3
id: poetry-cache
with:
path: ~/.local
key: key-4
- uses: snok/install-poetry@v1
with:
virtualenvs-create: false
version: 1.2.2
- uses: actions/cache@v3
id: cache-venv
with:
path: .venv
key: ${{ hashFiles('**/poetry.lock') }}-5
- run: |
pip install virtualenv
virtualenv .venv
source .venv/bin/activate
pip install pip setuptools wheel -U
poetry install --no-interaction --no-root
if: steps.cache-venv.outputs.cache-hit != 'true'
- run: |
source .venv/bin/activate
pip install "Django==${{ matrix.django-version }}"
- name: Run tests
run: |
source .venv/bin/activate
coverage run -m pytest tests
coverage xml
coverage report
- uses: codecov/codecov-action@v2
with:
file: ./coverage.xml
fail_ci_if_error: true
if: matrix.python-version == '3.11'
python-django-guid-3.4.1/.gitignore 0000664 0000000 0000000 00000000234 14546363661 0017233 0 ustar 00root root 0000000 0000000 *.pyc
.idea/*
env/
venv/
.venv/
build/
dist/
*.egg-info/
notes
.pytest_cache
.coverage
htmlcov/
# Sphinx documentation
docs/_build/
# celery
celerybeat-*
python-django-guid-3.4.1/.pre-commit-config.yaml 0000664 0000000 0000000 00000003247 14546363661 0021533 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/ambv/black
rev: 23.12.1
hooks:
- id: black
args: ['--quiet']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-case-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-ast
- id: check-json
- id: check-merge-conflict
- id: detect-private-key
- id: double-quote-string-fixer
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies: [
'flake8-bugbear', # Looks for likely bugs and design problems
'flake8-comprehensions', # Looks for unnecessary generator functions that can be converted to list comprehensions
'flake8-deprecated', # Looks for method deprecations
'flake8-use-fstring', # Enforces use of f-strings over .format and %s
'flake8-print', # Checks for print statements
'flake8-docstrings', # Verifies that all functions/methods have docstrings
'flake8-type-checking', # Looks for misconfigured type annotations
'flake8-annotations', # Enforces type annotation
]
args: ['--enable-extensions=G']
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
files: 'django_guid/.*'
- id: isort
files: 'tests/.*'
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
[
django
]
python-django-guid-3.4.1/CHANGELOG.rst 0000664 0000000 0000000 00000013530 14546363661 0017267 0 ustar 00root root 0000000 0000000 Changelog
=========
`3.2.1`_ - 13.12.2021
---------------------
Changes can be seen here_ going forward.
`3.2.0`_ - 04.12.2020
---------------------
**Features**
* Added a new setting, ``sentry_integration`` to the Celery integration, which sets ``transaction_id`` for Celery workers.
`3.1.0`_ - 18.11.2020
---------------------
**Features**
* Added a new setting, ``UUID_LENGTH``, which lets you crop the UUIDs generated for log filters.
* Added a new integration for tracing with Celery_.
`3.0.1`_ - 12.11.2020
---------------------
**Bugfix**
* Importing an integration before a ``SECRET_KEY`` was set would cause a circular import.
`3.0.0`_ - 28.10.2020 - Full Django3.1+(ASGI/async) support!
------------------------------------------------------------
Brings full async/ASGI (as well as the old WSGI) support to Django GUID using ContextVars instead of thread locals.
**Breaking changes**
This version requires ``Django>=3.1.1``. For previous versions of Django,
please use ``django-guid<3.0.0`` (Such as ``django-guid==2.2.0``).
If you've already implemented ``django-guid`` in your project and are currently upgrading to ``Django>=3.1.1``, please
see the `upgrading docs`_.
`2.2.0`_ - 04.11.2020
---------------------
**Features**
* ``IGNORE_URLS`` setting which disables the middleware on a list of URLs.
**Other**
* Added docs for the new setting
`2.1.0`_ - 03.11.2020
---------------------
**Features**
* Integration module, which enables the users of ``django_guid`` to extend functionality.
* Added a integration for Sentry, tagging the Sentry issue with the GUID used for the request.
**Other**
* Added docs for integrations
`2.0.0`_ - 02.03.2020
---------------------
**This version contains backwards incompatible changes. Read the entire changelog before upgrading**
**Deprecated**
* ``SKIP_CLEANUP``: After a request is finished, the Correlation ID is cleaned up using the ``request_finished`` Django signal.
**Incompatible changes**
* ``django_guid`` must be in ``INSTALLED_APPS`` due to usage of signals.
**Improvements**
* Restructured README and docs.
`1.1.1`_ - 12.02.2020
---------------------
**Improvements**
* Fixed ``EXPOSE_HEADER`` documentation issue. New release has to be pushed to fix PyPi docs.
`1.1.0`_ - 10.02.2020
---------------------
**Features**
* Added a ``EXPOSE_HEADER`` setting, which will add the ``Access-Control-Expose-Headers`` with the ``RETURN_HEADER`` as value to the response. This is to allow the JavaScript Fetch API to access the header with the GUID
`1.0.1`_ - 08.02.2020
---------------------
**Bugfix**
* Fixed validation of incoming GUID
**Improvements**
* Changed the ``middleware.py`` logger name to ``django_guid``
* Added a WARNING-logger for when validation fails
* Improved README
**Other**
* Added ``CONTRIBUTORS.rst``
`1.0.0`_ - 14.01.2020
---------------------
**Features**
* Added a ``RETURN_HEADER`` setting, which will return the GUID as a header with the same name
**Improvements**
* Added a Django Rest Framework test and added DRF to the ``demoproj``
* Improved tests to also check for headers in the response
* Added tests for the new setting
* Added examples to ``README.rst`` and docs, to show how the log messages get formatted
* Added an API page to the docs
* Fixed the ``readthedocs`` menu bug
`0.3.1`_ - 13.01.2020
---------------------
**Improvements**
* Changed logging from f'strings' to %strings
* Pre-commit hooks added, including ``black`` and ``flake8``
* Added ``CONTRIBUTING.rst``
* Added github actions to push to ``PyPi`` with github tags
`0.3.0`_ - 10.01.2020
---------------------
**Features**
* Added a SKIP_CLEANUP setting
**Improvements**
* Improved all tests to be more verbose
* Improved the README with more information and a list of all the available settings
`0.2.3`_ - 09.01.2020
---------------------
**Improvements**
* Added tests written in `pytests`, 100% codecov
* Added Django2.2 and Django3 to github workflow as two steps
* Improved logging
`0.2.2`_ - 21.12.2019
---------------------
**Improvements**
* Removed the mandatory DJANGO_GUID settings in settings.py. Added an example project to demonstrate how to set the project up
`0.2.1`_ - 21.12.2019
---------------------
**Improvements**
* Workflow added, better docstrings, easier to read flow
`0.2.0`_ - 21.12.2019
---------------------
**Features**
* Header name and header GUID validation can be specified through Django settings
20.10.2019
----------
* Initial release
.. _0.2.0: https://github.com/snok/django-guid/compare/0.1.2...0.2.0
.. _0.2.1: https://github.com/snok/django-guid/compare/0.2.0...0.2.1
.. _0.2.2: https://github.com/snok/django-guid/compare/0.2.1...0.2.2
.. _0.2.3: https://github.com/snok/django-guid/compare/0.2.2...0.2.3
.. _0.3.0: https://github.com/snok/django-guid/compare/0.2.3...0.3.0
.. _0.3.1: https://github.com/snok/django-guid/compare/0.3.0...0.3.1
.. _1.0.0: https://github.com/snok/django-guid/compare/0.3.0...1.0.0
.. _1.0.1: https://github.com/snok/django-guid/compare/1.0.0...1.0.1
.. _1.1.0: https://github.com/snok/django-guid/compare/1.0.1...1.1.0
.. _1.1.1: https://github.com/snok/django-guid/compare/1.1.0...1.1.1
.. _2.0.0: https://github.com/snok/django-guid/compare/1.1.1...2.0.0
.. _2.1.0: https://github.com/snok/django-guid/compare/2.0.0...2.1.0
.. _2.2.0: https://github.com/snok/django-guid/compare/2.1.0...2.2.0
.. _3.0.0: https://github.com/snok/django-guid/compare/2.2.0...3.0.0
.. _upgrading docs: https://django-guid.readthedocs.io/en/latest/upgrading.html
.. _3.0.1: https://github.com/snok/django-guid/compare/3.0.0...3.0.1
.. _3.1.0: https://github.com/snok/django-guid/compare/3.0.1...3.1.0
.. _3.2.0: https://github.com/snok/django-guid/compare/3.1.0...3.2.0
.. _3.2.1: https://github.com/snok/django-guid/compare/3.2.0...3.2.1
.. _Celery: https://docs.celeryproject.org/en/stable/
.. _here: https://github.com/snok/django-guid/releases
python-django-guid-3.4.1/LICENSE 0000664 0000000 0000000 00000002123 14546363661 0016247 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2023 Jonas Krüger Svensson & Sondre Lillebø Gundersen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
python-django-guid-3.4.1/README.rst 0000664 0000000 0000000 00000021321 14546363661 0016732 0 ustar 00root root 0000000 0000000 .. raw:: html
Django GUID
Now with ASGI support!
.. raw:: html
--------------
Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request.
In other words, all logs connected to a request now has a unique ID attached to it, making debugging simple.
Which version of Django GUID you should use depends on your Django version and whether you run ``ASGI`` or ``WSGI`` servers.
To determine which Django-GUID version you should use, please see the table below.
+---------------------+--------------------------+
| Django version | Django-GUID version |
+=====================+==========================+
| 3.1.1 or above | 3.x.x - ASGI and WSGI |
+---------------------+--------------------------+
| 3.0.0 - 3.1.0 | 2.x.x - Only WSGI |
+---------------------+--------------------------+
| 2.2.x | 2.x.x - Only WSGI |
+---------------------+--------------------------+
Django GUID >= 3.0.0 uses ``ContextVar`` to store and access the GUID. Previous versions stored the GUID to an object,
making it accessible by using the ID of the current thread. (Version 2 of Django GUID is supported until Django2.2 LTS is over.)
--------------
**Resources**:
* Free software: MIT License
* Documentation: https://django-guid.readthedocs.io
* Homepage: https://github.com/snok/django-guid
--------------
**Examples**
Log output with a GUID:
.. code-block:: flex
INFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID.
WARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function
INFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID.
INFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID.
WARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function
WARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function
Log output without a GUID:
.. code-block:: flex
INFO ... project.views This is a DRF view log, and should have a GUID.
WARNING ... project.services.file Some warning in a function
INFO ... project.views This is a DRF view log, and should have a GUID.
INFO ... project.views This is a DRF view log, and should have a GUID.
WARNING ... project.services.file Some warning in a function
WARNING ... project.services.file Some warning in a function
See the `documentation `_ for more examples.
************
Installation
************
Install using pip:
.. code-block:: bash
pip install django-guid
********
Settings
********
Package settings are added in your ``settings.py``:
.. code-block:: python
DJANGO_GUID = {
'GUID_HEADER_NAME': 'Correlation-ID',
'VALIDATE_GUID': True,
'RETURN_HEADER': True,
'EXPOSE_HEADER': True,
'INTEGRATIONS': [],
'IGNORE_URLS': [],
'UUID_LENGTH': 32,
}
**Optional Parameters**
* :code:`GUID_HEADER_NAME`
The name of the GUID to look for in a header in an incoming request. Remember that it's case insensitive.
Default: Correlation-ID
* :code:`VALIDATE_GUID`
Whether the :code:`GUID_HEADER_NAME` should be validated or not.
If the GUID sent to through the header is not a valid GUID (:code:`uuid.uuid4`).
Default: True
* :code:`RETURN_HEADER`
Whether to return the GUID (Correlation-ID) as a header in the response or not.
It will have the same name as the :code:`GUID_HEADER_NAME` setting.
Default: True
* :code:`EXPOSE_HEADER`
Whether to return :code:`Access-Control-Expose-Headers` for the GUID header if
:code:`RETURN_HEADER` is :code:`True`, has no effect if :code:`RETURN_HEADER` is :code:`False`.
This is allows the JavaScript Fetch API to access the header when CORS is enabled.
Default: True
* :code:`INTEGRATIONS`
Whether to enable any custom or available integrations with :code:`django_guid`.
As an example, using :code:`SentryIntegration()` as an integration would set Sentry's :code:`transaction_id` to
match the GUID used by the middleware.
Default: []
* :code:`IGNORE_URLS`
URL endpoints where the middleware will be disabled. You can put your health check endpoints here.
Default: []
* :code:`UUID_LENGTH`
Lets you optionally trim the length of the package generated UUIDs.
Default: 32
*************
Configuration
*************
Once settings have set up, add the following to your projects' ``settings.py``:
1. Installed Apps
=================
Add :code:`django_guid` to your :code:`INSTALLED_APPS`:
.. code-block:: python
INSTALLED_APPS = [
...
'django_guid',
]
2. Middleware
=============
Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``:
.. code-block:: python
MIDDLEWARE = [
'django_guid.middleware.guid_middleware',
...
]
It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID.
3. Logging Configuration
========================
Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration:
.. code-block:: python
LOGGING = {
...
'filters': {
'correlation_id': {
'()': 'django_guid.log_filters.CorrelationId'
}
}
}
Put that filter in your handler:
.. code-block:: python
LOGGING = {
...
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'medium',
'filters': ['correlation_id'],
}
}
}
And make sure to add the new ``correlation_id`` filter to one or all of your formatters:
.. code-block:: python
LOGGING = {
...
'formatters': {
'medium': {
'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s'
}
}
}
If these settings were confusing, please have a look in the demo projects'
`settings.py `_ file for a complete example.
4. Django GUID Logger (Optional)
================================
If you wish to see the Django GUID middleware outputs, you may configure a logger for the module.
Simply add django_guid to your loggers in the project, like in the example below:
.. code-block:: python
LOGGING = {
...
'loggers': {
'django_guid': {
'handlers': ['console', 'logstash'],
'level': 'WARNING',
'propagate': False,
}
}
}
This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs.
python-django-guid-3.4.1/demoproj/ 0000775 0000000 0000000 00000000000 14546363661 0017063 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/__init__.py 0000664 0000000 0000000 00000000000 14546363661 0021162 0 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/asgi.py 0000664 0000000 0000000 00000000616 14546363661 0020363 0 ustar 00root root 0000000 0000000 """
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()
python-django-guid-3.4.1/demoproj/celery.py 0000664 0000000 0000000 00000002126 14546363661 0020721 0 ustar 00root root 0000000 0000000 import logging
import os
from celery import Celery
logger = logging.getLogger(__name__)
if os.name == 'nt':
# Windows configuration to make celery run ok on Windows
os.environ.setdefault('FORKED_BY_MULTIPROCESSING', '1')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings')
app = Celery('django_guid')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
@app.task()
def debug_task() -> None:
"""
This is just an example task.
"""
logger.info('Debug task 1')
second_debug_task.delay()
second_debug_task.delay()
@app.task()
def second_debug_task() -> None:
"""
This is just an example task.
"""
logger.info('Debug task 2')
third_debug_task.delay()
fourth_debug_task.delay()
@app.task()
def third_debug_task() -> None:
"""
This is just an example task.
"""
logger.info('Debug task 3')
fourth_debug_task.delay()
fourth_debug_task.delay()
@app.task()
def fourth_debug_task() -> None:
"""
This is just an example task.
"""
logger.info('Debug task 4')
python-django-guid-3.4.1/demoproj/services/ 0000775 0000000 0000000 00000000000 14546363661 0020706 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/services/__init__.py 0000664 0000000 0000000 00000000000 14546363661 0023005 0 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/services/async_services.py 0000664 0000000 0000000 00000000515 14546363661 0024301 0 ustar 00root root 0000000 0000000 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
python-django-guid-3.4.1/demoproj/services/sync_services.py 0000664 0000000 0000000 00000000366 14546363661 0024144 0 ustar 00root root 0000000 0000000 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
python-django-guid-3.4.1/demoproj/settings.py 0000664 0000000 0000000 00000012363 14546363661 0021302 0 ustar 00root root 0000000 0000000 """
Django settings for demoproj project.
Generated by 'django-admin startproject' using Django 3.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
from typing import List
from celery.schedules import crontab
from django_guid.integrations import CeleryIntegration, SentryIntegration
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 = 'demoproj.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'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',
'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',
],
},
},
]
# 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'),
},
}
python-django-guid-3.4.1/demoproj/urls.py 0000664 0000000 0000000 00000002154 14546363661 0020424 0 ustar 00root root 0000000 0000000 """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'),
]
python-django-guid-3.4.1/demoproj/views/ 0000775 0000000 0000000 00000000000 14546363661 0020220 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/views/__init__.py 0000664 0000000 0000000 00000000000 14546363661 0022317 0 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/demoproj/views/async_views.py 0000664 0000000 0000000 00000002241 14546363661 0023123 0 ustar 00root root 0000000 0000000 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': ':)'})
python-django-guid-3.4.1/demoproj/views/sync_views.py 0000664 0000000 0000000 00000003070 14546363661 0022763 0 ustar 00root root 0000000 0000000 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}'})
python-django-guid-3.4.1/demoproj/wsgi.py 0000664 0000000 0000000 00000000530 14546363661 0020404 0 ustar 00root root 0000000 0000000 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()
python-django-guid-3.4.1/django_guid/ 0000775 0000000 0000000 00000000000 14546363661 0017516 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/django_guid/__init__.py 0000664 0000000 0000000 00000000372 14546363661 0021631 0 ustar 00root root 0000000 0000000 import django
from django_guid.api import clear_guid, get_guid, set_guid # noqa F401
__version__ = '3.4.0'
if django.VERSION < (3, 2):
default_app_config = 'django_guid.apps.DjangoGuidConfig'
__all__ = ['clear_guid', 'get_guid', 'set_guid']
python-django-guid-3.4.1/django_guid/api.py 0000664 0000000 0000000 00000001262 14546363661 0020642 0 ustar 00root root 0000000 0000000 import logging
from django_guid.context import guid
logger = logging.getLogger('django_guid')
def get_guid() -> str:
"""
Fetches the GUID of the current request
"""
return guid.get()
def set_guid(new_guid: str) -> str:
"""
Assigns a GUID to the current request
"""
old_guid = guid.get()
if old_guid:
logger.info('Changing the guid ContextVar from %s to %s', old_guid, new_guid)
guid.set(new_guid)
return new_guid
def clear_guid() -> None:
"""
Clears the GUID of the current request
"""
old_guid = guid.get()
if old_guid:
logger.info('Clearing %s from the guid ContextVar', old_guid)
guid.set(None)
python-django-guid-3.4.1/django_guid/apps.py 0000664 0000000 0000000 00000000533 14546363661 0021034 0 ustar 00root root 0000000 0000000 from django.apps import AppConfig
class DjangoGuidConfig(AppConfig):
name = 'django_guid'
def ready(self) -> None:
"""
In order to avoid circular imports we import signals here.
"""
from django_guid import signals # noqa F401
from django_guid.config import settings
settings.validate()
python-django-guid-3.4.1/django_guid/config.py 0000664 0000000 0000000 00000011723 14546363661 0021341 0 ustar 00root root 0000000 0000000 # flake8: noqa: D102
from collections import defaultdict
from typing import Dict, List, Union
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.inspect import func_accepts_kwargs
from django_guid.integrations.celery.config import CeleryIntegrationSettings
class IntegrationSettings:
def __init__(self, integration_settings: dict) -> None:
self.settings = integration_settings
@property
def celery(self) -> CeleryIntegrationSettings:
return CeleryIntegrationSettings(self.settings['CeleryIntegration'])
def validate(self) -> None:
if 'CeleryIntegration' in self.settings:
self.celery.validate()
class Settings:
def __init__(self) -> None:
if hasattr(django_settings, 'DJANGO_GUID'):
self.settings = django_settings.DJANGO_GUID
else:
self.settings = {}
@property
def guid_header_name(self) -> str:
return self.settings.get('GUID_HEADER_NAME', 'Correlation-ID')
@property
def return_header(self) -> bool:
return self.settings.get('RETURN_HEADER', True)
@property
def expose_header(self) -> bool:
return self.settings.get('EXPOSE_HEADER', True)
@property
def ignore_urls(self) -> List[str]:
return list({url.strip('/') for url in self.settings.get('IGNORE_URLS', [])})
@property
def validate_guid(self) -> bool:
return self.settings.get('VALIDATE_GUID', True)
@property
def integrations(self) -> Union[list, tuple]:
return self.settings.get('INTEGRATIONS', [])
@property
def integration_settings(self) -> IntegrationSettings:
return IntegrationSettings({integration.identifier: integration for integration in self.integrations})
@property
def uuid_length(self) -> int:
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):
raise ImproperlyConfigured('VALIDATE_GUID must be a boolean')
if not isinstance(self.guid_header_name, str):
raise ImproperlyConfigured('GUID_HEADER_NAME must be a string') # Note: Case insensitive
if not isinstance(self.return_header, bool):
raise ImproperlyConfigured('RETURN_HEADER must be a boolean')
if not isinstance(self.expose_header, bool):
raise ImproperlyConfigured('EXPOSE_HEADER must be a boolean')
if not isinstance(self.integrations, (list, tuple)):
raise ImproperlyConfigured('INTEGRATIONS must be an array')
if not isinstance(self.settings.get('IGNORE_URLS', []), (list, tuple)):
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 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()
def _validate_and_setup_integrations(self) -> None:
"""
Validate the INTEGRATIONS settings and verify each integration
"""
self.integration_settings.validate()
for integration in self.integrations:
# Make sure all integration methods are callable
for method, name in [
(integration.setup, 'setup'),
(integration.run, 'run'),
(integration.cleanup, 'cleanup'),
]:
# Make sure the methods are callable
if not callable(method):
raise ImproperlyConfigured(
f'Integration method `{name}` needs to be made callable for `{integration.identifier}`'
)
# Make sure the method takes kwargs
if name in ['run', 'cleanup'] and not func_accepts_kwargs(method):
raise ImproperlyConfigured(
f'Integration method `{name}` must '
f'accept keyword arguments (**kwargs) for `{integration.identifier}`'
)
# Run validate method
integration.setup()
settings = Settings()
python-django-guid-3.4.1/django_guid/context.py 0000664 0000000 0000000 00000000130 14546363661 0021546 0 ustar 00root root 0000000 0000000 from contextvars import ContextVar
guid: ContextVar = ContextVar('guid', default=None)
python-django-guid-3.4.1/django_guid/integrations/ 0000775 0000000 0000000 00000000000 14546363661 0022224 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/django_guid/integrations/__init__.py 0000664 0000000 0000000 00000000367 14546363661 0024343 0 ustar 00root root 0000000 0000000 from django_guid.integrations.base import Integration
from django_guid.integrations.celery import CeleryIntegration
from django_guid.integrations.sentry import SentryIntegration
__all__ = ['Integration', 'CeleryIntegration', 'SentryIntegration']
python-django-guid-3.4.1/django_guid/integrations/base.py 0000664 0000000 0000000 00000001624 14546363661 0023513 0 ustar 00root root 0000000 0000000 from typing import Any, Optional
from django.core.exceptions import ImproperlyConfigured
class Integration:
"""
Integration base class.
"""
identifier: Optional[str] = None # The name of your integration
def __init__(self) -> None:
if self.identifier is None:
raise ImproperlyConfigured('`identifier` cannot be None')
def setup(self) -> None:
"""
Holds validation and setup logic to be run when Django starts.
"""
pass
def run(self, guid: str, **kwargs: Any) -> None:
"""
Code here is executed in the middleware, before the view is called.
"""
raise ImproperlyConfigured(f'The integration `{self.identifier}` is missing a `run` method')
def cleanup(self, **kwargs: Any) -> None:
"""
Code here is executed in the middleware, after the view is called.
"""
pass
python-django-guid-3.4.1/django_guid/integrations/celery/ 0000775 0000000 0000000 00000000000 14546363661 0023507 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/django_guid/integrations/celery/__init__.py 0000664 0000000 0000000 00000000153 14546363661 0025617 0 ustar 00root root 0000000 0000000 from django_guid.integrations.celery.integration import CeleryIntegration
__all__ = ['CeleryIntegration']
python-django-guid-3.4.1/django_guid/integrations/celery/config.py 0000664 0000000 0000000 00000003076 14546363661 0025334 0 ustar 00root root 0000000 0000000 # flake8: noqa: D102
from typing import TYPE_CHECKING
from django.core.exceptions import ImproperlyConfigured
from django_guid.integrations import SentryIntegration
if TYPE_CHECKING:
from django_guid.integrations.celery import CeleryIntegration # pragma: no cover
class CeleryIntegrationSettings:
def __init__(self, instance: 'CeleryIntegration') -> None:
self.instance = instance
self.validate()
@property
def use_django_logging(self) -> bool:
return self.instance.use_django_logging
@property
def log_parent(self) -> bool:
return self.instance.log_parent
@property
def uuid_length(self) -> int:
return self.instance.uuid_length
@property
def sentry_integration(self) -> bool:
return self.instance.sentry_integration
def validate(self) -> None:
if not isinstance(self.use_django_logging, bool):
raise ImproperlyConfigured('The CeleryIntegration use_django_logging setting must be a boolean.')
if not isinstance(self.log_parent, bool):
raise ImproperlyConfigured('The CeleryIntegration log_parent setting must be a boolean.')
if type(self.uuid_length) is not int or not 1 <= self.uuid_length <= 32:
raise ImproperlyConfigured('The CeleryIntegration uuid_length setting must be an integer.')
if not isinstance(self.sentry_integration, bool):
raise ImproperlyConfigured('The CeleryIntegration sentry_integration setting must be a boolean.')
if self.sentry_integration:
SentryIntegration().setup()
python-django-guid-3.4.1/django_guid/integrations/celery/context.py 0000664 0000000 0000000 00000000262 14546363661 0025545 0 ustar 00root root 0000000 0000000 from contextvars import ContextVar
celery_parent: ContextVar = ContextVar('celery_parent', default=None)
celery_current: ContextVar = ContextVar('celery_current', default=None)
python-django-guid-3.4.1/django_guid/integrations/celery/integration.py 0000664 0000000 0000000 00000004107 14546363661 0026406 0 ustar 00root root 0000000 0000000 import logging
from typing import Any
from django_guid.integrations import Integration
logger = logging.getLogger('django_guid')
class CeleryIntegration(Integration):
"""
Passes correlation IDs from parent processes to child processes in a Celery context.
This means a correlation ID can be transferred from a request to a worker, or from a worker to another worker.
For workers executing scheduled tasks, a correlation ID is generated for each new task.
"""
identifier = 'CeleryIntegration'
def __init__(
self,
use_django_logging: bool = False,
log_parent: bool = False,
uuid_length: int = 32,
sentry_integration: bool = False,
) -> None:
"""
:param use_django_logging: If true, configures Celery to use the logging settings defined in settings.py
:param log_parent: If true, traces the origin of a task. Should be True if you wish to use the CeleryTracing log filter.
:param uuid_length: Optionally lets you set the length of the celery IDs generated for the log filter
"""
super().__init__()
self.log_parent = log_parent
self.use_django_logging = use_django_logging
self.uuid_length = uuid_length
self.sentry_integration = sentry_integration
def setup(self) -> None:
"""
Loads Celery signals.
"""
# Import pre-configured Celery signals that will pass on the correlation ID to a celery worker
# or will generate a correlation ID when a worker starts a scheduled task
from django_guid.integrations.celery.signals import before_task_publish, task_postrun, task_prerun # noqa
if self.use_django_logging:
# Import pre-configured Celery signals that makes Celery adopt the settings.py log config
from django_guid.integrations.celery.logging import config_loggers # noqa
def run(self, guid: str, **kwargs: Any) -> None:
"""
Does nothing, as all we need for Celery tracing is to register signals during setup.
"""
pass # pragma: no cover
python-django-guid-3.4.1/django_guid/integrations/celery/log_filters.py 0000664 0000000 0000000 00000001621 14546363661 0026372 0 ustar 00root root 0000000 0000000 from logging import Filter
from typing import TYPE_CHECKING
from django_guid.integrations.celery.context import celery_current, celery_parent
if TYPE_CHECKING:
from logging import LogRecord
class CeleryTracing(Filter):
# noinspection PyTypeHints
def filter(self, record: 'LogRecord') -> bool:
"""
Sets two record attributes: celery parent and celery current.
Celery origin is the tracing ID of the process that spawned the current
process, and celery current is the current process' tracing ID.
In other words, if a worker sent a task to be executed by the worker pool,
that celery worker's `current` tracing ID would become the next worker's `origin` tracing ID.
"""
record.celery_parent_id: str = celery_parent.get() # type: ignore
record.celery_current_id: str = celery_current.get() # type: ignore
return True
python-django-guid-3.4.1/django_guid/integrations/celery/logging.py 0000664 0000000 0000000 00000000562 14546363661 0025512 0 ustar 00root root 0000000 0000000 from typing import Any
from celery.signals import setup_logging
@setup_logging.connect
def config_loggers(*args: Any, **kwargs: Any) -> None: # pragma: no cover
"""
Configures celery to use the Django settings.py logging configuration.
"""
from logging.config import dictConfig
from django.conf import settings
dictConfig(settings.LOGGING)
python-django-guid-3.4.1/django_guid/integrations/celery/signals.py 0000664 0000000 0000000 00000006303 14546363661 0025523 0 ustar 00root root 0000000 0000000 import logging
from typing import TYPE_CHECKING, Any
from celery.signals import before_task_publish, task_postrun, task_prerun
from django_guid import clear_guid, get_guid, set_guid
from django_guid.config import settings
from django_guid.integrations.celery.context import celery_current, celery_parent
from django_guid.utils import generate_guid
if TYPE_CHECKING:
from celery import Task
logger = logging.getLogger('django_guid.celery')
parent_header = 'CELERY_PARENT_ID'
def set_transaction_id(guid: str) -> None:
"""
Sets the Sentry transaction ID if the Celery sentry integration setting is True.
"""
if settings.integration_settings.celery.sentry_integration:
from sentry_sdk import configure_scope
with configure_scope() as scope:
logger.debug('Setting Sentry transaction_id to %s', guid)
scope.set_tag('transaction_id', guid)
@before_task_publish.connect
def publish_task_from_worker_or_request(headers: dict, **kwargs: Any) -> None:
"""
Called when a request or celery worker publishes a task to the worker pool
by calling task.delay(), task.apply_async() or using another equivalent method.
This is where we transfer state from a parent process to a child process.
"""
guid = get_guid()
logger.info('Setting task request header as %s', guid)
headers[settings.guid_header_name] = guid
if settings.integration_settings.celery.log_parent:
current = celery_current.get()
if current:
headers[parent_header] = current
@task_prerun.connect
def worker_prerun(task: 'Task', **kwargs: Any) -> None:
"""
Called before a worker starts executing a task.
Here we make sure to set the appropriate correlation ID for all logs logged
during the tasks, and on the thread in general. In that regard, this does
the Celery equivalent to what the django-guid middleware does for a request.
"""
guid = task.request.get(settings.guid_header_name)
if guid:
logger.info('Setting GUID %s', guid)
set_guid(guid)
set_transaction_id(guid)
else:
generated_guid = generate_guid(uuid_length=settings.integration_settings.celery.uuid_length)
logger.info('Generated GUID %s', generated_guid)
set_guid(generated_guid)
set_transaction_id(generated_guid)
if settings.integration_settings.celery.log_parent:
origin = task.request.get(parent_header)
if origin:
logger.info('Setting parent ID %s', origin)
celery_parent.set(origin)
generated_current_guid = generate_guid(uuid_length=settings.integration_settings.celery.uuid_length)
logger.info('Generated current ID %s', generated_current_guid)
celery_current.set(generated_current_guid)
@task_postrun.connect
def clean_up(task: 'Task', **kwargs: Any) -> None:
"""
Called after a task is finished.
Here we make sure to clean up the IDs we set in the pre-run method, so that
the next task executed by the same worker doesn't inherit the same IDs.
"""
logger.debug('Cleaning up GUIDs')
clear_guid()
if settings.integration_settings.celery.log_parent:
celery_current.set(None)
celery_parent.set(None)
python-django-guid-3.4.1/django_guid/integrations/sentry.py 0000664 0000000 0000000 00000002377 14546363661 0024133 0 ustar 00root root 0000000 0000000 import logging
from typing import Any
from django.core.exceptions import ImproperlyConfigured
from django_guid.integrations import Integration
logger = logging.getLogger('django_guid')
class SentryIntegration(Integration):
"""
Ensures that each request's correlation ID is passed on to Sentry exception logs as a `transaction_id`.
"""
identifier = 'SentryIntegration'
def setup(self) -> None:
"""
Verifies that the sentry_sdk dependency is installed.
"""
# Makes sure the client has installed the `sentry_sdk` package, and that the header is appropriately named.
try:
import sentry_sdk # noqa: F401
except ModuleNotFoundError:
raise ImproperlyConfigured(
'The package `sentry-sdk` is required for extending your tracing IDs to Sentry. '
'Please run `pip install sentry-sdk` if you wish to include this integration.'
)
def run(self, guid: str, **kwargs: Any) -> None:
"""
Sets the Sentry transaction_id.
"""
import sentry_sdk
with sentry_sdk.configure_scope() as scope:
logger.debug('Setting Sentry transaction_id to %s', guid)
scope.set_tag('transaction_id', guid)
python-django-guid-3.4.1/django_guid/log_filters.py 0000664 0000000 0000000 00000001166 14546363661 0022405 0 ustar 00root root 0000000 0000000 from logging import Filter
from typing import TYPE_CHECKING
from django_guid.middleware import guid
if TYPE_CHECKING:
from logging import LogRecord
class CorrelationId(Filter):
def filter(self, record: 'LogRecord') -> bool:
"""
Determines that the specified record is to be logged.
From the docs:
Is the specified record to be logged? Returns 0 for no, nonzero for
yes. If deemed appropriate, the record may be modified in-place.
:param record: Log record
:return: True
"""
record.correlation_id = guid.get()
return True
python-django-guid-3.4.1/django_guid/middleware.py 0000664 0000000 0000000 00000006334 14546363661 0022213 0 ustar 00root root 0000000 0000000 import asyncio
import logging
from typing import Callable, Union
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django_guid.context import guid
from django_guid.utils import get_id_from_header, ignored_url
try:
from django.utils.decorators import sync_and_async_middleware
except ImportError: # pragma: no cover
raise ImproperlyConfigured('Please use Django GUID 2.x for Django>=3.1. (`pip install django-guid>3`).')
from typing import TYPE_CHECKING
from django_guid.config import settings
if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse
logger = logging.getLogger('django_guid')
def process_incoming_request(request: 'HttpRequest') -> None:
"""
Processes an incoming request. This function is called before the view and later middleware.
Same logic for both async and sync views.
"""
if not ignored_url(request=request):
# Process request and store the GUID in a contextvar
guid.set(get_id_from_header(request))
# Run all integrations
for integration in settings.integrations:
logger.debug('Running integration: `%s`', integration.identifier)
integration.run(guid=guid.get())
def process_outgoing_request(response: 'HttpResponse', request: 'HttpRequest') -> None:
"""
Process an outgoing request. This function is called after the view and before later middleware.
"""
if not ignored_url(request=request):
if settings.return_header:
response[settings.guid_header_name] = guid.get() # Adds the GUID to the response header
if settings.expose_header:
response['Access-Control-Expose-Headers'] = settings.guid_header_name
# Run tear down for all the integrations
for integration in settings.integrations:
logger.debug('Running tear down for integration: `%s`', integration.identifier)
integration.cleanup()
@sync_and_async_middleware
def guid_middleware(get_response: Callable) -> Callable:
"""
Add this middleware to the top of your middlewares.
"""
# One-time configuration and initialization.
if not apps.is_installed('django_guid'):
raise ImproperlyConfigured('django_guid must be in installed apps')
# fmt: off
if asyncio.iscoroutinefunction(get_response):
async def middleware(request: 'HttpRequest') -> Union['HttpRequest', 'HttpResponse']:
logger.debug('async middleware called')
process_incoming_request(request=request)
# ^ Code above this line is executed before the view and later middleware
response = await get_response(request)
process_outgoing_request(response=response, request=request)
return response
else:
def middleware(request: 'HttpRequest') -> Union['HttpRequest', 'HttpResponse']: # type: ignore
logger.debug('sync middleware called')
process_incoming_request(request=request)
# ^ Code above this line is executed before the view and later middleware
response = get_response(request)
process_outgoing_request(response=response, request=request)
return response
# fmt: on
return middleware
python-django-guid-3.4.1/django_guid/py.typed 0000664 0000000 0000000 00000000000 14546363661 0021203 0 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/django_guid/signals.py 0000664 0000000 0000000 00000001774 14546363661 0021541 0 ustar 00root root 0000000 0000000 import logging
from typing import Any, Optional
from django.core.signals import request_finished
from django.dispatch import receiver
from django_guid.middleware import guid
logger = logging.getLogger('django_guid')
@receiver(request_finished)
def clear_guid(sender: Optional[dict], **kwargs: Any) -> None:
"""
Receiver function for when a request finishes.
When a request is finished, clear the GUID from the contextvar. This ensures a GUID is never passed down to
the next request in sync views.
:param sender: The sender of the signal. By documentation, we must allow this input parameter.
:param kwargs: The request_finished signal does not actually send any kwargs, but Django will throw an error
if we don't accept them. This is because at any point arguments could get added to the signal, and the receiver
must be able to handle those new arguments.
:return: None
"""
logger.debug('Received signal `request_finished`, clearing guid')
guid.set(None)
python-django-guid-3.4.1/django_guid/utils.py 0000664 0000000 0000000 00000005632 14546363661 0021236 0 ustar 00root root 0000000 0000000 import logging
import uuid
from typing import TYPE_CHECKING, Optional, Union
from django_guid.config import settings
if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse
logger = logging.getLogger('django_guid')
def get_correlation_id_from_header(request: 'HttpRequest') -> str:
"""
Returns either the provided GUID or a new one depending on if the provided GUID is valid or not.
:param request: HttpRequest object
:return: GUID
"""
given_guid: str = str(request.headers.get(settings.guid_header_name))
if not settings.validate_guid:
logger.debug('Returning ID from header without validating it as a GUID')
return given_guid
elif validate_guid(given_guid):
logger.debug('%s is a valid GUID', given_guid)
return given_guid
else:
new_guid = generate_guid()
if all(letter.isalnum() or letter == '-' for letter in given_guid):
logger.warning('%s is not a valid GUID. New GUID is %s', given_guid, new_guid)
else:
logger.warning('Non-alnum %s provided. New GUID is %s', settings.guid_header_name, new_guid)
return new_guid
def get_id_from_header(request: 'HttpRequest') -> str:
"""
Checks if the request contains the header specified in the Django settings.
If it does, we fetch the header and attempt to validate the contents as GUID.
If no header is found, we generate a GUID to be injected instead.
:param request: HttpRequest object
:return: GUID
"""
header: str = request.headers.get(settings.guid_header_name) # Case insensitive headers.get added in Django2.2
if header:
logger.info('%s found in the header', settings.guid_header_name)
request.correlation_id = get_correlation_id_from_header(request)
else:
request.correlation_id = generate_guid()
logger.info(
'Header `%s` was not found in the incoming request. Generated new GUID: %s',
settings.guid_header_name,
request.correlation_id,
)
return request.correlation_id
def ignored_url(request: Union['HttpRequest', 'HttpResponse']) -> bool:
"""
Checks if the current URL is defined in the `IGNORE_URLS` setting.
:return: Boolean
"""
return request.get_full_path().strip('/') in settings.ignore_urls
def generate_guid(uuid_length: Optional[int] = None) -> str:
"""
Generates an UUIDv4/GUID as a string.
:return: GUID
"""
if settings.uuid_format == 'string':
guid = str(uuid.uuid4())
else:
guid = uuid.uuid4().hex
if uuid_length is None:
return guid[: settings.uuid_length]
return guid[:uuid_length]
def validate_guid(original_guid: str) -> bool:
"""
Validates a GUID.
:param original_guid: string to validate
:return: bool
"""
try:
return bool(uuid.UUID(original_guid, version=4).hex)
except ValueError:
return False
python-django-guid-3.4.1/docker-compose.yml 0000664 0000000 0000000 00000000144 14546363661 0020700 0 ustar 00root root 0000000 0000000 version: '3.7'
services:
redis:
image: redis:latest
ports:
- '127.0.0.1:6378:6379'
python-django-guid-3.4.1/docs/ 0000775 0000000 0000000 00000000000 14546363661 0016174 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/docs/Makefile 0000664 0000000 0000000 00000001172 14546363661 0017635 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
python-django-guid-3.4.1/docs/README_PYPI.rst 0000664 0000000 0000000 00000020133 14546363661 0020523 0 ustar 00root root 0000000 0000000 Django GUID
===========
.. image:: https://img.shields.io/pypi/v/django-guid.svg
:target: https://pypi.org/pypi/django-guid
.. image:: https://img.shields.io/badge/python-3.6+-blue.svg
:target: https://pypi.python.org/pypi/django-guid#downloads
.. image:: https://img.shields.io/badge/django-2.2%20|%203.0%20|%203.1%20-blue.svg
:target: https://pypi.python.org/pypi/django-guid
.. image:: https://img.shields.io/badge/ASGI-supported-brightgreen.svg
:target: https://img.shields.io/badge/ASGI-supported-brightgreen.svg
.. image:: https://img.shields.io/badge/WSGI-supported-brightgreen.svg
:target: https://img.shields.io/badge/WSGI-supported-brightgreen.svg
.. image:: https://readthedocs.org/projects/django-guid/badge/?version=latest
:target: https://django-guid.readthedocs.io/en/latest/?badge=latest
.. image:: https://codecov.io/gh/snok/django-guid/branch/master/graph/badge.svg
:target: https://codecov.io/gh/snok/django-guid
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
:target: https://github.com/pre-commit/pre-commit
--------------
Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request.
In other words, all logs connected to a request now has a unique ID attached to it, making debugging simple.
Which version of Django GUID you should use depends on your Django version and whether you run ``ASGI`` or ``WSGI`` servers.
To determine which Django-GUID version you should use, please see the table below.
+---------------------+--------------------------+
| Django version | Django-GUID version |
+=====================+==========================+
| 3.1.1 or above | 3.x.x - ASGI and WSGI |
+---------------------+--------------------------+
| 3.0.0 - 3.1.0 | 2.x.x - Only WSGI |
+---------------------+--------------------------+
| 2.2.x | 2.x.x - Only WSGI |
+---------------------+--------------------------+
Django GUID >= 3.0.0 uses ``ContextVar`` to store and access the GUID. Previous versions stored the GUID to an object,
making it accessible by using the ID of the current thread. (Version 2 of Django GUID is supported until Django2.2 LTS has passed.)
--------------
**Resources**:
* Free software: BSD License
* Documentation: https://django-guid.readthedocs.io
* Homepage: https://github.com/snok/django-guid
--------------
**Examples**
Log output with a GUID:
.. code-block::
INFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID.
WARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function
INFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID.
INFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID.
WARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function
WARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function
Log output without a GUID:
.. code-block::
INFO ... project.views This is a DRF view log, and should have a GUID.
WARNING ... project.services.file Some warning in a function
INFO ... project.views This is a DRF view log, and should have a GUID.
INFO ... project.views This is a DRF view log, and should have a GUID.
WARNING ... project.services.file Some warning in a function
WARNING ... project.services.file Some warning in a function
See the `documentation `_ for more examples.
************
Installation
************
Install using pip:
.. code-block:: bash
pip install django-guid
********
Settings
********
Package settings are added in your ``settings.py``:
.. code-block:: python
DJANGO_GUID = {
'GUID_HEADER_NAME': 'Correlation-ID',
'VALIDATE_GUID': True,
'RETURN_HEADER': True,
'EXPOSE_HEADER': True,
'INTEGRATIONS': [],
'IGNORE_URLS': [],
'UUID_LENGTH': 32,
}
**Optional Parameters**
* :code:`GUID_HEADER_NAME`
The name of the GUID to look for in a header in an incoming request. Remember that it's case insensitive.
Default: Correlation-ID
* :code:`VALIDATE_GUID`
Whether the :code:`GUID_HEADER_NAME` should be validated or not.
If the GUID sent to through the header is not a valid GUID (:code:`uuid.uuid4`).
Default: True
* :code:`RETURN_HEADER`
Whether to return the GUID (Correlation-ID) as a header in the response or not.
It will have the same name as the :code:`GUID_HEADER_NAME` setting.
Default: True
* :code:`EXPOSE_HEADER`
Whether to return :code:`Access-Control-Expose-Headers` for the GUID header if
:code:`RETURN_HEADER` is :code:`True`, has no effect if :code:`RETURN_HEADER` is :code:`False`.
This is allows the JavaScript Fetch API to access the header when CORS is enabled.
Default: True
* :code:`INTEGRATIONS`
Whether to enable any custom or available integrations with :code:`django_guid`.
As an example, using :code:`SentryIntegration()` as an integration would set Sentry's :code:`transaction_id` to
match the GUID used by the middleware.
Default: []
* :code:`IGNORE_URLS`
URL endpoints where the middleware will be disabled. You can put your health check endpoints here.
Default: []
* :code:`UUID_LENGTH`
Lets you optionally trim the length of the package generated UUIDs.
Default: 32
*************
Configuration
*************
Once settings have set up, add the following to your projects' ``settings.py``:
1. Installed Apps
=================
Add :code:`django_guid` to your :code:`INSTALLED_APPS`:
.. code-block:: python
INSTALLED_APPS = [
...
'django_guid',
]
2. Middleware
=============
Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``:
.. code-block:: python
MIDDLEWARE = [
'django_guid.middleware.guid_middleware',
...
]
It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID.
3. Logging Configuration
========================
Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration:
.. code-block:: python
LOGGING = {
...
'filters': {
'correlation_id': {
'()': 'django_guid.log_filters.CorrelationId'
}
}
}
Put that filter in your handler:
.. code-block:: python
LOGGING = {
...
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'medium',
'filters': ['correlation_id'],
}
}
}
And make sure to add the new ``correlation_id`` filter to one or all of your formatters:
.. code-block:: python
LOGGING = {
...
'formatters': {
'medium': {
'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s'
}
}
}
If these settings were confusing, please have a look in the demo projects'
`settings.py `_ file for a complete example.
4. Django GUID Logger (Optional)
================================
If you wish to see the Django GUID middleware outputs, you may configure a logger for the module.
Simply add django_guid to your loggers in the project, like in the example below:
.. code-block:: python
LOGGING = {
...
'loggers': {
'django_guid': {
'handlers': ['console', 'logstash'],
'level': 'WARNING',
'propagate': False,
}
}
}
This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs.
python-django-guid-3.4.1/docs/api.rst 0000664 0000000 0000000 00000002175 14546363661 0017504 0 ustar 00root root 0000000 0000000 API
===
Getting started
---------------
You can either use the ``contextvar`` directly by importing it with ``django_guid.middleware import guid``,
or use the API which also logs changes. If you want to use the contextvar, please see the official Python docs.
To use the API import the functions you'd like to use:
.. code-block:: python
from django_guid import get_guid, set_guid, clear_guid
get_guid()
----------
* **Returns**: ``str`` or ``None``, if set by Django-GUID.
Fetches the GUID.
.. code-block:: python
guid = get_guid()
set_guid()
----------
* **Parameters**: ``guid``: ``str``
Sets the GUID to the given input.
.. code-block:: python
set_guid('My GUID')
clear_guid()
------------
Clears the guid (sets it to ``None``)
.. code-block:: python
clear_guid()
Example usage
-------------
.. code-block:: python
import requests
from django.conf import settings
from django_guid import get_guid
requests.get(
url='http://localhost/api',
headers={
'Accept': 'application/json',
settings.DJANGO_GUID['GUID_HEADER_NAME']: get_guid(),
}
)
python-django-guid-3.4.1/docs/changelog.rst 0000664 0000000 0000000 00000000036 14546363661 0020654 0 ustar 00root root 0000000 0000000 .. include:: ../CHANGELOG.rst
python-django-guid-3.4.1/docs/conf.py 0000664 0000000 0000000 00000003733 14546363661 0017501 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'django-guid'
copyright = '2020, Jonas Krüger Svensson, Sondre Lillebø Gundersen'
author = 'Jonas Krüger Svensson, Sondre Lillebø Gundersen'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx_rtd_theme']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# The master toctree document.
master_doc = 'index'
python-django-guid-3.4.1/docs/configuration.rst 0000664 0000000 0000000 00000004735 14546363661 0021606 0 ustar 00root root 0000000 0000000 *************
Configuration
*************
Once django guid has been installed, add the following to your projects' ``settings.py``:
1. Installed Apps
-----------------
Add :code:`django_guid` to your :code:`INSTALLED_APPS`:
.. code-block:: python
INSTALLED_APPS = [
...
'django_guid',
]
2. Middleware
-------------
Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``:
.. code-block:: python
MIDDLEWARE = [
'django_guid.middleware.guid_middleware',
...
]
It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID.
3. Logging Configuration
------------------------
Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration:
.. code-block:: python
LOGGING = {
...
'filters': {
'correlation_id': {
'()': 'django_guid.log_filters.CorrelationId'
}
}
}
Put that filter in your handler:
.. code-block:: python
LOGGING = {
...
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'medium',
'filters': ['correlation_id'],
}
}
}
And make sure to add the new ``correlation_id`` filter to one or all of your formatters:
.. code-block:: python
LOGGING = {
...
'formatters': {
'medium': {
'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s'
}
}
}
If these settings were confusing, please have a look in the demo projects'
`settings.py `_ file for a complete example.
4. Django GUID Logger (Optional)
--------------------------------
If you wish to see the Django GUID middleware outputs, you may configure a logger for the module.
Simply add django_guid to your loggers in the project, like in the example below:
.. code-block:: python
LOGGING = {
...
'loggers': {
'django_guid': {
'handlers': ['console', 'logstash'],
'level': 'WARNING',
'propagate': False,
}
}
}
This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs.
python-django-guid-3.4.1/docs/contributing.rst 0000664 0000000 0000000 00000000041 14546363661 0021430 0 ustar 00root root 0000000 0000000 .. include:: ../CONTRIBUTING.rst
python-django-guid-3.4.1/docs/extended_example.rst 0000664 0000000 0000000 00000010360 14546363661 0022241 0 ustar 00root root 0000000 0000000 .. _extended_example:
Extended example
================
Using tools like ``ab`` (`Apache Benchmark `_) we can benchmark our application with concurrent requests, simulating
heavy load. This is an easy way to display the strength of ``django-guid``.
Experiment
----------
First, we run our application like we would in a production environment:
.. code-block:: bash
gunicorn demoproj.wsgi:application --bind 127.0.0.1:8080 -k gthread -w 4
Then, we do 3 concurrent requests to one of our endpoints:
.. code-block:: bash
ab -c 3 -n 3 http://127.0.0.1:8080/api
This results in these logs:
.. code-block:: bash
django-guid git:(master) ✗ gunicorn demoproj.wsgi:application --bind 127.0.0.1:8080 -k gthread -w 4
[2020-01-14 16:36:15 +0100] [8624] [INFO] Starting gunicorn 20.0.4
[2020-01-14 16:36:15 +0100] [8624] [INFO] Listening at: http://127.0.0.1:8080 (8624)
[2020-01-14 16:36:15 +0100] [8624] [INFO] Using worker: gthread
[2020-01-14 16:36:15 +0100] [8627] [INFO] Booting worker with pid: 8627
[2020-01-14 16:36:15 +0100] [8629] [INFO] Booting worker with pid: 8629
[2020-01-14 16:36:15 +0100] [8630] [INFO] Booting worker with pid: 8630
[2020-01-14 16:36:15 +0100] [8631] [INFO] Booting worker with pid: 8631
# First request
INFO 2020-01-14 15:40:48,953 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 773fa6885e03493498077a273d1b7f2d
INFO 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] demoproj.views This is a DRF view log, and should have a GUID.
WARNING 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] demoproj.services.useless_file Some warning in a function
DEBUG 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] django_guid.middleware Deleting 773fa6885e03493498077a273d1b7f2d from _guid
# Second and third request arrives at the same time
INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 0d1c3919e46e4cd2b2f4ac9a187a8ea1
INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 99d44111e9174c5a9494275aa7f28858
INFO 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] demoproj.views This is a DRF view log, and should have a GUID.
INFO 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.views This is a DRF view log, and should have a GUID.
WARNING 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] demoproj.services.useless_file Some warning in a function
WARNING 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.services.useless_file Some warning in a function
DEBUG 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] django_guid.middleware Deleting 0d1c3919e46e4cd2b2f4ac9a187a8ea1 from _guid
DEBUG 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] django_guid.middleware Deleting 99d44111e9174c5a9494275aa7f28858 from _guid
If we have a close look, we can see that the first request is completely done before the second and third arrives.
How ever, the second and third request arrives at the exact same time, and since ``gunicorn`` is run with multiple workers,
they are also handled concurrently. The result is logs that get mixed together, making them impossible to differentiate.
Now, depending on how you view your logs you can easily track a single request down. In these docs, try using ``ctrl`` + ``f``
and search for ``99d44111e9174c5a9494275aa7f28858``
If you're logging to a file you could use ``grep``:
.. code-block:: bash
➜ ~ cat demoproj/logs.log | grep 99d44111e9174c5a9494275aa7f28858
INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 99d44111e9174c5a9494275aa7f28858
INFO 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.views This is a DRF view log, and should have a GUID.
WARNING 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.services.useless_file Some warning in a function
DEBUG 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] django_guid.middleware Deleting 99d44111e9174c5a9494275aa7f28858 from _guid
python-django-guid-3.4.1/docs/img/ 0000775 0000000 0000000 00000000000 14546363661 0016750 5 ustar 00root root 0000000 0000000 python-django-guid-3.4.1/docs/img/sentry.png 0000664 0000000 0000000 00000240661 14546363661 0021013 0 ustar 00root root 0000000 0000000 PNG
IHDR iz sRGB gAMA a pHYs od IDATx^\ދ(X@E$vĂ%v/jEc5-50v,XDDD wmX 13;f|)**" K
d> ֜IOd RFM]a RFIY
<