├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── conftest.py ├── docs ├── Makefile ├── _static │ ├── __empty__ │ └── css │ │ └── custom.css ├── api.rst ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat └── releasing.rst ├── pyproject.toml ├── requirements ├── docs.txt ├── quality.txt └── tests.txt ├── sandbox ├── manage.py ├── sandbox │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── testapp │ ├── __init__.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── templates │ └── testapp │ │ └── testit.html │ ├── urls.py │ └── views.py ├── setup.cfg ├── setup.py ├── src └── cid │ ├── __init__.py │ ├── apps.py │ ├── backends │ ├── __init__.py │ ├── mysql │ │ ├── __init__.py │ │ └── base.py │ ├── oracle │ │ ├── __init__.py │ │ └── base.py │ ├── postgis │ │ ├── __init__.py │ │ └── base.py │ ├── postgresql │ │ ├── __init__.py │ │ └── base.py │ └── sqlite3 │ │ ├── __init__.py │ │ └── base.py │ ├── context_processors.py │ ├── cursor.py │ ├── locals │ ├── __init__.py │ ├── base.py │ └── context.py │ ├── log.py │ ├── middleware.py │ └── models.py └── tests ├── __init__.py ├── test_context_processors.py ├── test_cursor.py ├── test_locals.py └── test_middleware.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration checks 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 7 * * 0' # weekly 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | name: Python ${{ matrix.python-version }} / ${{ matrix.django-version }} 12 | strategy: 13 | matrix: 14 | python-version: 15 | - "3.8" 16 | - "3.9" 17 | - "3.10" 18 | - "3.11" 19 | django-version: 20 | - "3.1" 21 | - "3.2" 22 | - "4.0" 23 | - "4.1" 24 | - "4.2" 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Cache pip 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.cache/pip 35 | key: ${{ runner.os }} 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip setuptools wheel 39 | pip install -r requirements/tests.txt django=="${{ matrix.django-version }}.*" 40 | pip freeze 41 | - name: Run tests 42 | run: | 43 | pytest 44 | 45 | quality-oldest-supported-python: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Set up Python 50 | uses: actions/setup-python@v2 51 | with: 52 | python-version: '3.8' 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | pip install -r requirements/tests.txt 57 | pip install -r requirements/quality.txt 58 | pip freeze 59 | - name: Run quality checks 60 | run: | 61 | make quality 62 | 63 | quality-latest-supported-python: 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v2 67 | - name: Set up Python 68 | uses: actions/setup-python@v2 69 | with: 70 | python-version: '3.11' 71 | - name: Install dependencies 72 | run: | 73 | python -m pip install --upgrade pip 74 | pip install -r requirements/tests.txt 75 | pip install -r requirements/quality.txt 76 | pip freeze 77 | - name: Run quality checks 78 | run: | 79 | make quality 80 | 81 | docs: 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v2 85 | - name: Set up Python 86 | uses: actions/setup-python@v2 87 | with: 88 | python-version: '3.11' 89 | - name: Install dependencies 90 | run: | 91 | python -m pip install --upgrade pip 92 | pip install -r requirements/tests.txt 93 | pip install -r requirements/docs.txt 94 | pip freeze 95 | - name: Build documentation 96 | run: | 97 | make docs 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .cache 3 | .ropeproject 4 | *.sublime-* 5 | .python-version 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | htmlcov 26 | 27 | # Sphinx 28 | docs/_build 29 | 30 | # Django 31 | *.sqlite3 32 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: requirements/docs.txt 14 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Original author: Jonathan Moss . 6 | 7 | Current maintainers: the (mostly) nice people at `Polyconseil`_. 8 | 9 | Contributors: 10 | 11 | * Francis Reyes 12 | 13 | 14 | .. _Polyconseil: https://opensource.polyconseil.fr 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of contributions 11 | ---------------------- 12 | 13 | Report bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/Polyconseil/django-cid/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * the versions of ``django-cid``, Django and Python; 21 | * any details about your local setup that might be helpful in troubleshooting; 22 | * detailed steps to reproduce the bug. 23 | 24 | 25 | Write documentation 26 | ~~~~~~~~~~~~~~~~~~~ 27 | 28 | ``django-cid`` could always use more documentation. Don't hesitate to 29 | report typos or grammar correction. 30 | 31 | 32 | Submit feedback 33 | ~~~~~~~~~~~~~~~ 34 | 35 | The best way to send feedback is to file an issue at 36 | https://github.com/Polyconseil/django-cid/issues. 37 | 38 | If you are proposing a feature: 39 | 40 | * Explain in detail how it would work. 41 | * Keep the scope as narrow as possible, to make it easier to implement. 42 | * Remember that this is a volunteer-driven project, and that contributions 43 | are welcome. :) 44 | 45 | 46 | Get started! 47 | ------------ 48 | 49 | Ready to contribute? Here's how to set up ``django-cid`` for local development. 50 | 51 | 1. Fork the ``django-cid`` repo on GitHub. 52 | 2. Clone your fork locally:: 53 | 54 | $ git clone git@github.com:your_name_here/django-cid.git 55 | 56 | 3. Set up a virtual environment and install the dependencies:: 57 | 58 | $ pip install -e . 59 | $ pip install -r requirements/tests.txt 60 | 61 | 4. Create a branch for local development:: 62 | 63 | $ git checkout -b name-of-your-bugfix-or-feature 64 | 65 | Now you can make your changes locally. 66 | 67 | 5. Test your changes locally by running ``make test``. 68 | 69 | 5. Commit your changes and push your branch to GitHub:: 70 | 71 | $ git add . 72 | $ git commit -m "Your detailed description of your changes." 73 | $ git push origin name-of-your-bugfix-or-feature 74 | 75 | 6. Submit a pull request through the GitHub website. 76 | 77 | 78 | Sandbox project 79 | --------------- 80 | 81 | The repository has a ``sandbox`` directory that contains a Django 82 | project that showcases features and may help in testing and 83 | debugging. It does not replace automated tests, though. 84 | 85 | Install ``django-cid`` and you can run the server:: 86 | 87 | $ cd sandbox 88 | $ ./manage.py runserver 89 | [...] 90 | Starting development server at http://127.0.0.1:8000/ 91 | 92 | The home page at `http://127.0.0.1:8000/ `_ is self-documented. 93 | 94 | 95 | Pull request guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. 102 | 3. The pull request should work for all supported versions of Python and Django. 103 | Check https://travis-ci.org/Polyconseil/django-cid/pull_requests 104 | and make sure that the tests pass for all supported Python versions. 105 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ------- 3 | 4 | .. role:: strike 5 | :class: strike 6 | 7 | 8 | 9 | 3.1 (unreleased) 10 | ++++++++++++++++ 11 | 12 | - Nothing changed yet. 13 | 14 | 15 | 3.0 (2024-05-14) 16 | ++++++++++++++++ 17 | 18 | **Important:** This release drops compatibility with old, unsupported 19 | versions of Python and Django (see details below). If you still use 20 | those versions, you should pin ``django-cid < 3``. 21 | 22 | - Add support of Django 3.2, 4.0, 4.1 and 4.2. 23 | 24 | - |backward-incompatible| Drop support of Python 3.7 and prior 25 | versions. Only Python 3.8 and later are supported. 26 | 27 | - |backward-incompatible| Drop support of Django 3.0 and prior 28 | versions. Only Django 3.1 and later are supported. 29 | 30 | 31 | 2.4 (2024-05-14) 32 | ++++++++++++++++ 33 | 34 | - Add ``CID_SQL_STATEMENT_TEMPLATE`` setting to customize the position 35 | of the correlation relative to the original SQL statement. 36 | Contributed by Cauê Garcia Polimanti (@CaueP). 37 | 38 | 39 | 2.3 (2022-06-13) 40 | ++++++++++++++++ 41 | 42 | - Under Python 3.7 and later, use context variables (with the contextvars module) 43 | instead of a thread-local variable to avoid state bleeding in concurrent code. 44 | 45 | 46 | 2.2 (2021-03-15) 47 | ++++++++++++++++ 48 | 49 | - Add support of Django 3.1. 50 | 51 | - Remove support of Python 3.5. 52 | 53 | - :strike:`Under Python 3.7 and later, use context variables (with the contextvars module) instead of a thread-local variable to avoid state bleeding in concurrent code.` Version 2.2 had a bug that caused context variables to never be used. Thread-local variables were always used. 54 | 55 | 56 | 2.1 (2020-06-22) 57 | ++++++++++++++++ 58 | 59 | * Add support of Django 3.0 60 | * |backward-incompatible| Drop support of Django 2.1. 61 | 62 | 63 | 2.0 (2019-09-27) 64 | ++++++++++++++++ 65 | 66 | * |backward-incompatible| Drop support of Python 3.4. 67 | * |backward-incompatible| Drop support of Django 1.11 and Django 2.0. 68 | * Add ``CID_GENERATOR`` setting to allow the customization of the 69 | correlation id. 70 | 71 | 72 | 1.3 (2018-10-09) 73 | ++++++++++++++++ 74 | 75 | - **bugfix**: Fix packaging bug (introduced in version 1.2) that 76 | caused two extra packages ``tests`` and ``sandbox`` to be installed. 77 | 78 | 79 | 1.2 (2018-10-08) 80 | ++++++++++++++++ 81 | 82 | - **bugfix:** Fix bug (introduced in version 1.0) that caused the 83 | correlation id to be reused across all requests that were processed 84 | by the same thread. 85 | 86 | 87 | 1.1 (2018-10-01) 88 | ++++++++++++++++ 89 | 90 | - Allow to concatenate an upstream correlation id with a 91 | locally-generated one, with a new ``CID_CONCATENATE_IDS`` setting. 92 | 93 | 94 | 1.0 (2018-10-01) 95 | ++++++++++++++++ 96 | 97 | **Warning:** this release includes changes that are not backward 98 | compatible. Be sure to read the details below to know if and how you 99 | can migrate. 100 | 101 | * |backward-incompatible| Drop support of Django 1.10 and earlier. 102 | 103 | * |backward-incompatible| Drop support of Python 2. 104 | 105 | * Add support of Django 2. Version 0.x could already be used with 106 | Django 2 but tests were not run against it. They now are. 107 | 108 | * Generate cid outside of the middleware when ``GENERATE_CID`` is 109 | enabled, so that it's available even if the middleware is not used. 110 | 111 | * Fix support of Django 1.11 in database backends. 112 | 113 | * Add PostGIS database backend. 114 | 115 | * Add ``CID_SQL_COMMENT_TEMPLATE`` to customize how the cid is 116 | included as comments in SQL queries. 117 | 118 | * |backward-incompatible| Change the app name to be used in 119 | INSTALLED_APPS. 120 | 121 | **Migration from version 0.x:** if you had ``cid`` in ``INSTALLED_APPS``, 122 | replace it by ``cid.apps.CidAppConfig``. If you did not, add the 123 | latter. 124 | 125 | * |backward-incompatible| Drop compatibility with 126 | ``MIDDLEWARE_CLASSES``. You should use the ``MIDDLEWARE`` 127 | setting. If you already did, no change is necessary. 128 | 129 | If you really must use the old ``MIDDLEWARE_CLASSES`` setting, 130 | include ``CidOldStyleMiddleware`` instead of ``CidMiddleware``. 131 | 132 | 133 | 0.2.0 (2016-12-06) 134 | ++++++++++++++++++ 135 | 136 | * Added support for Django 1.10 middleware (thanks @qbey) 137 | 138 | 139 | 0.1.2 (2016-12-01) 140 | ++++++++++++++++++ 141 | 142 | * Made CID repsonse header configurable, and optional (thanks @dbaty) 143 | 144 | 0.1.0 (2014-08-05) 145 | ++++++++++++++++++ 146 | 147 | * First release on PyPI. 148 | 149 | 150 | .. role:: raw-html(raw) 151 | 152 | .. |backward-incompatible| raw:: html 153 | 154 | backward incompatible 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Snowball One 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of CID nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | recursive-include src/cid *.html *.png *.gif *js *.css *jpg *jpeg *svg *py 7 | 8 | prune docs 9 | prune requirements 10 | prune sandbox 11 | prune tests 12 | 13 | exclude Makefile 14 | exclude conftest.py 15 | exclude pyproject.toml 16 | exclude .readthedocs.yaml -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | rm -fr build/ 4 | rm -fr dist/ 5 | find . -name '*.pyc' -delete 6 | find . -name '*.pyo' -delete 7 | find . -name '*~' -delete 8 | 9 | .PHONY: docs 10 | docs: 11 | sphinx-build -W -n -b html docs ./build/sphinx/html 12 | 13 | .PHONY: quality 14 | quality: 15 | check-manifest 16 | isort --check-only --diff src tests 17 | pylint --reports=no setup.py src tests 18 | check-branches 19 | check-fixmes 20 | python setup.py sdist >/dev/null 2>&1 && twine check dist/* 21 | 22 | .PHONY: clean 23 | release: clean 24 | fullrelease 25 | 26 | .PHONY: test 27 | test: 28 | pytest 29 | 30 | .PHONY: coverage 31 | coverage: 32 | pytest --cov cid 33 | 34 | .PHONY: coverage-html 35 | coverage-html: 36 | pytest --cov cid --cov-report html 37 | python -c "import webbrowser; webbrowser.open('htmlcov/index.html')" 38 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Django Correlation ID 3 | ===================== 4 | 5 | Logging is important. Anyone who has had a call at 3am to say the site 6 | is down knows this. Without quality logging it is almost impossible to 7 | work out what on earth is happening. 8 | 9 | The more you log, the harder it is to track down exactly what the 10 | effects of a particular request are. Enter Django Correlation ID. 11 | Incoming requests are assigned a unique identifier. This can either 12 | happen in your public facing web server (e.g. nginx) or be applied 13 | by Django itself. 14 | 15 | This correlation id (also known as request id) is then available 16 | through the Django request/response cycle and may be automatically 17 | included in all log messages. That way, you can easily link all log 18 | messages that relate to the same request:: 19 | 20 | 2018-10-01T08:18:39.86+00:00 correlation_id=2433d5d4-27a3-4889-b14b-107a131368a3 Call to plug from cpoint=1 21 | 2018-10-01T08:18:39.90+00:00 correlation_id=72fbd7dd-a0ba-4f92-9ed0-0db358338e86 Call to state by cpoint=2 with {'state': {'B': 'idle', 'A': 'on_charge'}} 22 | 2018-10-01T08:18:39.92+00:00 correlation_id=2433d5d4-27a3-4889-b14b-107a131368a3 Ended rental=7 customer="John Smith" on plug 23 | 24 | In this example, we can see that the first and the third log messages 25 | are tied to the same request, while the second message relates to a 26 | distinct request. 27 | 28 | In addition to these logs, ``django-cid`` can include the correlation 29 | id: 30 | 31 | - in all SQL queries (as a comment); 32 | - in rendered templates; 33 | - as a header in the HTTP response generated by Django; 34 | - and possibly anywhere by using the API of ``django-cid``, for 35 | example as an HTTP header on a request to another internal system of 36 | yours, which is especially useful in service-oriented architecture. 37 | 38 | Documentation can be found at: https://django-correlation-id.readthedocs.org/ 39 | 40 | Sources are on GitHub: https://github.com/Polyconseil/django-cid 41 | 42 | 43 | Supported versions 44 | ------------------ 45 | 46 | We currently support Python >= 3.8 and Django >= 3.1. 47 | 48 | Other versions may work but are not supported. 49 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django import conf 6 | from django.core.files import temp as tempfile 7 | 8 | 9 | sys.path.insert(0, os.path.dirname(__file__)) 10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'sandbox')) 11 | 12 | 13 | def location(x): 14 | os.path.join(os.path.dirname(os.path.realpath(__file__)), x) 15 | 16 | 17 | def pytest_configure(): 18 | conf.settings.configure( 19 | DATABASES={ 20 | 'default': { 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | 'NAME': ':memory:', 23 | } 24 | }, 25 | DEBUG=False, 26 | SITE_ID=1, 27 | SECRET_KEY="the-secret-key", 28 | EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend', 29 | ROOT_URLCONF='sandbox.urls', 30 | 31 | INSTALLED_APPS=[ 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sessions', 35 | 'django.contrib.sites', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'django.contrib.admin', 39 | 40 | 'cid.apps.CidAppConfig', 41 | 'testapp', 42 | ], 43 | 44 | STATICFILES_DIRS=(location('static/'),), 45 | STATIC_ROOT=location('public'), 46 | STATIC_URL='/static/', 47 | MEDIA_ROOT=tempfile.gettempdir(), 48 | MEDIA_URL='/media/', 49 | 50 | TEMPLATE_CONTEXT_PROCESSORS=( 51 | "django.contrib.auth.context_processors.auth", 52 | "django.core.context_processors.request", 53 | "django.core.context_processors.debug", 54 | "django.core.context_processors.i18n", 55 | "django.core.context_processors.media", 56 | "django.core.context_processors.static", 57 | "django.contrib.messages.context_processors.messages", 58 | ), 59 | TEMPLATE_DIRS=( 60 | location('templates'), 61 | ), 62 | 63 | # Other settings go here 64 | LOGGING={ 65 | 'version': 1, 66 | 'formatters': { 67 | 'verbose': { 68 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(cid)s %(message)s' # noqa 69 | }, 70 | 'simple': { 71 | 'format': '%(levelname)s %(message)s' 72 | } 73 | }, 74 | 'filters': { 75 | 'cid': { 76 | '()': 'cid.log.CidContextFilter' 77 | } 78 | }, 79 | 'handlers': { 80 | 'cid': { 81 | 'level': 'DEBUG', 82 | 'class': 'logging.StreamHandler', 83 | 'formatter': 'verbose', 84 | 'filters': ['cid'] 85 | } 86 | }, 87 | 'loggers': { 88 | 'cid': { 89 | 'handlers': ['cid'], 90 | 'propagate': True, 91 | 'level': 'DEBUG' 92 | } 93 | } 94 | } 95 | ) 96 | django.setup() 97 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/__empty__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/docs/_static/__empty__ -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .strike { 2 | text-decoration: line-through; 3 | } 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. automodule:: cid.locals 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | import datetime 18 | import os 19 | 20 | 21 | os.environ['DJANGO_SETTINGS_MODULE'] = 'sandbox.settings' 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = 'Django Correlation Id' 26 | copyright = '%d, Contributors of django-cid' % datetime.date.today().year 27 | author = 'Contributors of django-cid' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'sphinx_rtd_theme' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ['_static'] 59 | 60 | html_css_files = [ 61 | 'css/custom.css', 62 | ] 63 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-cid: Correlation id in Django 2 | ==================================== 3 | 4 | .. include:: ../README.rst 5 | 6 | 7 | Topics 8 | ------ 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | installation 14 | api 15 | contributing 16 | releasing 17 | authors 18 | history 19 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Installation and configuration 3 | ============================== 4 | 5 | Installation 6 | ============ 7 | 8 | At the command line:: 9 | 10 | $ pip install django-cid 11 | 12 | 13 | Configuration 14 | ============= 15 | 16 | You need to add ``cid.apps.CidAppConfig`` to your list of installed apps. 17 | 18 | .. code-block:: python 19 | 20 | INSTALLED_APPS = ( 21 | # some apps 22 | 'cid.apps.CidAppConfig', 23 | # some other apps 24 | ) 25 | 26 | 27 | Generation of the correlation id 28 | -------------------------------- 29 | 30 | The correlation id may be generated by ``django-cid`` itself or come 31 | from upstream through an incoming HTTP header. 32 | 33 | To let ``django-cid`` generate an id, set ``CID_GENERATE`` to true in 34 | the settings: 35 | 36 | .. code-block:: python 37 | 38 | CID_GENERATE = True 39 | 40 | By default, ``django-cid`` uses ``str(uuid.uuid4())`` to generate the 41 | correlation id but you can customize this generation to suit your 42 | needs in the settings: 43 | 44 | .. code-block:: python 45 | 46 | CID_GENERATOR = lambda: f'{time.time()}-{random.random()}' 47 | 48 | Letting ``django-cid`` generate a new correlation id is perfectly 49 | acceptable but does suffer one drawback. If you host your Django 50 | application behind another web server such as nginx, then nginx logs 51 | won't contain the correlation id. 52 | ``django-cid`` can handle this by extracting a correlation id created 53 | in nginx and passed through as a header in the HTTP request. For this 54 | to work, you must enable a middleware in the settings, like this: 55 | 56 | .. code-block:: python 57 | 58 | MIDDLEWARE = ( 59 | 'cid.middleware.CidMiddleware', 60 | # other middlewares 61 | ) 62 | 63 | The middleware takes care of getting the correlation from the HTTP 64 | request header. By default it looks for a header named 65 | ``X_CORRELATION_ID``, but you can change this with the ``CID_HEADER`` 66 | setting: 67 | 68 | .. code-block:: python 69 | 70 | CID_HEADER = 'X_MY_CID_HEADER' 71 | 72 | .. note:: 73 | 74 | Most WSGI implementations sanitize HTTP headers by appending an 75 | ``HTTP_`` prefix and replacing ``-`` by ``_``. For example, an 76 | incoming ``X-Correlation-Id`` header would be available as 77 | ``HTTP_X_CORRELATION_ID`` in Django. When using such a WSGI server 78 | in front of Django, the latter, sanitized value should be used in 79 | the settings. 80 | 81 | If a correlation id is provided upstream (e.g. "1234"), it is possible 82 | to concatenate it with a newly generated one. The cid will then look 83 | like ``1234, 1aa38e4e-89c6-4655-9b8e-38ca349da017``. To do so, use the 84 | following settings: 85 | 86 | .. code-block:: python 87 | 88 | CID_GENERATE = True 89 | CID_CONCATENATE_IDS = True 90 | 91 | This is useful when you use a service-oriented architecture and want 92 | to be able to follow a request amongst all systems (by looking at logs 93 | that have the first correlation id that was set upstream), and also on 94 | a particular system (by looking at logs that have the id added by the 95 | system itself). 96 | 97 | 98 | Inclusion of the correlation id in the response 99 | ----------------------------------------------- 100 | 101 | By default ``django-cid`` sets an HTTP header in the HTTP response 102 | with the same name as configured in ``CID_HEADER``. You may customize 103 | it with ``CID_RESPONSE_HEADER`` in the settings: 104 | 105 | .. code-block:: python 106 | 107 | CID_RESPONSE_HEADER = 'X-Something-Completely-Different' 108 | 109 | .. note:: 110 | 111 | As indicated in the note above, if Django is behind a WSGI server 112 | that sanitizes HTTP headers, you need to customize 113 | ``CID_RESPONSE_HEADER`` if you want the same header name in the 114 | response as in the request. 115 | 116 | .. code-block:: python 117 | 118 | # Nginx sets ``X-Correlation-Id`` but it is sanitized by the WSGI server. 119 | CID_HEADER = 'HTTP_X_CORRELATION_ID' 120 | # Don't use the default value (equal to CID_HEADER) for the response header. 121 | CID_RESPONSE_HEADER = 'X-Correlation-Id' 122 | 123 | If you don't want the header to appear in the HTTP response, you must 124 | explicitly set ``CID_RESPONSE_HEADER`` to ``None``. 125 | 126 | .. code-block:: python 127 | 128 | # Don't include the header in the HTTP response. 129 | CID_RESPONSE_HEADER = None 130 | 131 | 132 | Inclusion of the correlation id in logs 133 | --------------------------------------- 134 | 135 | The most useful feature of ``django-cid`` is to include the 136 | correlation id in logs. For this you need to add the 137 | ``cid.log.CidContextFilter`` log filter in your log settings, apply it 138 | to each logger, and customize the formatter(s) to include the ``cid`` 139 | variable. 140 | 141 | Here is what it looks like on the the default logging configuration 142 | provided by Django's ``startproject``. Changed lines are highlighted. 143 | 144 | .. code-block:: python 145 | :emphasize-lines: 5, 8, 18-22, 26 146 | 147 | LOGGING = { 148 | 'version': 1, 149 | 'formatters': { 150 | 'verbose': { 151 | 'format': '[cid: %(cid)s] %(levelname)s %(asctime)s %(module)s %(message)s' 152 | }, 153 | 'simple': { 154 | 'format': '[cid: %(cid)s] %(levelname)s %(message)s' 155 | }, 156 | }, 157 | 'handlers': { 158 | 'console': { 159 | 'level': 'INFO', 160 | 'class': 'logging.StreamHandler', 161 | 'formatter': 'verbose', 162 | 'filters': ['correlation'], 163 | }, 164 | }, 165 | 'filters': { 166 | 'correlation': { 167 | '()': 'cid.log.CidContextFilter' 168 | }, 169 | }, 170 | 'loggers': { 171 | 'testapp': { 172 | 'handlers': ['console'], 173 | 'filters': ['correlation'], 174 | 'propagate': True, 175 | }, 176 | }, 177 | } 178 | 179 | You can then use your loggers as you normally do, safe in the 180 | knowledge that you can tie them all back to the correlation id. 181 | 182 | If you want to include the correlation id in all logs, you need to 183 | tweak the "root" key like this: 184 | 185 | .. code-block:: python 186 | 187 | LOGGING = { 188 | # ... 189 | 'root': { 190 | 'level': 'INFO', 191 | 'handlers': ['console'], 192 | 'filters': ['correlation'], 193 | }, 194 | # ... 195 | } 196 | 197 | 198 | 199 | Inclusion of the correlation id in SQL queries 200 | ---------------------------------------------- 201 | 202 | ``django-cid`` can add the correlation id as a comment before the SQL 203 | query so that the correlation id appears in your database logs like 204 | this: 205 | 206 | .. code:: sql 207 | 208 | /* cid: 1234567-68e8-45fc-85c1-e025e5dffd1e */ 209 | SELECT col FROM table 210 | 211 | For this you need to change your database backend to one that is 212 | provided by ``django-cid``. For example, for sqlite3 you need to use 213 | the following: 214 | 215 | .. code-block:: python 216 | 217 | DATABASES = { 218 | 'default': { 219 | 'ENGINE': 'cid.backends.sqlite3', 220 | 'NAME': location('db.sqlite3'), 221 | } 222 | } 223 | 224 | ``django-cid`` has a wrapper for all backends that are currently 225 | supported by Django. Here is the full list: 226 | 227 | mysql 228 | cid.backends.mysql 229 | oracle 230 | cid.backends.oracle 231 | postgis 232 | cid.backends.postgis 233 | postgresql 234 | cid.backends.postgresql 235 | sqlite3 236 | cid.backends.sqlite3 237 | 238 | By default, the correlation id appears as shown in the example above. 239 | You may change that by defining a ``CID_SQL_COMMENT_TEMPLATE`` that is 240 | a string with a ``cid`` format parameter: 241 | 242 | .. code-block:: python 243 | 244 | CID_SQL_COMMENT_TEMPLATE = 'correlation={cid}' 245 | 246 | 247 | Also, you may change the position of the correlation id injected in 248 | the statement by defining a ``CID_SQL_STATEMENT_TEMPLATE`` that is 249 | a string with a ``cid`` and a ``sql`` format parameter: 250 | 251 | .. code-block:: python 252 | 253 | CID_SQL_STATEMENT_TEMPLATE = '/* {cid} */\n{sql}' 254 | 255 | 256 | Inclusion of the correlation id in templates 257 | -------------------------------------------- 258 | 259 | ``django-cid`` provides a template context processor that adds the 260 | correlation id to the template context if it is available. To enable 261 | it, you need to add it in the list of ``TEMPLATE_CONTEXT_PROCESSORS`` 262 | in the settings: 263 | 264 | .. code-block:: python 265 | 266 | TEMPLATE_CONTEXT_PROCESSORS = ( 267 | # other template processors 268 | 'cid.context_processors.cid_context_processor', 269 | ) 270 | 271 | It will add a context variable ``correlation_id`` if a correlation id 272 | is available. You may include it in your template with the follwing 273 | snippet: 274 | 275 | .. code-block:: django 276 | 277 | {% if correlation_id %} 278 | 279 | {% endif %} 280 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/releasing.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Releasing a new version 3 | ======================= 4 | 5 | We use the excellent `zest.releaser`_ tool to make new releases. There 6 | is a Makefile rule that does a bit more cleaning beforehand. Just 7 | type:: 8 | 9 | make release 10 | 11 | And then follow the instructions. 12 | 13 | We try to use `semantic versioning`_, i.e. use MAJOR.MINOR.PATCH 14 | version numbers with: 15 | 16 | - MAJOR version when we make incompatible API changes; 17 | - MINOR version when we add functionality in a backwards-compatible manner; 18 | - PATCH version when we make backwards-compatible bug fixes. 19 | 20 | Although the distinction between MINOR and PATCH has not always been 21 | followed, the changelog should be clear enough. 22 | 23 | 24 | .. _zest.releaser: https://zestreleaser.readthedocs.io/en/latest/ 25 | .. _semantic versioning: https://semver.org/ 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | multi_line_output = 3 3 | use_parentheses = true 4 | include_trailing_comma = true 5 | force_grid_wrap = 0 6 | combine_as_imports = true 7 | line_length = 88 8 | lines_after_imports = 2 9 | force_single_line = true 10 | force_sort_within_sections = true 11 | sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER" 12 | known_tests = ["tests"] 13 | skip = [ 14 | "docs/conf.py", 15 | ] 16 | 17 | 18 | [tool.pylint.MASTER] 19 | load-plugins = [ 20 | "pylint_strict_informational", 21 | ] 22 | 23 | 24 | [tool.pylint.REPORTS] 25 | msg-template = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" 26 | 27 | [tool.pylint.FORMAT] 28 | max-line-length = "120" 29 | 30 | [tool.pylint."MESSAGES CONTROL"] 31 | enable = [ 32 | "use-symbolic-message-instead", 33 | "useless-suppression", 34 | ] 35 | 36 | disable = [ 37 | "arguments-differ", 38 | "attribute-defined-outside-init", 39 | "cyclic-import", 40 | "duplicate-code", 41 | "fixme", 42 | "file-ignored", 43 | "import-outside-toplevel", 44 | "invalid-name", 45 | "locally-disabled", 46 | "missing-docstring", 47 | "no-member", 48 | "protected-access", 49 | "superfluous-parens", 50 | "too-few-public-methods", 51 | "too-many-ancestors", 52 | "too-many-arguments", 53 | "too-many-branches", 54 | "too-many-instance-attributes", 55 | "too-many-lines", 56 | "too-many-locals", 57 | "too-many-public-methods", 58 | "too-many-statements", 59 | "ungrouped-imports", 60 | "unused-argument", 61 | "useless-return", 62 | "wrong-import-order", # we have a custom isort config which pylint can't grok 63 | ] -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | sphinx 4 | sphinx_rtd_theme 5 | 6 | django 7 | -------------------------------------------------------------------------------- /requirements/quality.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | check-oldies[toml] 3 | isort 4 | pylint 5 | pylint_strict_informational 6 | twine 7 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | pytest 3 | pytest-django 4 | -------------------------------------------------------------------------------- /sandbox/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /sandbox/sandbox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/sandbox/sandbox/__init__.py -------------------------------------------------------------------------------- /sandbox/sandbox/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for sandbox project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /sandbox/sandbox/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for sandbox project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-dpcpb#6ws0z5#5h1ae3343)$2#=%&uh_$00zp7#^!)pp#)$o*i' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'cid.apps.CidAppConfig', 42 | 43 | 'testapp.apps.TestappConfig', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'sandbox.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | 70 | 'cid.context_processors.cid_context_processor', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'sandbox.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'cid.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 123 | 124 | STATIC_URL = 'static/' 125 | 126 | # Default primary key field type 127 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 128 | 129 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 130 | 131 | CID_GENERATE = True 132 | 133 | LOGGING = { 134 | 'version': 1, 135 | 'formatters': { 136 | 'verbose': { 137 | 'format': '[cid: %(cid)s] %(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 138 | }, 139 | 'simple': { 140 | 'format': '[cid: %(cid)s] %(levelname)s %(message)s' 141 | }, 142 | }, 143 | 'handlers': { 144 | 'console': { 145 | 'level': 'INFO', 146 | 'class': 'logging.StreamHandler', 147 | 'formatter': 'verbose', 148 | 'filters': ['correlation'], 149 | }, 150 | }, 151 | 'filters': { 152 | 'correlation': { 153 | '()': 'cid.log.CidContextFilter' 154 | }, 155 | }, 156 | 'loggers': { 157 | 'testapp': { 158 | 'handlers': ['console'], 159 | 'propagate': True, 160 | }, 161 | }, 162 | # If you want to include the correlation id on all logs, uncomment 163 | # the following block: 164 | # 165 | # 'root': { 166 | # 'level': 'INFO', 167 | # 'handlers': ['console'], 168 | # 'filters': ['correlation'], 169 | # } 170 | } 171 | -------------------------------------------------------------------------------- /sandbox/sandbox/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include 3 | from django.urls import path 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('', include('testapp.urls')), 8 | ] 9 | -------------------------------------------------------------------------------- /sandbox/sandbox/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sandbox project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /sandbox/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/sandbox/testapp/__init__.py -------------------------------------------------------------------------------- /sandbox/testapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestappConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'testapp' 7 | -------------------------------------------------------------------------------- /sandbox/testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2023-07-28 10:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Item', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('number', models.IntegerField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /sandbox/testapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/sandbox/testapp/migrations/__init__.py -------------------------------------------------------------------------------- /sandbox/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Item(models.Model): 5 | number = models.IntegerField() 6 | -------------------------------------------------------------------------------- /sandbox/testapp/templates/testapp/testit.html: -------------------------------------------------------------------------------- 1 |

Test page for django-cid

2 | 3 | 4 |

Template context processors

5 |

6 | The correlation id should be automatically added in this rendered 7 | template, hereafter between the brackets: 8 |

9 |
[{{correlation_id }}]
10 | 11 |

SQL queries

12 |

13 | The view has made an SQL query on the testapp_item table 14 | that should contain the correlation id as a comment. Check your 15 | database logs. The query looks like the following (but with a 16 | comment): 17 |

18 |
{{ sql_query }}
19 | 20 | 21 |

Logs

22 |

23 | The view has logged a warning. It should contain a correlation 24 | id. Check the console. 25 |

26 | -------------------------------------------------------------------------------- /sandbox/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | path('ok', views.ok, name='ok'), 8 | path('', views.testit, name='testit'), 9 | ) 10 | -------------------------------------------------------------------------------- /sandbox/testapp/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import connection 4 | from django.http import HttpResponse 5 | from django.shortcuts import render 6 | 7 | from testapp.models import Item 8 | 9 | 10 | logger = logging.getLogger('testapp') 11 | 12 | 13 | def testit(request): 14 | logger.warning("This is a warning from django-cid test application.") 15 | list(Item.objects.all()) 16 | context = { 17 | 'sql_query': connection.queries[0]['sql'], 18 | } 19 | return render(request, 'testapp/testit.html', context) 20 | 21 | 22 | def ok(request): 23 | """A simple view for integration tests.""" 24 | return HttpResponse('ok') 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-cid 3 | version = 3.1.dev0 4 | description = Correlation IDs in Django for debugging requests 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | url = https://django-correlation-id.readthedocs.io/ 8 | author = Snowball One 9 | author_email = opensource+django-cid@polyconseil.fr 10 | maintainer = Polyconseil 11 | maintainer_email = opensource+django-cid@polyconseil.fr 12 | license = BSD-3-Clause 13 | license_files = LICENSE.txt 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Framework :: Django 17 | Framework :: Django :: 3 18 | Framework :: Django :: 3.1 19 | Framework :: Django :: 3.2 20 | Framework :: Django :: 4 21 | Framework :: Django :: 4.0 22 | Framework :: Django :: 4.1 23 | Framework :: Django :: 4.2 24 | Intended Audience :: Developers 25 | License :: OSI Approved :: BSD License 26 | Natural Language :: English 27 | Programming Language :: Python 28 | Programming Language :: Python :: 3 29 | Programming Language :: Python :: 3 :: Only 30 | Programming Language :: Python :: 3.8 31 | Programming Language :: Python :: 3.9 32 | Programming Language :: Python :: 3.10 33 | Programming Language :: Python :: 3.11 34 | keywords = django logging correlation id debugging 35 | platform = any 36 | project_urls = 37 | Documentation = https://django-correlation-id.readthedocs.io/ 38 | Source = https://github.com/Polyconseil/django-cid 39 | Bug Tracker = https://github.com/Polyconseil/django-cid/issues 40 | Changelog = https://django-correlation-id.readthedocs.io/en/latest/history.html 41 | 42 | 43 | [options] 44 | python_requires = >=3.8 45 | install_requires = 46 | django>=3.1 47 | package_dir= 48 | =src 49 | packages=find: 50 | 51 | [options.packages.find] 52 | where = src 53 | 54 | [zest.releaser] 55 | create-wheel = yes -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | setuptools.setup() 5 | -------------------------------------------------------------------------------- /src/cid/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | 4 | __version__ = importlib.metadata.version('django-cid') 5 | -------------------------------------------------------------------------------- /src/cid/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CidAppConfig(AppConfig): 5 | name = 'cid' 6 | verbose_name = 'Django Correlation Id' 7 | -------------------------------------------------------------------------------- /src/cid/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/mysql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/mysql/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/mysql/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.mysql.base import DatabaseWrapper as BaseDatabaseWrapper 2 | 3 | from ...cursor import CidCursorWrapper 4 | 5 | 6 | class DatabaseWrapper(BaseDatabaseWrapper): 7 | def create_cursor(self, name=None): 8 | base_cursor = super().create_cursor(name) 9 | return CidCursorWrapper(base_cursor) 10 | -------------------------------------------------------------------------------- /src/cid/backends/oracle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/oracle/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/oracle/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.oracle.base import DatabaseWrapper as BaseDatabaseWrapper 2 | 3 | from ...cursor import CidCursorWrapper 4 | 5 | 6 | class DatabaseWrapper(BaseDatabaseWrapper): 7 | def create_cursor(self, name=None): 8 | base_cursor = super().create_cursor(name) 9 | return CidCursorWrapper(base_cursor) 10 | -------------------------------------------------------------------------------- /src/cid/backends/postgis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/postgis/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/postgis/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.base import ( 2 | DatabaseWrapper as BaseDatabaseWrapper, 3 | ) 4 | 5 | from ...cursor import CidCursorWrapper 6 | 7 | 8 | class DatabaseWrapper(BaseDatabaseWrapper): 9 | def create_cursor(self, name=None): 10 | base_cursor = super().create_cursor(name) 11 | return CidCursorWrapper(base_cursor) 12 | -------------------------------------------------------------------------------- /src/cid/backends/postgresql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/postgresql/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/postgresql/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.postgresql.base import DatabaseWrapper as BaseDatabaseWrapper 2 | 3 | from ...cursor import CidCursorWrapper 4 | 5 | 6 | class DatabaseWrapper(BaseDatabaseWrapper): 7 | def create_cursor(self, name=None): 8 | base_cursor = super().create_cursor(name) 9 | return CidCursorWrapper(base_cursor) 10 | -------------------------------------------------------------------------------- /src/cid/backends/sqlite3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/backends/sqlite3/__init__.py -------------------------------------------------------------------------------- /src/cid/backends/sqlite3/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.sqlite3.base import DatabaseWrapper as BaseDatabaseWrapper 2 | 3 | from ...cursor import CidCursorWrapper 4 | 5 | 6 | class DatabaseWrapper(BaseDatabaseWrapper): 7 | 8 | def create_cursor(self, name=None): 9 | base_cursor = super().create_cursor(name) 10 | return CidCursorWrapper(base_cursor) 11 | -------------------------------------------------------------------------------- /src/cid/context_processors.py: -------------------------------------------------------------------------------- 1 | from .locals import get_cid 2 | 3 | 4 | def cid_context_processor(request): 5 | """ 6 | Adds the correlation id as ``correlation_id`` to template contexts 7 | """ 8 | cid = get_cid() 9 | if cid: 10 | return {'correlation_id': get_cid()} 11 | return {} 12 | -------------------------------------------------------------------------------- /src/cid/cursor.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from .locals import get_cid 4 | 5 | 6 | DEFAULT_CID_SQL_COMMENT_TEMPLATE = 'cid: {cid}' 7 | DEFAULT_SQL_STATEMENT_TEMPLATE = '/* {cid} */\n{sql}' 8 | 9 | 10 | class CidCursorWrapper: 11 | """ 12 | A cursor wrapper that attempts to add a cid comment to each query 13 | """ 14 | def __init__(self, cursor): 15 | self.cursor = cursor 16 | 17 | def __getattr__(self, attr): 18 | if attr in self.__dict__: 19 | return self.__dict__[attr] 20 | return getattr(self.cursor, attr) 21 | 22 | def __iter__(self): 23 | return iter(self.cursor) 24 | 25 | def __enter__(self): 26 | return self 27 | 28 | def __exit__(self, exc_type, exc_value, traceback): 29 | self.close() 30 | 31 | def add_comment(self, sql): 32 | cid_sql_template = getattr( 33 | settings, 'CID_SQL_COMMENT_TEMPLATE', DEFAULT_CID_SQL_COMMENT_TEMPLATE 34 | ) 35 | sql_statement_template = getattr( 36 | settings, 'CID_SQL_STATEMENT_TEMPLATE', DEFAULT_SQL_STATEMENT_TEMPLATE 37 | ) 38 | cid = get_cid() 39 | if not cid: 40 | return sql 41 | cid = cid.replace('/*', r'\/\*').replace('*/', r'\*\/') 42 | cid = cid_sql_template.format(cid=cid) 43 | statement = sql_statement_template.format(cid=cid, sql=sql) 44 | return statement 45 | 46 | # The following methods cannot be implemented in __getattr__, because the 47 | # code must run when the method is invoked, not just when it is accessed. 48 | 49 | def callproc(self, procname, params=None): 50 | return self.cursor.callproc(procname, params) 51 | 52 | def execute(self, sql, params=None): 53 | sql = self.add_comment(sql) 54 | return self.cursor.execute(sql, params) 55 | 56 | def executemany(self, sql, param_list): 57 | sql = self.add_comment(sql) 58 | return self.cursor.executemany(sql, param_list) 59 | -------------------------------------------------------------------------------- /src/cid/locals/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import generate_new_cid 2 | from .context import get_cid 3 | from .context import set_cid 4 | 5 | 6 | __all__ = ( 7 | 'generate_new_cid', 8 | 'set_cid', 9 | 'get_cid', 10 | ) 11 | -------------------------------------------------------------------------------- /src/cid/locals/base.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.conf import settings 4 | 5 | 6 | def generate_new_cid(upstream_cid=None): 7 | """Generate a new correlation id, possibly based on the given one.""" 8 | if upstream_cid is None: 9 | return build_cid() if getattr(settings, 'CID_GENERATE', False) else None 10 | if ( 11 | getattr(settings, 'CID_CONCATENATE_IDS', False) 12 | and getattr(settings, 'CID_GENERATE', False) 13 | ): 14 | return f'{upstream_cid}, {build_cid()}' 15 | return upstream_cid 16 | 17 | 18 | def build_cid(): 19 | """Build a new cid""" 20 | return str(getattr(settings, 'CID_GENERATOR', uuid.uuid4)()) 21 | -------------------------------------------------------------------------------- /src/cid/locals/context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from django.conf import settings 4 | 5 | from .base import build_cid 6 | 7 | 8 | correlation_id = ContextVar('correlation_id', default=None) 9 | 10 | 11 | def set_cid(cid): 12 | """Set the correlation id on the context.""" 13 | correlation_id.set(cid) 14 | 15 | 16 | def get_cid(): 17 | """Return the currently set correlation id (if any). 18 | """ 19 | cid = correlation_id.get() 20 | if cid is None and getattr(settings, 'CID_GENERATE', False): 21 | cid = build_cid() 22 | set_cid(cid) 23 | return cid 24 | -------------------------------------------------------------------------------- /src/cid/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from cid.locals import get_cid 4 | 5 | 6 | class CidContextFilter(logging.Filter): 7 | 8 | def filter(self, record): 9 | record.cid = get_cid() 10 | return True 11 | -------------------------------------------------------------------------------- /src/cid/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from cid.locals import generate_new_cid 4 | from cid.locals import get_cid 5 | from cid.locals import set_cid 6 | 7 | 8 | class CidMiddleware: 9 | """ 10 | Middleware class to extract the correlation id from incoming headers 11 | and add them to outgoing headers 12 | """ 13 | 14 | def __init__(self, get_response): 15 | self.get_response = get_response 16 | self.cid_request_header = getattr( 17 | settings, 'CID_HEADER', 'X_CORRELATION_ID' 18 | ) 19 | self.cid_response_header = getattr( 20 | settings, 'CID_RESPONSE_HEADER', self.cid_request_header 21 | ) 22 | 23 | def _process_request(self, request): 24 | upstream_cid = request.META.get(self.cid_request_header, None) 25 | cid = generate_new_cid(upstream_cid) 26 | request.correlation_id = cid 27 | set_cid(cid) 28 | return request 29 | 30 | def _process_response(self, response): 31 | cid = get_cid() 32 | if cid and self.cid_response_header: 33 | response[self.cid_response_header] = cid 34 | return response 35 | 36 | def __call__(self, request): 37 | request = self._process_request(request) 38 | response = self.get_response(request) 39 | return self._process_response(response) 40 | -------------------------------------------------------------------------------- /src/cid/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/src/cid/models.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polyconseil/django-cid/699bde72a812c854996f90861eb013752412ef78/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_context_processors.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from cid.context_processors import cid_context_processor 6 | 7 | 8 | class TestContextProcessor(TestCase): 9 | 10 | @mock.patch('cid.context_processors.get_cid') 11 | def test_cid_added(self, get_cid): 12 | get_cid.return_value = 'a-text-cid' 13 | self.assertEqual( 14 | {'correlation_id': 'a-text-cid'}, 15 | cid_context_processor(mock.Mock()) 16 | ) 17 | 18 | @mock.patch('cid.context_processors.get_cid') 19 | def test_cid_not_added_if_not_present(self, get_cid): 20 | get_cid.return_value = None 21 | self.assertEqual({}, cid_context_processor(mock.Mock())) 22 | -------------------------------------------------------------------------------- /tests/test_cursor.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | from django.test.utils import override_settings 5 | 6 | from cid.cursor import CidCursorWrapper 7 | 8 | 9 | class TestCidCursor(TestCase): 10 | 11 | def setUp(self): 12 | self.cursor = mock.Mock() 13 | self.cursor.execute = mock.Mock(return_value=None) 14 | self.cursor.executemany = mock.Mock(return_value=None) 15 | self.cursor_wrapper = CidCursorWrapper(self.cursor) 16 | 17 | @mock.patch('cid.cursor.get_cid') 18 | def test_adds_comment(self, get_cid): 19 | get_cid.return_value = 'testing-cursor' 20 | expected = "/* cid: testing-cursor */\nSELECT 1;" 21 | self.assertEqual( 22 | expected, 23 | self.cursor_wrapper.add_comment("SELECT 1;") 24 | ) 25 | 26 | @override_settings(CID_SQL_COMMENT_TEMPLATE='correlation_id={cid}') 27 | @mock.patch('cid.cursor.get_cid') 28 | def test_adds_comment_setting_overriden(self, get_cid): 29 | get_cid.return_value = 'testing-cursor' 30 | expected = "/* correlation_id=testing-cursor */\nSELECT 1;" 31 | self.assertEqual( 32 | expected, 33 | self.cursor_wrapper.add_comment("SELECT 1;") 34 | ) 35 | 36 | @override_settings(CID_SQL_STATEMENT_TEMPLATE='{sql}\n/* {cid} */') 37 | @mock.patch('cid.cursor.get_cid') 38 | def test_adds_comment_with_statement_template_setting_overriden(self, get_cid): 39 | get_cid.return_value = 'testing-cursor-after-sql-statement' 40 | expected = "SELECT 1;\n/* cid: testing-cursor-after-sql-statement */" 41 | self.assertEqual( 42 | expected, 43 | self.cursor_wrapper.add_comment("SELECT 1;") 44 | ) 45 | 46 | @mock.patch('cid.cursor.get_cid') 47 | def test_no_comment_when_cid_is_none(self, get_cid): 48 | get_cid.return_value = None 49 | expected = "SELECT 1;" 50 | self.assertEqual( 51 | expected, 52 | self.cursor_wrapper.add_comment("SELECT 1;") 53 | ) 54 | 55 | @mock.patch('cid.cursor.CidCursorWrapper.add_comment') 56 | def test_execute_calls_add_comment(self, add_comment): 57 | sql = "SELECT 1;" 58 | self.cursor_wrapper.execute(sql) 59 | add_comment.assert_called_with(sql) 60 | 61 | @mock.patch('cid.cursor.CidCursorWrapper.add_comment') 62 | def test_executemany_calls_add_comment(self, add_comment): 63 | sql = "SELECT 1;" 64 | self.cursor_wrapper.executemany(sql, []) 65 | add_comment.assert_called_with(sql) 66 | 67 | @mock.patch('cid.cursor.get_cid') 68 | def test_escape_cid(self, get_cid): 69 | get_cid.return_value = '/* a correlation id with funny characters */' 70 | expected = '/* cid: \\/\\* a correlation id with funny characters \\*\\/ */\nSELECT 1;' 71 | self.assertEqual(self.cursor_wrapper.add_comment('SELECT 1;'), expected) 72 | -------------------------------------------------------------------------------- /tests/test_locals.py: -------------------------------------------------------------------------------- 1 | from threading import local 2 | 3 | from django.test import TestCase 4 | from django.test import override_settings 5 | 6 | from cid.locals import generate_new_cid 7 | from cid.locals import get_cid 8 | from cid.locals import set_cid 9 | 10 | 11 | _thread_locals = local() 12 | 13 | 14 | class TestCidStorage(TestCase): 15 | 16 | def setUp(self): 17 | self.clear_cid() 18 | self.cid = 'test-cid' 19 | 20 | def tearDown(self): 21 | self.clear_cid() 22 | 23 | def clear_cid(self): 24 | try: 25 | delattr(_thread_locals, 'CID') 26 | except AttributeError: 27 | pass 28 | 29 | def test_get_empty_cid(self): 30 | self.assertIsNone(get_cid()) 31 | 32 | def test_set_cid(self): 33 | self.assertIsNone(get_cid()) 34 | set_cid(self.cid) 35 | self.assertEqual(self.cid, get_cid()) 36 | 37 | @override_settings(CID_GENERATE=True, CID_GENERATOR=lambda: 'constant_correlation') 38 | def test_custom_generator(self): 39 | assert generate_new_cid() == 'constant_correlation' 40 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | from django.test.utils import override_settings 5 | from django.urls import reverse 6 | 7 | import cid.locals 8 | from cid.middleware import CidMiddleware 9 | 10 | 11 | class DummyResponse: 12 | def __init__(self, request): 13 | self.headers = {} 14 | self.request = request 15 | def __getitem__(self, header): 16 | return self.headers[header] 17 | def __setitem__(self, header, value): 18 | self.headers[header] = value 19 | 20 | 21 | def get_response(request): 22 | return DummyResponse(request) 23 | 24 | 25 | def make_request(cid_=None, header_name='X_CORRELATION_ID'): 26 | request = mock.Mock() 27 | request.META = {} 28 | if cid_: 29 | request.META[header_name] = cid_ 30 | return request 31 | 32 | 33 | class TestCidMiddleware(TestCase): 34 | 35 | def setUp(self): 36 | super().setUp() 37 | cid.locals.set_cid(None) # don't leak cid between each test 38 | 39 | def test_with_cid_from_upstream(self): 40 | request = make_request('cid-from-upstream') 41 | middleware = CidMiddleware(get_response=get_response) 42 | response = middleware(request) 43 | self.assertEqual(request.correlation_id, 'cid-from-upstream') 44 | self.assertEqual(cid.locals.get_cid(), 'cid-from-upstream') 45 | self.assertEqual(response['X_CORRELATION_ID'], 'cid-from-upstream') 46 | 47 | def test_no_cid_from_upstream(self): 48 | request = make_request(cid_=None) 49 | middleware = CidMiddleware(get_response=get_response) 50 | response = middleware(request) 51 | self.assertIsNone(request.correlation_id, None) 52 | self.assertIsNone(cid.locals.get_cid(), None) 53 | self.assertEqual(response.headers, {}) 54 | 55 | @override_settings(CID_GENERATE=True) 56 | @mock.patch('uuid.uuid4') 57 | def test_generate_cid(self, uuid4): 58 | uuid4.return_value = 'generated-cid' 59 | request = make_request(cid_=None) 60 | middleware = CidMiddleware(get_response=get_response) 61 | response = middleware(request) 62 | self.assertEqual(request.correlation_id, 'generated-cid') 63 | self.assertEqual(cid.locals.get_cid(), 'generated-cid') 64 | self.assertEqual(response['X_CORRELATION_ID'], 'generated-cid') 65 | 66 | @override_settings(CID_GENERATE=True, CID_CONCATENATE_IDS=True) 67 | @mock.patch('uuid.uuid4') 68 | def test_concatenate_ids(self, uuid4): 69 | uuid4.return_value = 'local-cid' 70 | request = make_request(cid_='upstream-cid') 71 | middleware = CidMiddleware(get_response=get_response) 72 | response = middleware(request) 73 | self.assertEqual(request.correlation_id, 'upstream-cid, local-cid') 74 | self.assertEqual(cid.locals.get_cid(), 'upstream-cid, local-cid') 75 | self.assertEqual(response['X_CORRELATION_ID'], 'upstream-cid, local-cid') 76 | 77 | @override_settings(CID_HEADER='X_CUSTOM_HEADER') 78 | def test_custom_request_header(self): 79 | request = make_request('cid-from-upstream', header_name='X_CUSTOM_HEADER') 80 | middleware = CidMiddleware(get_response=get_response) 81 | response = middleware(request) 82 | self.assertEqual(request.correlation_id, 'cid-from-upstream') 83 | self.assertEqual(cid.locals.get_cid(), 'cid-from-upstream') 84 | self.assertEqual(response['X_CUSTOM_HEADER'], 'cid-from-upstream') 85 | 86 | @override_settings(CID_RESPONSE_HEADER='X_CUSTOM_HEADER') 87 | def test_custom_response_header(self): 88 | request = make_request('cid-from-upstream') 89 | middleware = CidMiddleware(get_response=get_response) 90 | response = middleware(request) 91 | self.assertEqual(request.correlation_id, 'cid-from-upstream') 92 | self.assertEqual(cid.locals.get_cid(), 'cid-from-upstream') 93 | self.assertEqual(response['X_CUSTOM_HEADER'], 'cid-from-upstream') 94 | 95 | @override_settings(CID_RESPONSE_HEADER=None) 96 | def test_no_response_header(self): 97 | request = make_request('cid-from-upstream') 98 | middleware = CidMiddleware(get_response=get_response) 99 | response = middleware(request) 100 | self.assertEqual(request.correlation_id, 'cid-from-upstream') 101 | self.assertEqual(cid.locals.get_cid(), 'cid-from-upstream') 102 | self.assertEqual(response.headers, {}) 103 | 104 | 105 | class TestIntegration(TestCase): 106 | 107 | @override_settings( 108 | MIDDLEWARE=('cid.middleware.CidMiddleware', ), 109 | CID_GENERATE=True, 110 | ) 111 | def test_integration(self): 112 | url = reverse('ok') # comes from sandbox/testapp 113 | 114 | # A request without any correlation id 115 | response = self.client.get(url) 116 | cid_ = response.get('X_CORRELATION_ID') 117 | self.assertIsNotNone(cid_) 118 | 119 | # A request *with* a correlation id 120 | response = self.client.get(url, X_CORRELATION_ID=cid_) 121 | self.assertEqual(response['X_CORRELATION_ID'], cid_) 122 | --------------------------------------------------------------------------------