├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── black.yml │ └── pr-checks.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── django_datadog_logger ├── __init__.py ├── celery.py ├── encoders.py ├── formatters │ ├── __init__.py │ └── datadog.py ├── middleware │ ├── __init__.py │ ├── error_log.py │ ├── request_id.py │ └── request_log.py ├── recursion.py ├── rest_framework │ ├── __init__.py │ └── mixins.py └── wsgi.py ├── docs ├── Makefile ├── authors.md ├── conf.py ├── contributing.md ├── history.md ├── index.rst ├── installation.rst ├── make.bat ├── readme.md ├── requirements.txt └── usage.rst ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── settings.py └── test_django_datadog_logger.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Django DataDog Logger version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Black 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: psf/black@stable 11 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: CI Checks 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | needs: test 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python 3.11 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: 3.11 25 | - name: Install dependencies 26 | run: | 27 | pip install --upgrade pip setuptools wheel 28 | pip install .[build] 29 | - name: Build 30 | run: | 31 | make dist 32 | test: 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] 37 | django-version: [ "4", "5" ] 38 | exclude: 39 | - python-version: "3.7" 40 | django-version: "4" 41 | - python-version: "3.7" 42 | django-version: "5" 43 | - python-version: "3.8" 44 | django-version: "5" 45 | - python-version: "3.9" 46 | django-version: "5" 47 | - python-version: "3.11" 48 | django-version: "3" 49 | - python-version: "3.12" 50 | django-version: "3" 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v3 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | - name: Install dependencies 58 | run: | 59 | pip install --upgrade pip setuptools wheel 60 | pip install Django==${{ matrix.django-version }} 61 | pip install -r requirements_dev.txt 62 | - name: Lint 63 | run: | 64 | make lint 65 | - name: Test 66 | run: | 67 | make test 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE settings 105 | .vscode/ 106 | .idea/ 107 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt 18 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Development Lead 4 | 5 | - Lenno Nagel \<\> 6 | 7 | ## Contributors 8 | 9 | None yet. Why not be the first? 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every 4 | little bit helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at https://github.com/namespace-ee/django-datadog-logger/issues 13 | 14 | If you are reporting a bug, please include: 15 | 16 | - Your operating system name and version. 17 | - Any details about your local setup that might be helpful in 18 | troubleshooting. 19 | - Detailed steps to reproduce the bug. 20 | 21 | ### Fix Bugs 22 | 23 | Look through the GitHub issues for bugs. Anything tagged with "bug" and 24 | "help wanted" is open to whoever wants to implement it. 25 | 26 | ### Implement Features 27 | 28 | Look through the GitHub issues for features. Anything tagged with 29 | "enhancement" and "help wanted" is open to whoever wants to implement 30 | it. 31 | 32 | ### Write Documentation 33 | 34 | Django DataDog Logger could always use more documentation, whether as 35 | part of the official Django DataDog Logger docs, in docstrings, or even 36 | on the web in blog posts, articles, and such. 37 | 38 | ### Submit Feedback 39 | 40 | The best way to send feedback is to file an issue at https://github.com/namespace-ee/django-datadog-logger/issues 41 | 42 | If you are proposing a feature: 43 | 44 | - Explain in detail how it would work. 45 | - Keep the scope as narrow as possible, to make it easier to 46 | implement. 47 | - Remember that this is a volunteer-driven project, and that 48 | contributions are welcome :) 49 | 50 | ## Get Started! 51 | 52 | Ready to contribute? Here's how to set up django-datadog-logger for local development. 53 | 54 | 1. Fork the repo on GitHub. 55 | 56 | 2. Clone your fork locally: 57 | 58 | ``` shell 59 | $ git clone git@github.com:your_name_here/django-datadog-logger.git 60 | ``` 61 | 62 | 3. Install your local copy into a virtualenv. Assuming you have 63 | virtualenvwrapper installed, this is how you set up your fork for 64 | local development: 65 | 66 | ``` shell 67 | $ mkvirtualenv django-datadog-logger 68 | $ cd django-datadog-logger/ 69 | $ python setup.py develop 70 | ``` 71 | 72 | 4. Create a branch for local development: 73 | 74 | ``` shell 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | ``` 77 | 78 | Now you can make your changes locally. 79 | 80 | 5. When you're done making changes, check that your changes pass flake8 81 | and the tests, including testing other Python versions with tox: 82 | 83 | ``` shell 84 | $ flake8 django_datadog_logger tests 85 | $ make test 86 | $ tox 87 | ``` 88 | 89 | To get flake8 and tox, just pip install them into your virtualenv. 90 | 91 | 6. Commit your changes and push your branch to GitHub: 92 | 93 | ``` shell 94 | $ git add . 95 | $ git commit -m "Your detailed description of your changes." 96 | $ git push origin name-of-your-bugfix-or-feature 97 | ``` 98 | 99 | 7. Submit a pull request through the GitHub website. 100 | 101 | ## Pull Request Guidelines 102 | 103 | Before you submit a pull request, check that it meets these guidelines: 104 | 105 | 1. The pull request should include tests. 106 | 2. If the pull request adds functionality, the docs should be updated. 107 | Put your new functionality into a function with a docstring, and add 108 | the feature to the list in README.md. 109 | 3. The pull request should work for Python 3.6, 3.7 and 3.8, and for 110 | PyPy. Check 111 | and 112 | make sure that the tests pass for all supported Python versions. 113 | 114 | ## Deploying 115 | 116 | A reminder for the maintainers on how to deploy. Make sure all your 117 | changes are committed (including an entry in HISTORY.md). Then run: 118 | 119 | ``` shell 120 | $ bump2version patch # possible: major / minor / patch 121 | $ git push 122 | $ git push --tags 123 | ``` 124 | 125 | GitHub Actions will then deploy to PyPI if tests pass. 126 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 0.6.2 (2023-04-27) 4 | 5 | - Fixed case where accessing request.auth may raise errors 6 | 7 | ## 0.6.1 (2023-04-27) 8 | 9 | - Removed dependency on Celery package, fixed import error 10 | 11 | ## 0.6.0 (2023-04-27) 12 | 13 | - Removed dependency on Celery package 14 | 15 | ## 0.5.6 (2023-01-17) 16 | 17 | - Datadog formatter: avoid recursion loop when accessing WSGI request 18 | auth attribute 19 | 20 | ## 0.5.5 (2023-01-16) 21 | 22 | - Improved support for request version reporting in http.request_version 24 | - Add support for http.url_details.view_name 26 | 27 | ## 0.5.4 (2023-01-16) 28 | 29 | - Added support for HTTP Accept header as http.accept 31 | 32 | ## 0.5.3 (2022-12-19) 33 | 34 | - Added support for JWT cid claim 35 | 36 | ## 0.5.2 (2022-11-23) 37 | 38 | - Fixed: don't let the logger throw a DisallowedHost error 39 | 40 | ## 0.5.1 (2022-08-09) 41 | 42 | - Fixed: ActionLoginMixin class methods perform_create and perform_update call super(). Remove atomic transaction 46 | 47 | ## 0.5.0 (2021-10-20) 48 | 49 | - Added support for Celery v5+ 50 | 51 | ## 0.4.0 (2021-08-27) 52 | 53 | - Enhancement: Updated formatting in README.rst \#5 54 | - Enhancement: Extract and add dd.\* attributes from log record to log 55 | entry dict \#6 56 | - Fixed: KeyError because a dict appears where a list is expected \#7 57 | 58 | ## 0.3.5 (2021-06-14) 59 | 60 | - Prevent recursion when warnings are logged whilst accessing WSGI 61 | request.user 62 | 63 | ## 0.3.4 (2021-06-14) 64 | 65 | - Fixed import error for future package 66 | 67 | ## 0.3.3 (2020-11-04) 68 | 69 | - Added support for incoming HTTP X-Request-ID header values 70 | 71 | ## 0.3.2 (2020-04-24) 72 | 73 | - Respect User.USERNAME_FIELD 74 | 75 | ## 0.3.1 (2020-04-24) 76 | 77 | - Removed API_LOG_REQUEST_DURATION_WARN_SECONDS 78 | 79 | ## 0.3.0 (2020-04-15) 80 | 81 | - Improved Celery task received messages logging. 82 | - Removed RequestIdFilter (not needed anymore). 83 | 84 | ## 0.2.0 (2020-04-14) 85 | 86 | - Added Celery request local. 87 | 88 | ## 0.1.0 (2020-02-17) 89 | 90 | - First release on PyPI. 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Lenno Nagel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.md 2 | include CONTRIBUTING.md 3 | include HISTORY.md 4 | include LICENSE 5 | include README.md 6 | include requirements.txt 7 | 8 | recursive-include tests * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | 12 | recursive-include docs *.md conf.py Makefile make.bat *.jpg *.png *.gif 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint: ## check style with flake8 51 | flake8 django_datadog_logger tests 52 | 53 | test: ## run tests quickly with the default Python 54 | DJANGO_SETTINGS_MODULE=tests.settings python -m unittest discover 55 | 56 | test-all: ## run tests on every Python version with tox 57 | tox 58 | 59 | coverage: ## check code coverage quickly with the default Python 60 | DJANGO_SETTINGS_MODULE=tests.settings coverage run --source django_datadog_logger setup.py test 61 | coverage report -m 62 | coverage html 63 | $(BROWSER) htmlcov/index.html 64 | 65 | docs: ## generate Sphinx HTML documentation, including API docs 66 | rm -f docs/django_datadog_logger.rst 67 | rm -f docs/modules.rst 68 | sphinx-apidoc -o docs/ django_datadog_logger 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs html 71 | $(BROWSER) docs/_build/html/index.html 72 | 73 | servedocs: docs ## compile the docs watching for changes 74 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 75 | 76 | release: dist ## package and upload a release 77 | twine upload dist/* 78 | 79 | dist: clean ## builds source and wheel package 80 | python setup.py sdist 81 | python setup.py bdist_wheel 82 | ls -l dist 83 | 84 | install: clean ## install the package to the active Python's site-packages 85 | python setup.py install 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django DataDog Logger 2 | 3 | [![image](https://img.shields.io/pypi/v/django-datadog-logger.svg)](https://pypi.python.org/pypi/django-datadog-logger) 4 | [![CI Checks](https://github.com/namespace-ee/django-datadog-logger/actions/workflows/pr-checks.yml/badge.svg)](https://github.com/namespace-ee/django-datadog-logger/actions/workflows/pr-checks.yml) 5 | [![Black](https://github.com/namespace-ee/django-datadog-logger/actions/workflows/black.yml/badge.svg)](https://github.com/namespace-ee/django-datadog-logger/actions/workflows/black.yml) 6 | [![Documentation Status](https://readthedocs.org/projects/django-datadog-logger/badge/?version=latest)](https://django-datadog-logger.readthedocs.io/en/latest/?badge=latest) 7 | 8 | Django DataDog Logger integration package. 9 | 10 | - Free software: MIT license 11 | - Documentation: . 12 | 13 | ## Quick start 14 | 15 | Set up request id tracking (in front) and logging middlewares (at the 16 | end): 17 | 18 | ``` python 19 | MIDDLEWARE = [ 20 | "django_datadog_logger.middleware.request_id.RequestIdMiddleware", 21 | # ... 22 | "django_datadog_logger.middleware.error_log.ErrorLoggingMiddleware", 23 | "django_datadog_logger.middleware.request_log.RequestLoggingMiddleware", 24 | ] 25 | ``` 26 | 27 | Configure LOGGERS in your Django settings file: 28 | 29 | ``` python 30 | API_LOG_ROOT = env.str("API_LOG_ROOT") 31 | LOGGING = { 32 | "version": 1, 33 | "disable_existing_loggers": False, 34 | "formatters": { 35 | "console": {"format": "{levelname} {message}", "style": "{"}, 36 | "json": {"()": "django_datadog_logger.formatters.datadog.DataDogJSONFormatter"}, 37 | }, 38 | "handlers": { 39 | "console": {"level": "INFO", "class": "logging.StreamHandler", "formatter": "console"}, 40 | "application": { 41 | "level": API_LOG_APPLICATION_LEVEL, 42 | "class": "logging.FileHandler", 43 | "filename": os.path.join(API_LOG_ROOT, "api.application.log"), 44 | "formatter": "json", 45 | }, 46 | "state": { 47 | "level": API_LOG_STATE_LEVEL, 48 | "class": "logging.FileHandler", 49 | "filename": os.path.join(API_LOG_ROOT, "api.state.log"), 50 | "formatter": "json", 51 | }, 52 | "request": { 53 | "level": API_LOG_REQUEST_LEVEL, 54 | "class": "logging.FileHandler", 55 | "filename": os.path.join(API_LOG_ROOT, "api.request.log"), 56 | "formatter": "json", 57 | }, 58 | "session": { 59 | "level": API_LOG_SESSION_LEVEL, 60 | "class": "logging.FileHandler", 61 | "filename": os.path.join(API_LOG_ROOT, "api.session.log"), 62 | "formatter": "json", 63 | }, 64 | "error": { 65 | "level": API_LOG_ERROR_LEVEL, 66 | "class": "logging.FileHandler", 67 | "filename": os.path.join(API_LOG_ROOT, "api.error.log"), 68 | "formatter": "json", 69 | }, 70 | }, 71 | "loggers": { 72 | "": {"handlers": ["console", "error"], "level": "DEBUG", "propagate": True}, 73 | "ddtrace": {"handlers": ["error"], "level": "ERROR", "propagate": False}, 74 | "django.db.backends": {"handlers": ["error"], "level": "ERROR", "propagate": False}, 75 | "twilio": {"handlers": ["error"], "level": "ERROR", "propagate": False}, 76 | "my_project": {"handlers": ["application"], "level": "INFO", "propagate": False}, 77 | "my_project.throttling": {"handlers": ["application"], "level": "DEBUG", "propagate": False}, 78 | "my_project.vehicles.viewsets.state": {"handlers": ["state"], "level": "INFO", "propagate": False}, 79 | "my_project.accounts.session": {"handlers": ["session"], "level": "DEBUG", "propagate": False}, 80 | "my_project.session": {"handlers": ["session"], "level": "DEBUG", "propagate": False}, 81 | "django_auth_ldap": {"level": "DEBUG", "handlers": ["session"], "propagate": False}, 82 | "django_datadog_logger.middleware.error_log": {"handlers": ["error"], "level": "INFO", "propagate": False}, 83 | "django_datadog_logger.middleware.request_log": {"handlers": ["request"], "level": "INFO", "propagate": False}, 84 | "django_datadog_logger.rest_framework": {"handlers": ["application"], "level": "INFO", "propagate": False}, 85 | }, 86 | } 87 | ``` 88 | 89 | If you would like to whitelist your projects for passing extra arguments 90 | to the json log record, please set the following regular expression: 91 | 92 | ``` python 93 | DJANGO_DATADOG_LOGGER_EXTRA_INCLUDE = r"^(django_datadog_logger|my_project)(|\..+)$" 94 | ``` 95 | 96 | Add Celery logger configuration and request_id tracking decorator to 97 | tasks: 98 | 99 | ``` python 100 | import logging 101 | 102 | from celery import Celery, shared_task 103 | from celery.result import AsyncResult 104 | from celery.signals import after_setup_logger, after_setup_task_logger 105 | from django.conf import settings 106 | from django_datadog_logger.celery import store_celery_request 107 | 108 | logger = logging.getLogger(__name__) 109 | 110 | 111 | @after_setup_logger.connect 112 | def on_after_setup_logger(logger, *args, **kwargs): 113 | from django_datadog_logger.formatters.datadog import DataDogJSONFormatter 114 | 115 | if settings.API_LOG_CELERY_JSON: 116 | formatter = DataDogJSONFormatter() 117 | for handler in list(logger.handlers): 118 | handler.setFormatter(formatter) 119 | handler.setLevel(settings.API_LOG_CELERY_LEVEL) 120 | 121 | 122 | @after_setup_task_logger.connect 123 | def on_after_setup_task_logger(logger, *args, **kwargs): 124 | from django_datadog_logger.formatters.datadog import DataDogJSONFormatter 125 | 126 | if settings.API_LOG_CELERY_JSON: 127 | formatter = DataDogJSONFormatter() 128 | for handler in list(logger.handlers): 129 | handler.setFormatter(formatter) 130 | handler.setLevel(settings.API_LOG_CELERY_LEVEL) 131 | 132 | 133 | app = Celery("my_project") 134 | 135 | # Using a string here means the worker will not have to 136 | # pickle the object when using Windows. 137 | app.config_from_object("django.conf:settings", namespace="CELERY") 138 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 139 | 140 | 141 | @shared_task(bind=True) 142 | @store_celery_request 143 | def debug_task(self): 144 | print("Request: {0!r}".format(self.request)) 145 | logger.critical("CRITICAL", extra={"level": "CRITICAL"}) 146 | logger.error("ERROR", extra={"level": "ERROR"}) 147 | logger.warning("WARNING", extra={"level": "WARNING"}) 148 | logger.info("INFO", extra={"level": "INFO"}) 149 | logger.debug("DEBUG", extra={"level": "DEBUG"}) 150 | return 42 151 | ``` 152 | 153 | ## ddtrace 154 | 155 | The ddtrace library has an option to inject tracing context data into 156 | log records: 157 | 158 | 159 | There is a helper to look for those attributes and add them 160 | automatically to the log entry created by this library. 161 | 162 | ``` python 163 | # log.py 164 | 165 | # Patch logging library to inject dd.* attributes on log records 166 | import ddtrace 167 | ddtrace.patch(logging=True) 168 | 169 | # Configure logger with DataDogJSONFormatter 170 | import logging 171 | from django_datadog_logger.formatters.datadog import DataDogJSONFormatter 172 | 173 | logger = logging.root 174 | 175 | handler = logging.StreamHandler() 176 | handler.formatter = DataDogJSONFormatter() 177 | logger.addHandler(handler) 178 | logger.setLevel(logging.INFO) 179 | 180 | 181 | # Log a test message 182 | logger.info("test") 183 | ``` 184 | 185 | ``` bash 186 | $ DD_SERVICE=django DD_ENV=test DD_VERSION=1234 python log.py 187 | {"message": "test", "logger.name": "root", "logger.thread_name": "MainThread", "logger.method_name": "", "syslog.timestamp": "2021-08-23T18:26:10.391099+00:00", "syslog.severity": "INFO", "dd.version": "1234", "dd.env": "test", "dd.service": "django", "dd.trace_id": "0", "dd.span_id": "0"} 188 | ``` 189 | 190 | If you remove the call to datadog.patch(logging=True) you end up with: 192 | 193 | ``` bash 194 | $ python test.py 195 | {"message": "test", "logger.name": "root", "logger.thread_name": "MainThread", "logger.method_name": "", "syslog.timestamp": "2021-08-23T18:27:47.951461+00:00", "syslog.severity": "INFO"} 196 | ``` 197 | 198 | ## Credits 199 | 200 | This package was created with 201 | [Cookiecutter](https://github.com/audreyr/cookiecutter) and the 202 | [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) 203 | project template. 204 | -------------------------------------------------------------------------------- /django_datadog_logger/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.7.3" 2 | -------------------------------------------------------------------------------- /django_datadog_logger/celery.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from asgiref.local import Local # NOQA 4 | 5 | local = Local() 6 | 7 | 8 | def get_celery_request(): 9 | try: 10 | return local.request 11 | except AttributeError: 12 | return None 13 | 14 | 15 | def get_task_name(request): 16 | if hasattr(request, "task"): 17 | if isinstance(request.task, str): 18 | return request.task 19 | elif hasattr(request.task, "name"): 20 | return request.task.name 21 | return None 22 | 23 | 24 | def store_celery_request(func): 25 | @wraps(func) 26 | def function_wrapper(*args, **kwargs): 27 | try: 28 | if args and hasattr(args[0], "request"): 29 | request = args[0].request 30 | if (type(request).__module__, type(request).__name__) == ("celery.app.task", "Context"): 31 | local.request = request 32 | return func(*args, **kwargs) 33 | finally: 34 | if hasattr(local, "request"): 35 | del local.request 36 | 37 | return function_wrapper 38 | 39 | 40 | __all__ = ["local", "get_celery_request", "store_celery_request"] 41 | -------------------------------------------------------------------------------- /django_datadog_logger/encoders.py: -------------------------------------------------------------------------------- 1 | from django.core.serializers.json import DjangoJSONEncoder 2 | 3 | 4 | class SafeJsonEncoder(DjangoJSONEncoder): 5 | def default(self, o): 6 | try: 7 | return super(SafeJsonEncoder, self).default(o) 8 | except TypeError as e: 9 | return str(e) 10 | -------------------------------------------------------------------------------- /django_datadog_logger/formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namespace-ee/django-datadog-logger/187a2f9f1f9932b03cd9598b7aff65fc09ab87b8/django_datadog_logger/formatters/__init__.py -------------------------------------------------------------------------------- /django_datadog_logger/formatters/datadog.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import traceback 4 | import typing 5 | from logging import LogRecord 6 | 7 | try: 8 | import zoneinfo 9 | except ImportError: 10 | from backports import zoneinfo 11 | 12 | import json_log_formatter 13 | from django.conf import settings 14 | from django.core.exceptions import DisallowedHost 15 | from django.http.request import split_domain_port, HttpRequest 16 | from django.urls import resolve, NoReverseMatch, Resolver404 17 | from rest_framework.compat import unicode_http_header 18 | 19 | import django_datadog_logger.celery 20 | import django_datadog_logger.wsgi 21 | from django_datadog_logger.celery import get_task_name, get_celery_request 22 | from django_datadog_logger.encoders import SafeJsonEncoder 23 | from django_datadog_logger.recursion import not_recursive 24 | 25 | # those fields are excluded from extra dict 26 | # and remains acceptable in record 27 | 28 | EXCLUDE_FROM_EXTRA_ATTRS = { 29 | "user", 30 | "auth", 31 | "username", 32 | "request_id", 33 | "client_ip", 34 | "request", 35 | "celery_request", 36 | "wsgi_request", 37 | "params", 38 | "sql", 39 | } 40 | 41 | 42 | def get_client_ip(request): 43 | x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") 44 | if x_forwarded_for: 45 | return x_forwarded_for.split(",")[0] or None 46 | else: 47 | return request.META.get("REMOTE_ADDR") or None 48 | 49 | 50 | def determine_version(request): 51 | if hasattr(request, "version"): 52 | if request.version is not None: 53 | return str(request.version) 54 | elif hasattr(request, "accepted_types"): 55 | for t in request.accepted_types: 56 | if t.params.get("version") is not None: 57 | return unicode_http_header(t.params.get("version")) 58 | return None 59 | 60 | 61 | @not_recursive 62 | def get_wsgi_request_auth(wsgi_request): 63 | try: 64 | if getattr(wsgi_request, "auth", None) is not None and isinstance(wsgi_request.auth, dict): 65 | return wsgi_request.auth 66 | except Exception: # NOQA 67 | return None 68 | 69 | 70 | @not_recursive 71 | def get_wsgi_request_user(wsgi_request): 72 | if getattr(wsgi_request, "user", None) is not None: 73 | if getattr(wsgi_request.user, "is_authenticated", False): 74 | return wsgi_request.user 75 | 76 | 77 | class DataDogJSONFormatter(json_log_formatter.JSONFormatter): 78 | def json_record(self, message: str, extra: typing.Dict, record: LogRecord) -> typing.Dict: 79 | log_entry_dict = { 80 | "message": message, 81 | "logger.name": record.name, 82 | "logger.thread_name": record.threadName, 83 | "logger.method_name": record.funcName, 84 | "date": (datetime.datetime.fromtimestamp(record.created, tz=zoneinfo.ZoneInfo("UTC")).isoformat()), 85 | "status": record.levelname, 86 | } 87 | 88 | # Add special `dd.` log record attributes added by `ddtrace` library 89 | # For example: dd.trace_id, dd.span_id, dd.service, dd.env, dd.version, etc 90 | log_entry_dict.update(self.get_datadog_attributes(record)) 91 | 92 | wsgi_request = self.get_wsgi_request() 93 | if wsgi_request is not None: 94 | log_entry_dict["network.client.ip"] = get_client_ip(wsgi_request) 95 | 96 | try: 97 | domain, port = split_domain_port(wsgi_request.get_host()) 98 | except DisallowedHost: 99 | domain, port = None, None 100 | 101 | try: 102 | resolver_match = resolve(wsgi_request.path) 103 | except (NoReverseMatch, Resolver404): 104 | resolver_match = None 105 | 106 | log_entry_dict["http.url"] = wsgi_request.get_full_path() 107 | log_entry_dict["http.url_details.host"] = domain 108 | log_entry_dict["http.url_details.port"] = int(port) if port else None 109 | log_entry_dict["http.url_details.path"] = wsgi_request.path_info 110 | log_entry_dict["http.url_details.queryString"] = wsgi_request.GET.dict() 111 | log_entry_dict["http.url_details.scheme"] = wsgi_request.scheme 112 | log_entry_dict["http.url_details.view_name"] = resolver_match.view_name if resolver_match else None 113 | log_entry_dict["http.method"] = wsgi_request.method 114 | log_entry_dict["http.accept"] = wsgi_request.META.get("HTTP_ACCEPT") 115 | log_entry_dict["http.referer"] = wsgi_request.META.get("HTTP_REFERER") 116 | log_entry_dict["http.useragent"] = wsgi_request.META.get("HTTP_USER_AGENT") 117 | log_entry_dict["http.request_version"] = determine_version(wsgi_request) 118 | 119 | if hasattr(wsgi_request, "request_id"): 120 | log_entry_dict["http.request_id"] = wsgi_request.request_id 121 | 122 | try: 123 | auth = get_wsgi_request_auth(wsgi_request) 124 | except: # NOQA: we cannot crash the log formatter under any circumstances 125 | auth = None 126 | 127 | if auth: 128 | if "sid" in auth: 129 | log_entry_dict["usr.session_id"] = auth["sid"] 130 | if "cid" in auth: 131 | log_entry_dict["usr.client_id"] = auth["cid"] 132 | 133 | try: 134 | user = get_wsgi_request_user(wsgi_request) 135 | except: # NOQA: we cannot crash the log formatter under any circumstances 136 | user = None 137 | 138 | if user: 139 | log_entry_dict["usr.id"] = getattr(user, "pk", None) 140 | log_entry_dict["usr.name"] = getattr(user, getattr(user, "USERNAME_FIELD", "username"), None) 141 | log_entry_dict["usr.email"] = getattr(user, "email", None) 142 | 143 | if getattr(wsgi_request, "session", None) is not None and getattr(wsgi_request.session, "session_key"): 144 | log_entry_dict["usr.session_key"] = wsgi_request.session.session_key 145 | 146 | if record.exc_info: 147 | if hasattr(record, "status_code"): 148 | log_entry_dict["error.kind"] = record.status_code 149 | log_entry_dict["error.message"] = record.msg 150 | elif record.exc_info[0] is not None: 151 | log_entry_dict["error.kind"] = record.exc_info[0].__name__ 152 | for msg in traceback.format_exception_only(record.exc_info[0], record.exc_info[1]): 153 | log_entry_dict["error.message"] = msg.strip() 154 | log_entry_dict["error.stack"] = self.formatException(record.exc_info) 155 | 156 | if hasattr(record, "duration"): 157 | log_entry_dict["duration"] = record.duration 158 | 159 | if hasattr(record, "sql"): 160 | log_entry_dict["db.statement"] = record.sql 161 | 162 | celery_request = get_celery_request() 163 | if celery_request is not None: 164 | log_entry_dict["celery.request_id"] = celery_request.id 165 | log_entry_dict["celery.task_name"] = get_task_name(celery_request) 166 | elif record.name in {"celery.app.trace", "celery.worker.strategy"} and "data" in extra: 167 | if "id" in extra["data"]: 168 | log_entry_dict["celery.request_id"] = extra["data"]["id"] 169 | if "name" in extra["data"]: 170 | log_entry_dict["celery.task_name"] = extra["data"]["name"] 171 | if "runtime" in extra["data"]: 172 | log_entry_dict["duration"] = extra["data"]["runtime"] * 1000000000 173 | 174 | if hasattr(settings, "DJANGO_DATADOG_LOGGER_EXTRA_INCLUDE"): 175 | if re.match(getattr(settings, "DJANGO_DATADOG_LOGGER_EXTRA_INCLUDE"), record.name): 176 | log_entry_dict.update(extra) 177 | 178 | return log_entry_dict 179 | 180 | def get_datadog_attributes(self, record: LogRecord) -> typing.Dict: 181 | """Helper to extract dd.* attributes from the log record.""" 182 | return {attr_name: record.__dict__[attr_name] for attr_name in record.__dict__ if attr_name.startswith("dd.")} 183 | 184 | def get_wsgi_request(self) -> typing.Optional[HttpRequest]: 185 | return django_datadog_logger.wsgi.get_wsgi_request() 186 | 187 | def to_json(self, record: typing.Dict) -> str: 188 | """Converts record dict to a JSON string.""" 189 | return self.json_lib.dumps(record, cls=SafeJsonEncoder) 190 | 191 | def extra_from_record(self, record: LogRecord) -> typing.Dict: 192 | """Returns `extra` dict you passed to logger. 193 | 194 | The `extra` keyword argument is used to populate the `__dict__` of 195 | the `LogRecord`. 196 | 197 | """ 198 | return { 199 | attr_name: record.__dict__[attr_name] 200 | for attr_name in record.__dict__ 201 | if attr_name not in json_log_formatter.BUILTIN_ATTRS.union(EXCLUDE_FROM_EXTRA_ATTRS) 202 | } 203 | -------------------------------------------------------------------------------- /django_datadog_logger/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namespace-ee/django-datadog-logger/187a2f9f1f9932b03cd9598b7aff65fc09ab87b8/django_datadog_logger/middleware/__init__.py -------------------------------------------------------------------------------- /django_datadog_logger/middleware/error_log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.exceptions import PermissionDenied, BadRequest, SuspiciousOperation 4 | from django.http import Http404 5 | from django.http.multipartparser import MultiPartParserError 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class ErrorLoggingMiddleware: 11 | def __init__(self, get_response=None): 12 | self.get_response = get_response 13 | 14 | def __call__(self, request): 15 | return self.get_response(request) 16 | 17 | def process_exception(self, request, exception): 18 | if isinstance( 19 | exception, 20 | (PermissionDenied, Http404, MultiPartParserError, BadRequest, SuspiciousOperation), 21 | ): 22 | return 23 | logger.exception(exception) 24 | 25 | def process_response(self, request, response): 26 | return response 27 | -------------------------------------------------------------------------------- /django_datadog_logger/middleware/request_id.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import uuid 4 | 5 | from django_datadog_logger.wsgi import local 6 | 7 | 8 | def generate_request_id(): 9 | return str(uuid.uuid4()) 10 | 11 | 12 | def get_or_create_request_id(request): 13 | request_id = request.META.get("HTTP_X_REQUEST_ID") 14 | if request_id and re.match("^[a-zA-Z0-9+/=-]{20,200}$", request_id): 15 | return request_id 16 | else: 17 | return generate_request_id() 18 | 19 | 20 | class RequestIdMiddleware: 21 | def __init__(self, get_response=None): 22 | self.get_response = get_response 23 | 24 | def __call__(self, request): 25 | request.request_id = get_or_create_request_id(request) 26 | request.request_start_time = time.time() 27 | local.request = request 28 | response = self.get_response(request) 29 | response["X-Request-ID"] = request.request_id 30 | if hasattr(local, "request"): 31 | del local.request 32 | return response 33 | -------------------------------------------------------------------------------- /django_datadog_logger/middleware/request_log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from rest_framework.utils.serializer_helpers import ReturnDict 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class RequestLoggingMiddleware: 10 | def __init__(self, get_response=None): 11 | self.get_response = get_response 12 | 13 | def __call__(self, request): 14 | response = self.get_response(request) 15 | self.log_response(request, response) 16 | return response 17 | 18 | def process_response(self, request, response): 19 | self.log_response(request, response) 20 | return response 21 | 22 | def log_response(self, request, response): 23 | log_entry_dict = {"http.status_code": response.status_code} 24 | 25 | if hasattr(request, "request_start_time"): 26 | duration_seconds = getattr(response, "response_start_time", time.time()) - request.request_start_time 27 | log_entry_dict["duration"] = duration_seconds * 1000000000.0 28 | 29 | if 400 <= response.status_code < 500: 30 | log_entry_dict["error.kind"] = response.status_code 31 | log_entry_dict["error.message"] = response.reason_phrase 32 | if hasattr(response, "data") and isinstance(response.data, (list, dict, ReturnDict)): 33 | log_entry_dict["error.stack"] = response.data 34 | logger.warning( 35 | f"HTTP {response.status_code} {response.reason_phrase}", 36 | extra=log_entry_dict, 37 | ) 38 | elif 500 <= response.status_code < 600: 39 | log_entry_dict["error.kind"] = response.status_code 40 | log_entry_dict["error.message"] = response.reason_phrase 41 | logger.error( 42 | f"HTTP {response.status_code} {response.reason_phrase}", 43 | extra=log_entry_dict, 44 | ) 45 | else: 46 | logger.info( 47 | f"HTTP {response.status_code} {response.reason_phrase}", 48 | extra=log_entry_dict, 49 | ) 50 | -------------------------------------------------------------------------------- /django_datadog_logger/recursion.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import wraps 3 | 4 | 5 | class RecursionDetected(RuntimeError): 6 | """function has been detected to be recursing""" 7 | 8 | 9 | def not_recursive(f): 10 | """ 11 | raise an exception if recursive 12 | """ 13 | 14 | @wraps(f) 15 | def wrapper(*args, **kwargs): 16 | for frame in inspect.stack(): 17 | if f.__name__ == frame.function: 18 | raise RecursionDetected(f"function '{f.__name__}' is recursive") 19 | 20 | return f(*args, **kwargs) 21 | 22 | return wrapper 23 | -------------------------------------------------------------------------------- /django_datadog_logger/rest_framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namespace-ee/django-datadog-logger/187a2f9f1f9932b03cd9598b7aff65fc09ab87b8/django_datadog_logger/rest_framework/__init__.py -------------------------------------------------------------------------------- /django_datadog_logger/rest_framework/mixins.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | 4 | from django.db import transaction 5 | from django.db.models import Manager, QuerySet 6 | from rest_framework.fields import ModelField 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class ActionLogMixin: 12 | @staticmethod 13 | def get_create_log_entity(serializer, instance, action): 14 | validated_data = copy.copy(serializer.validated_data) 15 | log_entity = {"pk": instance.pk} 16 | ModelClass = serializer.Meta.model 17 | 18 | for field_name, field in serializer.fields.items(): 19 | if field.source == "*": 20 | value = instance 21 | elif field.source in validated_data: 22 | value = validated_data.pop(field.source, None) 23 | else: 24 | value = getattr(instance, field.source, None) 25 | 26 | if isinstance(field, ModelField): 27 | log_entity[field_name] = field.to_representation(instance) 28 | elif isinstance(value, (Manager, QuerySet)): 29 | log_entity[field_name] = field.to_representation(value.all()) 30 | elif callable(value): 31 | log_entity[field_name] = field.to_representation(value()) 32 | elif value is not None: 33 | log_entity[field_name] = field.to_representation(value) 34 | else: 35 | log_entity[field_name] = None 36 | 37 | for attr, value in validated_data.items(): 38 | log_entity[attr] = repr(value) 39 | 40 | message = "{} {}".format(ModelClass.__name__, action) 41 | extra = {"{}.{}".format(ModelClass._meta.app_label, ModelClass.__name__): log_entity} 42 | return message, extra 43 | 44 | def perform_create(self, serializer): 45 | super().perform_create(serializer) 46 | message, extra = self.get_create_log_entity(serializer, serializer.instance, "created") 47 | logger.info(message, extra=extra) 48 | 49 | def perform_update(self, serializer): 50 | super().perform_update(serializer) 51 | message, extra = self.get_create_log_entity(serializer, serializer.instance, "updated") 52 | logger.info(message, extra=extra) 53 | 54 | def perform_destroy(self, instance): 55 | with transaction.atomic(): 56 | msg = {"pk": instance.pk} 57 | super().perform_destroy(instance) 58 | logger.info( 59 | "{} deleted".format(type(instance).__name__), 60 | extra={"{}.{}".format(instance._meta.app_config.label, type(instance).__name__): msg}, 61 | ) 62 | -------------------------------------------------------------------------------- /django_datadog_logger/wsgi.py: -------------------------------------------------------------------------------- 1 | from asgiref.local import Local # NOQA 2 | 3 | local = Local() 4 | 5 | 6 | def get_wsgi_request(): 7 | try: 8 | return local.request 9 | except AttributeError: 10 | return None 11 | 12 | 13 | __all__ = ["local", "get_wsgi_request"] 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = django_datadog_logger 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/authors.md: -------------------------------------------------------------------------------- 1 | ```{include} ../AUTHORS.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # django_datadog_logger documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | 23 | sys.path.insert(0, os.path.abspath("..")) 24 | 25 | import django_datadog_logger 26 | 27 | # -- General configuration --------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 35 | extensions = [ 36 | "myst_parser", 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.viewcode", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix(es) of source filenames. 45 | source_suffix = [".rst", ".md"] 46 | 47 | # The master toctree document. 48 | master_doc = "index" 49 | 50 | # General information about the project. 51 | project = "Django DataDog Logger" 52 | copyright = "2020, Lenno Nagel" 53 | author = "Lenno Nagel" 54 | 55 | # The version info for the project you're documenting, acts as replacement 56 | # for |version| and |release|, also used in various other places throughout 57 | # the built documents. 58 | # 59 | # The short X.Y version. 60 | version = django_datadog_logger.__version__ 61 | # The full version, including alpha/beta/rc tags. 62 | release = django_datadog_logger.__version__ 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This patterns also effect to html_static_path and html_extra_path 74 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = "sphinx" 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = False 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = "alabaster" 89 | 90 | # Theme options are theme-specific and customize the look and feel of a 91 | # theme further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ["_static"] 100 | 101 | 102 | # -- Options for HTMLHelp output --------------------------------------- 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = "django_datadog_loggerdoc" 106 | 107 | 108 | # -- Options for LaTeX output ------------------------------------------ 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | # Latex figure (float) alignment 121 | # 122 | # 'figure_align': 'htbp', 123 | } 124 | 125 | # Grouping the document tree into LaTeX files. List of tuples 126 | # (source start file, target name, title, author, documentclass 127 | # [howto, manual, or own class]). 128 | latex_documents = [ 129 | (master_doc, "django_datadog_logger.tex", "Django DataDog Logger Documentation", "Lenno Nagel", "manual"), 130 | ] 131 | 132 | 133 | # -- Options for manual page output ------------------------------------ 134 | 135 | # One entry per manual page. List of tuples 136 | # (source start file, name, description, authors, manual section). 137 | man_pages = [(master_doc, "django_datadog_logger", "Django DataDog Logger Documentation", [author], 1)] 138 | 139 | 140 | # -- Options for Texinfo output ---------------------------------------- 141 | 142 | # Grouping the document tree into Texinfo files. List of tuples 143 | # (source start file, target name, title, author, 144 | # dir menu entry, description, category) 145 | texinfo_documents = [ 146 | ( 147 | master_doc, 148 | "django_datadog_logger", 149 | "Django DataDog Logger Documentation", 150 | author, 151 | "django_datadog_logger", 152 | "One line description of project.", 153 | "Miscellaneous", 154 | ), 155 | ] 156 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CONTRIBUTING.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | ```{include} ../HISTORY.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Django DataDog Logger's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | modules 12 | contributing 13 | authors 14 | history 15 | 16 | Indices and tables 17 | ================== 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install Django DataDog Logger, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install django_datadog_logger 16 | 17 | This is the preferred method to install Django DataDog Logger, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for Django DataDog Logger can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/namespace-ee/django-datadog-logger 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OJL https://github.com/namespace-ee/django-datadog-logger/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/namespace-ee/django-datadog-logger 51 | .. _tarball: https://github.com/namespace-ee/django-datadog-logger/tarball/master 52 | -------------------------------------------------------------------------------- /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=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=django_datadog_logger 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=7,<8 2 | myst-parser>=2,<3 3 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use Django DataDog Logger in a project:: 6 | 7 | import django_datadog_logger 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ["py37", "py38"] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | | build 9 | | dist 10 | 11 | )/ 12 | ''' 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | JSON-log-formatter 2 | Django 3 | djangorestframework 4 | backports.zoneinfo;python_version<"3.9" 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -r docs/requirements.txt 3 | pip 4 | bump2version 5 | wheel 6 | watchdog 7 | flake8 8 | tox 9 | coverage 10 | twine 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.7.3 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:django_datadog_logger/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | max-line-length = 120 20 | 21 | [aliases] 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open("README.md") as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open("HISTORY.md") as history_file: 11 | history = history_file.read() 12 | 13 | requirements = [] 14 | with open("requirements.txt") as requirements_file: 15 | for line in requirements_file.readlines(): 16 | # skip to next iteration if comment or empty line 17 | if line.startswith("#") or line == "" or line.startswith("http") or line.startswith("git"): 18 | continue 19 | # add line to requirements 20 | requirements.append(line) 21 | 22 | setup_requirements = [] 23 | 24 | setup( 25 | author="Lenno Nagel", 26 | author_email="lenno@namespace.ee", 27 | python_requires=">=3.7", 28 | classifiers=[ 29 | "Development Status :: 2 - Pre-Alpha", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Natural Language :: English", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | "Programming Language :: Python :: 3.12", 40 | ], 41 | description="Django DataDog Logger integration package.", 42 | install_requires=requirements, 43 | license="MIT license", 44 | long_description=readme + "\n\n" + history, 45 | long_description_content_type="text/markdown", 46 | include_package_data=True, 47 | keywords="django_datadog_logger", 48 | name="django-datadog-logger", 49 | packages=find_packages(include=["django_datadog_logger", "django_datadog_logger.*"]), 50 | setup_requires=setup_requirements, 51 | url="https://github.com/namespace-ee/django-datadog-logger", 52 | version="0.7.3", 53 | zip_safe=False, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for django_datadog_logger.""" 2 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namespace-ee/django-datadog-logger/187a2f9f1f9932b03cd9598b7aff65fc09ab87b8/tests/settings.py -------------------------------------------------------------------------------- /tests/test_django_datadog_logger.py: -------------------------------------------------------------------------------- 1 | """Tests for `django_datadog_logger` package.""" 2 | 3 | import logging 4 | import unittest 5 | 6 | from django.conf import settings 7 | 8 | from django_datadog_logger.formatters.datadog import DataDogJSONFormatter 9 | 10 | 11 | class DjangoDatadogLoggerTestCase(unittest.TestCase): 12 | @classmethod 13 | def setUpClass(cls): 14 | if not settings.configured: 15 | settings.configure() 16 | 17 | def test_format_json_accepts_a_tuple_of_nones_as_exc_info(self): 18 | """ 19 | When logger is called with exc_info=True, then the exc_info 20 | attribute of the LogRecord is a tuple of (None, None, None). 21 | """ 22 | record = logging.LogRecord("foo", logging.ERROR, "foo.py", 42, "This is an error", None, (None, None, None)) 23 | formatter = DataDogJSONFormatter() 24 | json_record = formatter.json_record("Foo", {}, record) 25 | 26 | self.assertEqual(json_record.get("error.kind"), None) 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py37 4 | py38 5 | py39 6 | py310 7 | py311 8 | flake8 9 | 10 | [testenv:flake8] 11 | basepython = python 12 | deps = flake8 13 | commands = flake8 django_datadog_logger tests 14 | 15 | [testenv] 16 | setenv = 17 | PYTHONPATH = {toxinidir} 18 | 19 | commands = DJANGO_SETTINGS_MODULE=tests.settings python setup.py test 20 | --------------------------------------------------------------------------------