├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config-ci.yaml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENCE ├── README.md ├── SECURITY.md ├── django_logging ├── __init__.py ├── apps.py ├── constants │ ├── __init__.py │ ├── ansi_colors.py │ ├── config_types.py │ ├── date_format_directives.py │ ├── default_settings.py │ ├── log_format_options.py │ ├── log_format_specifiers.py │ └── required_email_settings.py ├── contextvar │ ├── __init__.py │ └── contextvar_manager.py ├── decorators │ ├── __init__.py │ └── execution_tracking.py ├── filters │ ├── __init__.py │ ├── context_filter.py │ └── log_level_filter.py ├── formatters │ ├── __init__.py │ ├── base.py │ ├── colored_formatter.py │ ├── flat_formatter.py │ ├── json_formatter.py │ └── xml_formatter.py ├── handlers │ ├── __init__.py │ └── email_handler.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── generate_pretty_json.py │ │ ├── generate_pretty_xml.py │ │ ├── logs_size_audit.py │ │ └── send_logs.py ├── middleware │ ├── __init__.py │ ├── base.py │ ├── monitor_log_size.py │ └── request_middleware.py ├── settings │ ├── __init__.py │ ├── checks.py │ ├── conf.py │ └── manager.py ├── static │ └── LogiBoard │ │ ├── css │ │ ├── app.css │ │ └── error_response.css │ │ ├── imgs │ │ ├── Lazarus.jpeg │ │ ├── close-50.png │ │ ├── done-50.png │ │ ├── exclamation-mark.png │ │ ├── file-icon.png │ │ ├── folder-icon.png │ │ ├── json-icon.png │ │ ├── multiply-50.png │ │ ├── open-50.png │ │ ├── pause-50.png │ │ ├── search-icon.png │ │ ├── stop-50.png │ │ ├── txt-icon.png │ │ ├── upload-to-cloud-50.png │ │ └── xml-icon.png │ │ └── js │ │ └── scripts.js ├── templates │ ├── email_notifier_template.html │ ├── error_response.html │ └── log_iboard.html ├── tests │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── test_generate_pretty_json.py │ │ ├── test_generate_pretty_xml.py │ │ ├── test_logs_size_audit.py │ │ └── test_send_logs.py │ ├── conftest.py │ ├── constants.py │ ├── contextvar │ │ ├── __init__.py │ │ └── test_contextvar_manager.py │ ├── decorators │ │ ├── __init__.py │ │ └── test_execution_tracking.py │ ├── filters │ │ ├── __init__.py │ │ └── test_log_level_filter.py │ ├── fixtures │ │ ├── __init__.py │ │ ├── commands_fixture.py │ │ ├── conf_fixture.py │ │ ├── email_handler_fixture.py │ │ ├── email_notifier_fixture.py │ │ ├── email_settings_fixture.py │ │ ├── formatters.py │ │ ├── log_and_notify_fixture.py │ │ ├── log_record_fixture.py │ │ ├── logger_fixture.py │ │ ├── request_middleware_fixture.py │ │ ├── settings_fixture.py │ │ └── views_fixture.py │ ├── formatters │ │ ├── __init__.py │ │ ├── test_base_formatter.py │ │ ├── test_colored_formatter.py │ │ ├── test_flat_line_formatter.py │ │ ├── test_json_formatter.py │ │ └── test_xml_formatter.py │ ├── handlers │ │ ├── __init__.py │ │ └── test_email_handler.py │ ├── middleware │ │ ├── __init__.py │ │ ├── test_base_middleware.py │ │ ├── test_monitor_log_size.py │ │ └── test_request_middleware.py │ ├── settings │ │ ├── __init__.py │ │ ├── test_checks.py │ │ └── test_conf.py │ ├── setup.py │ ├── utils │ │ ├── __init__.py │ │ ├── test_context_manager.py │ │ ├── test_email_notifier.py │ │ ├── test_get_conf.py │ │ ├── test_log_and_notify.py │ │ ├── test_process_file.py │ │ └── test_set_conf.py │ ├── validators │ │ ├── __init__.py │ │ ├── test_config_validators.py │ │ └── test_email_settings_validator.py │ └── views │ │ ├── __init__.py │ │ └── test_log_iboard.py ├── urls.py ├── utils │ ├── __init__.py │ ├── command │ │ ├── __init__.py │ │ └── process_file.py │ ├── console_colorizer.py │ ├── context_manager.py │ ├── get_conf.py │ ├── log_email_notifier │ │ ├── __init__.py │ │ ├── log_and_notify.py │ │ └── notifier.py │ ├── set_conf.py │ └── time.py ├── validators │ ├── __init__.py │ ├── config_validators.py │ └── email_settings_validator.py └── views │ ├── __init__.py │ └── log_iboard.py ├── docs ├── Makefile ├── conf.py ├── contributing.rst ├── index.rst ├── log_iboard.rst ├── make.bat ├── quick_start.rst ├── rules.rst ├── settings.rst └── usage.rst ├── packages ├── requirements-dev.txt └── requirements.txt ├── poetry.lock ├── pyproject.toml └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: django_logging 4 | patreon: 5 | open_collective: django_logging 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: "pypi/dj-logging" 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/packages" # Location of the requirements.txt file 10 | schedule: 11 | interval: "weekly" 12 | # If you want to target the specific file 13 | target-branch: "main" # Replace with your default branch if different 14 | 15 | - package-ecosystem: "pip" 16 | directory: "/packages" # Location of the requirements-dev.txt file 17 | schedule: 18 | interval: "weekly" 19 | # If you want to target the specific file 20 | target-branch: "main" # Replace with your default branch if different 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Python ${{ matrix.python-version }} 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | python-version: 13 | - '3.9' 14 | - '3.10' 15 | - '3.11' 16 | - '3.12' 17 | - '3.13' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install coverage codecov pytest poetry 31 | pip install -r packages/requirements-dev.txt 32 | 33 | - name: Run tests with coverage 34 | run: pytest --cov=django_logging --cov-report=xml 35 | 36 | - name: Run Tox tests 37 | run: tox 38 | 39 | - name: Run pre-commit hooks 40 | run: pre-commit run --all-files --config=.pre-commit-config-ci.yaml 41 | 42 | - name: Upload coverage to Codecov 43 | run: codecov 44 | env: 45 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | release: 10 | name: Build and Release 11 | runs-on: ubuntu-latest 12 | 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install poetry 26 | 27 | - name: Build package 28 | run: | 29 | poetry build 30 | 31 | - name: Publish to PyPI 32 | run: | 33 | poetry publish --username __token__ --password ${{ secrets.PYPI_TOKEN }} 34 | env: 35 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 36 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 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 | logs/ 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | .idea/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | 104 | media/upload/ 105 | # mypy 106 | .mypy_cache/ 107 | settings.ini 108 | .vscode 109 | 110 | collectstatic/ 111 | CACHE/ 112 | # **/migrations/** 113 | # !**/migrations 114 | # !**/migrations/__init__.py 115 | 116 | .vscode/* 117 | 118 | docker-compose.yml 119 | Dockerfile 120 | 121 | jwt-key 122 | jwt-key.pub 123 | 124 | .docker 125 | 126 | kernel/ 127 | manage.py 128 | 129 | docs/sage_painless/ 130 | 131 | coverage_html_report/ 132 | 133 | psude.py 134 | sage_painless/services/model_generator.py_pseudo.txt 135 | 136 | fifo.py 137 | gunicorn-conf.py 138 | uwsgi.ini 139 | .env.prod 140 | nginx.conf 141 | test_app/ 142 | myenv/ 143 | myenv3_8/ 144 | -------------------------------------------------------------------------------- /.pre-commit-config-ci.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | files: \.yaml$ 8 | - id: trailing-whitespace 9 | exclude: (migrations/|tests/|docs/).* 10 | - id: end-of-file-fixer 11 | exclude: (migrations/|tests/|docs/).* 12 | - id: check-added-large-files 13 | exclude: (migrations/|tests/|docs/).* 14 | - id: check-case-conflict 15 | exclude: (migrations/|tests/|docs/).* 16 | - id: check-merge-conflict 17 | exclude: (migrations/|tests/|docs/).* 18 | - id: check-docstring-first 19 | exclude: (migrations/|tests/|docs/).* 20 | 21 | - repo: https://github.com/tox-dev/pyproject-fmt 22 | rev: v2.5.1 23 | hooks: 24 | - id: pyproject-fmt 25 | 26 | - repo: https://github.com/tox-dev/tox-ini-fmt 27 | rev: 1.5.0 28 | hooks: 29 | - id: tox-ini-fmt 30 | 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.19.1 33 | hooks: 34 | - id: pyupgrade 35 | 36 | - repo: https://github.com/pre-commit/mirrors-isort 37 | rev: v5.10.1 38 | hooks: 39 | - id: isort 40 | exclude: (migrations/|tests/|docs/).* 41 | 42 | - repo: https://github.com/psf/black 43 | rev: 25.1.0 44 | hooks: 45 | - id: black 46 | args: [ "--config=pyproject.toml" ] 47 | exclude: (migrations/|tests/|docs/).* 48 | 49 | - repo: https://github.com/commitizen-tools/commitizen 50 | rev: v4.6.0 51 | hooks: 52 | - id: commitizen 53 | exclude: (migrations/|tests/|docs/).* 54 | 55 | - repo: https://github.com/PyCQA/bandit 56 | rev: 1.8.3 57 | hooks: 58 | - id: bandit 59 | args: [ "-c", "pyproject.toml", "-r", "." ] 60 | additional_dependencies: [ "bandit[toml]" ] 61 | exclude: (migrations/|tests/|docs/).* 62 | 63 | - repo: https://github.com/PyCQA/docformatter 64 | rev: v1.7.5 65 | hooks: 66 | - id: docformatter 67 | args: [ "--in-place", "--recursive", "--blank" ] 68 | exclude: (migrations/|tests/|docs/).* 69 | 70 | - repo: https://github.com/adamchainz/blacken-docs 71 | rev: 1.19.1 72 | hooks: 73 | - id: blacken-docs 74 | additional_dependencies: 75 | - black==24.4.2 76 | files: '\.rst$' 77 | 78 | - repo: https://github.com/rstcheck/rstcheck 79 | rev: "v6.2.4" 80 | hooks: 81 | - id: rstcheck 82 | args: [ "--report-level=warning" ] 83 | files: ^(docs/(.*/)*.*\.rst) 84 | additional_dependencies: [ Sphinx==6.2.1 ] 85 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | files: \.yaml$ 8 | - id: trailing-whitespace 9 | exclude: (migrations/|tests/|docs/).* 10 | - id: end-of-file-fixer 11 | exclude: (migrations/|tests/|docs/).* 12 | - id: check-added-large-files 13 | exclude: (migrations/|tests/|docs/).* 14 | - id: check-case-conflict 15 | exclude: (migrations/|tests/|docs/).* 16 | - id: check-merge-conflict 17 | exclude: (migrations/|tests/|docs/).* 18 | - id: check-docstring-first 19 | exclude: (migrations/|tests/|docs/).* 20 | 21 | - repo: https://github.com/tox-dev/pyproject-fmt 22 | rev: v2.5.1 23 | hooks: 24 | - id: pyproject-fmt 25 | 26 | - repo: https://github.com/tox-dev/tox-ini-fmt 27 | rev: 1.5.0 28 | hooks: 29 | - id: tox-ini-fmt 30 | 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.19.1 33 | hooks: 34 | - id: pyupgrade 35 | 36 | - repo: https://github.com/pre-commit/mirrors-isort 37 | rev: v5.10.1 38 | hooks: 39 | - id: isort 40 | exclude: (migrations/|tests/|docs/).* 41 | 42 | - repo: https://github.com/psf/black 43 | rev: 25.1.0 44 | hooks: 45 | - id: black 46 | args: [ "--config=pyproject.toml" ] 47 | exclude: (migrations/|tests/|docs/).* 48 | 49 | - repo: https://github.com/commitizen-tools/commitizen 50 | rev: v4.6.0 51 | hooks: 52 | - id: commitizen 53 | exclude: (migrations/|tests/|docs/).* 54 | 55 | - repo: https://github.com/PyCQA/bandit 56 | rev: 1.8.3 57 | hooks: 58 | - id: bandit 59 | args: [ "-c", "pyproject.toml", "-r", "." ] 60 | additional_dependencies: [ "bandit[toml]" ] 61 | exclude: (migrations/|tests/|docs/).* 62 | 63 | - repo: https://github.com/PyCQA/docformatter 64 | rev: v1.7.5 65 | hooks: 66 | - id: docformatter 67 | args: [ "--in-place", "--recursive", "--blank" ] 68 | exclude: (migrations/|tests/|docs/).* 69 | 70 | - repo: https://github.com/adamchainz/blacken-docs 71 | rev: 1.19.1 72 | hooks: 73 | - id: blacken-docs 74 | additional_dependencies: 75 | - black==24.4.2 76 | files: '\.rst$' 77 | 78 | - repo: https://github.com/rstcheck/rstcheck 79 | rev: "v6.2.4" 80 | hooks: 81 | - id: rstcheck 82 | args: [ "--report-level=warning" ] 83 | files: ^(docs/(.*/)*.*\.rst) 84 | additional_dependencies: [ Sphinx==6.2.1 ] 85 | 86 | - repo: local 87 | hooks: 88 | - id: pytest 89 | name: Pytest 90 | entry: poetry run pytest -v 91 | language: system 92 | types: [ python ] 93 | stages: [ commit ] 94 | pass_filenames: false 95 | always_run: true 96 | 97 | - id: pylint 98 | name: pylint 99 | entry: pylint 100 | language: system 101 | types: [ python ] 102 | require_serial: true 103 | args: 104 | - "-rn" 105 | - "-sn" 106 | - "--rcfile=pyproject.toml" 107 | files: ^django_logging/ 108 | exclude: (migrations/|tests/|docs/).* 109 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version, and other tools 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | 14 | # Build documentation with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | # formats: 20 | # - pdf 21 | # - epub 22 | 23 | 24 | # Optional but recommended, declare the Python requirements required 25 | # to build your documentation 26 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 27 | python: 28 | install: 29 | - requirements: packages/requirements-dev.txt 30 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ## Authors 2 | 3 | - **Aryan Niknezhad** 4 | - GitHub: [ARYAN-NIKNEZHAD](https://github.com/ARYAN-NIKNEZHAD) 5 | - Email: aryan513966@gmail.com 6 | 7 | 8 | - **Mehrshad Mirshekary** 9 | - GitHub: [MEHRSHAD-MIRSHEKARY](https://github.com/MEHRSHAD-MIRSHEKARY) 10 | - Email: mehrshad_mirshekary@email.com 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to django_logging 2 | 3 | 4 | We’re excited that you’re interested in contributing to `django_logging`! Whether you’re fixing a bug, adding a feature, or improving the project, your help is appreciated. 5 | 6 | ## Overview 7 | 8 | 9 | - **Setting Up Your Environment** 10 | - **Testing Your Changes** 11 | - **Code Style Guidelines** 12 | - **Utilizing Pre-commit Hooks** 13 | - **Creating a Pull Request** 14 | - **Reporting Issues** 15 | - **Resources** 16 | 17 | ## Setting Up Your Environment 18 | 19 | 20 | 1. **Fork the Repository:** 21 | 22 | Begin by forking the `django_logging` repository on GitHub. This creates your own copy where you can make changes. 23 | 24 | 2. **Clone Your Fork:** 25 | 26 | Use the following command to clone your fork locally: 27 | 28 | ```bash 29 | git clone https://github.com/your-username/django_logging.git 30 | cd django_logging 31 | ``` 32 | 33 | 3. **Install Dependencies:** 34 | 35 | Install the necessary dependencies using `Poetry`. If Poetry isn't installed on your machine, you can find installation instructions on the [Poetry website](https://python-poetry.org/docs/#installation). 36 | 37 | ```bash 38 | poetry install 39 | ``` 40 | 41 | 4. **Create a Feature Branch:** 42 | 43 | It’s a good practice to create a new branch for your work: 44 | 45 | ```bash 46 | git checkout -b feature/your-feature-name 47 | ``` 48 | 49 | ## Testing Your Changes 50 | 51 | We use `pytest` for running tests. Before submitting your changes, ensure that all tests pass: 52 | 53 | ```bash 54 | poetry run pytest 55 | ``` 56 | 57 | If you’re adding a new feature or fixing a bug, don’t forget to write tests to cover your changes. 58 | 59 | 60 | ## Code Style Guidelines 61 | 62 | Maintaining a consistent code style is crucial. We use `black` for code formatting and `isort` for import sorting. Make sure your code adheres to these styles: 63 | 64 | ```bash 65 | poetry run black . 66 | poetry run isort . 67 | ``` 68 | For linting, `pylint` is used to enforce style and catch potential errors: 69 | 70 | ```bash 71 | poetry run pylint django_logging 72 | ``` 73 | 74 | ## Utilizing Pre-commit Hooks 75 | 76 | Pre-commit hooks are used to automatically check and format code before you make a commit. This ensures consistency and quality in the codebase. 77 | 78 | 1. **Install Pre-commit:** 79 | 80 | ```bash 81 | poetry add --group dev pre-commit 82 | ``` 83 | 84 | 2. **Set Up the Hooks:** 85 | 86 | Install the pre-commit hooks by running: 87 | 88 | ```bash 89 | poetry run pre-commit install 90 | ``` 91 | 3. **Manual Hook Execution (Optional):** 92 | 93 | To run all hooks manually on your codebase: 94 | 95 | ```bash 96 | poetry run pre-commit run --all-files 97 | ``` 98 | 99 | ## Creating a Pull Request 100 | 101 | Once your changes are ready, follow these steps to submit them: 102 | 103 | 1. **Commit Your Changes:** 104 | 105 | Write clear and concise commit messages. Following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format is recommended: 106 | 107 | ```bash 108 | git commit -am 'feat: add custom logging formatter' 109 | ``` 110 | 2. **Push Your Branch:** 111 | 112 | Push your branch to your fork on GitHub: 113 | 114 | ```bash 115 | git push origin feature/your-feature-name 116 | ``` 117 | 118 | 3. **Open a Pull Request:** 119 | 120 | Go to the original `django_logging` repository and open a pull request. Include a detailed description of your changes and link any related issues. 121 | 122 | 4. **Respond to Feedback:** 123 | 124 | After submitting, a maintainer will review your pull request. Be prepared to make revisions based on their feedback. 125 | 126 | ## Reporting Issues 127 | 128 | Found a bug or have a feature request? We’d love to hear from you! 129 | 130 | 1. **Open an Issue:** 131 | 132 | Head over to the `Issues` section of the `django_logging` repository and click "New Issue". 133 | 134 | 2. **Describe the Problem:** 135 | 136 | Fill out the issue template with as much detail as possible. This helps us understand and address the issue more effectively. 137 | 138 | ## Resources 139 | 140 | Here are some additional resources that might be helpful: 141 | 142 | - [Poetry Documentation](https://python-poetry.org/docs/) 143 | - [Black Documentation](https://black.readthedocs.io/en/stable/) 144 | - [isort Documentation](https://pycqa.github.io/isort/) 145 | - [pytest Documentation](https://docs.pytest.org/en/stable/) 146 | - [pylint Documentation](https://pylint.pycqa.org/en/latest/) 147 | - [Pre-commit Documentation](https://pre-commit.com/) 148 | 149 | --- 150 | 151 | Thank you for your interest in contributing to `django_logging`! We look forward to your contributions. 152 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | We would like to thank the following people for their contributions to the `django_logging` project: 4 | 5 | ## Core Contributors 6 | 7 | | Name | Role | GitHub | Email | Contributions | Image | 8 | |-----------------------|----------------------------|-----------------------------------------------------------------------------|----------------------------------|-----------------------------------------------------|-------------------------------------------------------------------| 9 | | **Aryan Niknezhad** | Project Creator & Lead Maintainer | [ARYAN-NIKNEZHAD](https://github.com/ARYAN-NIKNEZHAD) | aryan513966@gmail.com | Project creator and lead maintainer. | ![Aryan Niknezhad](https://avatars.githubusercontent.com/u/127540182?v=4) | 10 | | **Mehrshad Mirshekary**| Maintainer | [MEHRSHAD-MIRSHEKARY](https://github.com/MEHRSHAD-MIRSHEKARY) | mehrshad_mirshekary@email.com | Maintainer | ![Mehrshad Mirshekary](https://avatars.githubusercontent.com/u/121759619?v=4) | 11 | | **Fatemeh Zahra** | Contributor | [FATEMEH-Z-HASHEMI](https://github.com/FATEMEH-Z-HASHEMI) | fatemehzahrahashemi0@gmail.com | contributor || 12 | 13 | --- 14 | 15 | ## Special Thanks 16 | 17 | - **Fatemeh Zahra** 18 | - Refactoring and improving the email template design and styles for the `django_logging` package. 19 | 20 | --- 21 | 22 | To be added to this list, please contribute to the project by submitting a pull request, opening issues, or helping improve the documentation. We appreciate all contributions, big and small! 23 | 24 | If you have contributed and are not listed here, please feel free to add your name and details in a pull request. 25 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lazarus 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We actively support the following versions of `django_logging` with security updates: 6 | 7 | | Version | Supported | 8 | | --------- | ------------------ | 9 | | 2.0.x | ✅ Fully supported | 10 | | 1.2.x | ✅ Supported | 11 | | 1.1.x | ✅ Supported | 12 | | 1.0.x | ⚠️ Limited support | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | We take security issues seriously. If you find a vulnerability in `django_logging`, please report it confidentially. Here are the steps to report security vulnerabilities: 17 | 18 | 1. **Email**: Please send an email to [aryan513966@gmail.com](mailto:aryan513966@gmail.com) with a detailed description of the vulnerability. 19 | 2. **Details**: In your email, include the following details: 20 | - Description of the vulnerability. 21 | - Potential impact and severity. 22 | - Steps to reproduce the issue. 23 | - Any other relevant information, such as proof of concept or screenshots. 24 | 25 | We will: 26 | - Acknowledge your report within 2 business days. 27 | - Work with you to understand and resolve the issue as quickly as possible. 28 | - Provide an estimate of when a patch will be available and credit you (if desired) in the changelog. 29 | 30 | ## Handling Vulnerabilities 31 | 32 | When a vulnerability is confirmed: 33 | - We will create a fix and apply it to all actively supported versions of `django_logging`. 34 | - A new release with the security fix will be published, and the vulnerability will be disclosed in the changelog or via a security advisory. 35 | - We may delay the disclosure of details about the vulnerability until a sufficient number of users have updated to the patched version. 36 | 37 | ## General Security Guidelines 38 | 39 | - Keep your `django_logging` package up to date with the latest versions to ensure you benefit from the latest security fixes. 40 | - Follow our changelog for announcements regarding security fixes. 41 | - Ensure that your logging configuration is secure and does not expose sensitive information in log files. 42 | 43 | ## Responsible Disclosure 44 | 45 | We strongly encourage responsible disclosure and will work to fix issues in a timely manner. We appreciate any effort to help make `django_logging` more secure for all users. 46 | 47 | Thank you for helping us improve the security of `django_logging`! 48 | -------------------------------------------------------------------------------- /django_logging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/__init__.py -------------------------------------------------------------------------------- /django_logging/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class DjangoLoggingConfig(AppConfig): 6 | default_auto_field = "django.db.models.BigAutoField" 7 | name = "django_logging" 8 | verbose_name = _("Django Logging") 9 | 10 | def ready(self) -> None: 11 | """This method is called when the Django application is ready. 12 | 13 | The `ready` method is used to perform application-specific 14 | startup tasks. In this case, it performs the following actions: 15 | 16 | 1. Imports necessary modules for checks, configuration retrieval, 17 | and logging setup. 18 | 2. Retrieves the logging configuration from settings using `get_config`. 19 | 3. Sets up the logging configuration using the retrieved configs. 20 | 21 | This ensures that the logging configuration is correctly set up 22 | when the application starts. 23 | 24 | """ 25 | from django_logging.settings import checks 26 | from django_logging.utils.get_conf import get_config 27 | from django_logging.utils.set_conf import set_config 28 | 29 | conf = get_config() 30 | 31 | # Set the logging configuration 32 | set_config(**conf) 33 | -------------------------------------------------------------------------------- /django_logging/constants/__init__.py: -------------------------------------------------------------------------------- 1 | from .date_format_directives import VALID_DIRECTIVES 2 | from .default_settings import DefaultConsoleSettings, DefaultLoggingSettings 3 | from .log_format_options import FORMAT_OPTIONS 4 | from .log_format_specifiers import LOG_FORMAT_SPECIFIERS 5 | 6 | # Used in settings.conf 7 | ALLOWED_EXTRA_FILE_TYPES = ["JSON", "XML"] 8 | ALLOWED_FILE_FORMAT_TYPES = ["JSON", "XML", "FLAT"] 9 | -------------------------------------------------------------------------------- /django_logging/constants/ansi_colors.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | # pylint: disable=too-many-instance-attributes 5 | @dataclass(frozen=True) 6 | class AnsiColors: 7 | BLACK: str = "\033[0;30m" 8 | RED: str = "\033[0;31m" 9 | GREEN: str = "\033[0;32m" 10 | YELLOW: str = "\033[0;33m" 11 | BLUE: str = "\033[0;34m" 12 | MAGENTA: str = "\033[0;35m" 13 | CYAN: str = "\033[0;36m" 14 | GRAY: str = "\033[0;37m" 15 | WHITE: str = "\033[0;38m" 16 | RESET: str = "\033[0m" 17 | BRIGHT_BLACK: str = "\033[0;90m" 18 | BRIGHT_RED: str = "\033[0;91m" 19 | BRIGHT_GREEN: str = "\033[0;92m" 20 | BRIGHT_YELLOW: str = "\033[0;93m" 21 | BRIGHT_BLUE: str = "\033[0;94m" 22 | BRIGHT_MAGENTA: str = "\033[0;95m" 23 | BRIGHT_CYAN: str = "\033[0;96m" 24 | BRIGHT_WHITE: str = "\033[0;97m" 25 | PINK: str = "\033[38;5;213m" 26 | LIGHT_PURPLE = "\033[38;5;129m" 27 | BLACK_BACKGROUND: str = "\033[40m" 28 | RED_BACKGROUND: str = "\033[41m" 29 | GREEN_BACKGROUND: str = "\033[42m" 30 | YELLOW_BACKGROUND: str = "\033[43m" 31 | BLUE_BACKGROUND: str = "\033[44m" 32 | MAGENTA_BACKGROUND: str = "\033[45m" 33 | CYAN_BACKGROUND: str = "\033[46m" 34 | WHITE_BACKGROUND: str = "\033[47m" 35 | BRIGHT_BLACK_BACKGROUND: str = "\033[100m" 36 | BRIGHT_RED_BACKGROUND: str = "\033[101m" 37 | BRIGHT_GREEN_BACKGROUND: str = "\033[102m" 38 | BRIGHT_YELLOW_BACKGROUND: str = "\033[103m" 39 | BRIGHT_BLUE_BACKGROUND: str = "\033[104m" 40 | BRIGHT_MAGENTA_BACKGROUND: str = "\033[105m" 41 | BRIGHT_CYAN_BACKGROUND: str = "\033[106m" 42 | BRIGHT_WHITE_BACKGROUND: str = "\033[107m" 43 | 44 | BOLD: str = "\033[1m" 45 | DIM: str = "\033[2m" 46 | ITALIC: str = "\033[3m" 47 | BOLD_ITALIC: str = "\033[1;3m" 48 | UNDERLINE: str = "\033[4m" 49 | BLINK: str = "\033[5m" 50 | INVERT: str = "\033[7m" 51 | STRIKETHROUGH: str = "\033[9m" 52 | 53 | 54 | # Mapping log levels to ANSI colors 55 | LOG_LEVEL_COLORS = { 56 | "DEBUG": AnsiColors.BLUE, 57 | "INFO": AnsiColors.GREEN, 58 | "WARNING": AnsiColors.YELLOW, 59 | "ERROR": AnsiColors.RED, 60 | "CRITICAL": AnsiColors.RED_BACKGROUND, 61 | } 62 | -------------------------------------------------------------------------------- /django_logging/constants/config_types.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal, TypedDict, Union 2 | 3 | FormatOption = Union[int, str] 4 | 5 | # Type Aliases for configurations 6 | LogFileFormatType = Literal["JSON", "XML", "FLAT", "NORMAL"] 7 | LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 8 | LogDir = str 9 | LogLevels = List[LogLevel] 10 | NotifierLogLevels = List[Union[Literal["ERROR", "CRITICAL"], None]] 11 | LogDateFormat = str 12 | 13 | 14 | class LogEmailNotifier(TypedDict, total=False): 15 | ENABLE: bool 16 | NOTIFY_ERROR: bool 17 | NOTIFY_CRITICAL: bool 18 | LOG_FORMAT: FormatOption 19 | USE_TEMPLATE: bool 20 | 21 | 22 | class LogFileFormats(TypedDict, total=False): 23 | DEBUG: FormatOption 24 | INFO: FormatOption 25 | WARNING: FormatOption 26 | ERROR: FormatOption 27 | CRITICAL: FormatOption 28 | 29 | 30 | class LogFileFormatTypes(TypedDict, total=False): 31 | DEBUG: LogFileFormatType 32 | INFO: LogFileFormatType 33 | WARNING: LogFileFormatType 34 | ERROR: LogFileFormatType 35 | CRITICAL: LogFileFormatType 36 | 37 | 38 | class ExtraLogFiles(TypedDict, total=False): 39 | DEBUG: bool 40 | INFO: bool 41 | WARNING: bool 42 | ERROR: bool 43 | CRITICAL: bool 44 | -------------------------------------------------------------------------------- /django_logging/constants/date_format_directives.py: -------------------------------------------------------------------------------- 1 | from typing import Set 2 | 3 | VALID_DIRECTIVES: Set = { 4 | "%Y", 5 | "%m", 6 | "%d", 7 | "%H", 8 | "%M", 9 | "%S", 10 | "%a", 11 | "%A", 12 | "%b", 13 | "%B", 14 | "%p", 15 | "%I", 16 | "%j", 17 | "%U", 18 | "%W", 19 | "%c", 20 | "%x", 21 | "%X", 22 | } 23 | -------------------------------------------------------------------------------- /django_logging/constants/default_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass, field 3 | from typing import cast 4 | 5 | from django_logging.constants.config_types import ( 6 | ExtraLogFiles, 7 | FormatOption, 8 | LogDateFormat, 9 | LogDir, 10 | LogEmailNotifier, 11 | LogFileFormats, 12 | LogFileFormatTypes, 13 | LogLevel, 14 | LogLevels, 15 | ) 16 | 17 | 18 | # pylint: disable=too-many-instance-attributes 19 | @dataclass(frozen=True) 20 | class DefaultLoggingSettings: 21 | log_dir: LogDir = field(default_factory=lambda: os.path.join(os.getcwd(), "logs")) 22 | log_dir_size_limit: int = 1024 # MB 23 | log_levels: LogLevels = field( 24 | default_factory=lambda: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 25 | ) 26 | log_date_format: LogDateFormat = "%Y-%m-%d %H:%M:%S" 27 | auto_initialization_enable: bool = True 28 | initialization_message_enable: bool = True 29 | log_sql_queries_enable: bool = False 30 | include_log_iboard: bool = False 31 | log_file_formats: LogFileFormats = field( 32 | default_factory=lambda: cast( 33 | LogFileFormats, 34 | { 35 | "DEBUG": 1, 36 | "INFO": 1, 37 | "WARNING": 1, 38 | "ERROR": 1, 39 | "CRITICAL": 1, 40 | }, 41 | ) 42 | ) 43 | log_file_format_types: LogFileFormatTypes = field( 44 | default_factory=lambda: cast( 45 | LogFileFormatTypes, 46 | { 47 | "DEBUG": "normal", 48 | "INFO": "normal", 49 | "WARNING": "normal", 50 | "ERROR": "normal", 51 | "CRITICAL": "normal", 52 | }, 53 | ) 54 | ) 55 | 56 | extra_log_files: ExtraLogFiles = field( 57 | default_factory=lambda: cast( 58 | ExtraLogFiles, 59 | { 60 | "DEBUG": False, 61 | "INFO": False, 62 | "WARNING": False, 63 | "ERROR": False, 64 | "CRITICAL": False, 65 | }, 66 | ) 67 | ) 68 | 69 | log_email_notifier: LogEmailNotifier = field( 70 | default_factory=lambda: cast( 71 | LogEmailNotifier, 72 | { 73 | "ENABLE": False, 74 | "NOTIFY_ERROR": False, 75 | "NOTIFY_CRITICAL": False, 76 | "LOG_FORMAT": 1, 77 | "USE_TEMPLATE": True, 78 | }, 79 | ) 80 | ) 81 | 82 | 83 | @dataclass 84 | class DefaultConsoleSettings: 85 | log_console_level: LogLevel = "DEBUG" 86 | log_console_format: FormatOption = 1 87 | log_console_colorize: bool = True 88 | -------------------------------------------------------------------------------- /django_logging/constants/log_format_options.py: -------------------------------------------------------------------------------- 1 | FORMAT_OPTIONS = { 2 | 1: "%(levelname)s | %(asctime)s | %(module)s | %(message)s | %(context)s", 3 | 2: "%(levelname)s | %(asctime)s | %(context)s | %(message)s | %(exc_text)s", 4 | 3: "%(levelname)s | %(context)s | %(message)s | %(stack_info)s", 5 | 4: "%(context)s | %(asctime)s - %(name)s - %(levelname)s - %(message)s", 6 | 5: "%(levelname)s | %(message)s | %(context)s | [in %(pathname)s:%(lineno)d]", 7 | 6: "%(asctime)s | %(context)s | %(levelname)s | %(message)s | %(exc_info)s", 8 | 7: "%(levelname)s | %(asctime)s | %(context)s | in %(module)s: %(message)s", 9 | 8: "%(levelname)s | %(context)s | %(message)s | [%(filename)s:%(lineno)d]", 10 | 9: "[%(asctime)s] | %(levelname)s | %(context)s | in %(module)s: %(message)s", 11 | 10: "%(asctime)s | %(processName)s | %(context)s | %(name)s | %(levelname)s | %(message)s", 12 | 11: "%(asctime)s | %(context)s | %(threadName)s | %(name)s | %(levelname)s | %(message)s", 13 | 12: "%(levelname)s | [%(asctime)s] | %(context)s | (%(filename)s:%(lineno)d) | %(message)s", 14 | 13: "%(levelname)s | [%(asctime)s] | %(context)s | {%(name)s} | (%(filename)s:%(lineno)d): %(message)s", 15 | 14: "[%(asctime)s] | %(levelname)s | %(context)s | %(name)s | %(module)s | %(message)s", 16 | 15: "%(levelname)s | %(context)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s", 17 | 16: "%(levelname)s | %(context)s | %(message)s | [%(asctime)s] | %(module)s", 18 | 17: "%(levelname)s | %(context)s | [%(asctime)s] | %(process)d | %(message)s", 19 | 18: "%(levelname)s | %(context)s | %(asctime)s | %(name)s | %(message)s", 20 | 19: "%(levelname)s | %(asctime)s | %(context)s | %(module)s:%(lineno)d | %(message)s", 21 | 20: "[%(asctime)s] | %(levelname)s | %(context)s | %(thread)d | %(message)s", 22 | } 23 | -------------------------------------------------------------------------------- /django_logging/constants/log_format_specifiers.py: -------------------------------------------------------------------------------- 1 | LOG_FORMAT_SPECIFIERS = [ 2 | "asctime", 3 | "created", 4 | "exc_info", 5 | "exc_text", 6 | "relativeCreated", 7 | "msecs", 8 | "levelname", 9 | "levelno", 10 | "name", 11 | "module", 12 | "filename", 13 | "pathname", 14 | "lineno", 15 | "funcName", 16 | "process", 17 | "processName", 18 | "thread", 19 | "threadName", 20 | "message", 21 | "stack_info", 22 | "context", 23 | ] 24 | -------------------------------------------------------------------------------- /django_logging/constants/required_email_settings.py: -------------------------------------------------------------------------------- 1 | EMAIL_REQUIRED_SETTINGS = [ 2 | "EMAIL_HOST", 3 | "EMAIL_PORT", 4 | "EMAIL_HOST_USER", 5 | "EMAIL_HOST_PASSWORD", 6 | "EMAIL_USE_TLS", 7 | "DEFAULT_FROM_EMAIL", 8 | ] 9 | 10 | NOTIFIER_EXTRA_REQUIRED_SETTING = "ADMIN_EMAIL" 11 | -------------------------------------------------------------------------------- /django_logging/contextvar/__init__.py: -------------------------------------------------------------------------------- 1 | from .contextvar_manager import manager 2 | -------------------------------------------------------------------------------- /django_logging/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | from .execution_tracking import execution_tracker 2 | -------------------------------------------------------------------------------- /django_logging/decorators/execution_tracking.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | from functools import wraps 5 | from typing import Any, Callable, Optional 6 | 7 | from django.conf import settings 8 | from django.db import connection 9 | 10 | from django_logging.utils.time import format_elapsed_time 11 | from django_logging.validators.config_validators import ( 12 | validate_boolean_setting, 13 | validate_integer_setting, 14 | ) 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | # pylint: disable=too-many-locals 20 | def execution_tracker( 21 | logging_level: int = logging.INFO, 22 | log_queries: bool = False, 23 | query_threshold: Optional[int] = None, 24 | query_exceed_warning: bool = False, 25 | ) -> Callable: 26 | """Decorator to log execution time, number of database queries, and 27 | arguments for a function, along with detailed function information (module, 28 | file, and line number). 29 | 30 | This decorator allows you to track the performance of a function by logging its execution time, 31 | the number of database queries made (if DEBUG is True and log_queries is enabled), and function 32 | arguments. It also logs a warning if the number of queries exceeds the specified threshold. 33 | 34 | Args: 35 | logging_level (int): The logging level at which to log the performance details (default is logging.INFO). 36 | log_queries (bool): Whether to log database queries. If set to True, and DEBUG is True, the number of 37 | queries will be included in the logs (default is False). 38 | query_threshold (Optional[int]): Optional threshold for the number of database queries. If exceeded, 39 | a warning will be logged (default is None). 40 | query_exceed_warning (bool): Whether to log a warning message if number of queries exceeded the threshold. 41 | 42 | Returns: 43 | Callable: A decorator that logs performance details. 44 | 45 | Raises: 46 | ValueError: If any of the provided settings (logging_level, query_threshold, or log_queries) are invalid. 47 | 48 | """ 49 | errors = [] 50 | errors.extend( 51 | validate_integer_setting(logging_level, "execution_tracker.logging_level") 52 | ) 53 | errors.extend( 54 | validate_boolean_setting(log_queries, "execution_tracker.log_queries") 55 | ) 56 | if query_threshold: 57 | errors.extend( 58 | validate_integer_setting( 59 | query_threshold, "execution_tracker.query_threshold" 60 | ) 61 | ) 62 | errors.extend( 63 | validate_boolean_setting( 64 | query_exceed_warning, "execution_tracker.query_exceed_warning" 65 | ) 66 | ) 67 | 68 | if errors: 69 | raise ValueError(errors[0]) # raises the first error to be fixed 70 | 71 | def decorator(func: Callable) -> Callable: 72 | @wraps(func) 73 | def wrapper(*args: Any, **kwargs: Any) -> Any: 74 | start_time = time.time() 75 | 76 | # Check if DEBUG is True and log_queries is enabled; if not, ignore query tracking 77 | if settings.DEBUG and log_queries: 78 | connection.queries_log.clear() 79 | 80 | try: 81 | # Execute the function 82 | result = func(*args, **kwargs) 83 | 84 | # Calculate execution time 85 | elapsed_time = time.time() - start_time 86 | 87 | # Get detailed function information 88 | module_name = func.__module__ 89 | function_name = func.__qualname__ 90 | file_path = os.path.abspath(func.__code__.co_filename) 91 | line_number = func.__code__.co_firstlineno 92 | 93 | time_message = format_elapsed_time(elapsed_time) 94 | log_message = ( 95 | f"Performance Metrics for Function: '{function_name}'\n" 96 | f" Module: {module_name}\n" 97 | f" File: {file_path}, Line: {line_number}\n" 98 | f" Execution Time: {time_message}" 99 | ) 100 | 101 | # If log_queries is enabled and DEBUG is True, include number of queries 102 | if settings.DEBUG and log_queries: 103 | num_queries = len(connection.queries_log) 104 | log_message += f"\n Database Queries: {num_queries} queries " 105 | 106 | # Log query threshold warning separately, if applicable 107 | if query_threshold and num_queries > query_threshold: 108 | log_message += f"(exceeds threshold of ({query_threshold}))" 109 | if query_exceed_warning: 110 | logger.warning( 111 | "Number of database queries (%s) " 112 | "exceeded threshold (%s) for function '%s'", 113 | num_queries, 114 | query_threshold, 115 | function_name, 116 | ) 117 | elif log_queries and not settings.DEBUG: 118 | logger.warning( 119 | "DEBUG mode is disabled, so database queries are not tracked. " 120 | "To include number of queries, set DEBUG to True in your django settings." 121 | ) 122 | 123 | # Log the performance metrics 124 | logger.log(logging_level, log_message) 125 | 126 | return result 127 | 128 | except Exception as e: 129 | logger.error( 130 | "Error executing function '%s': %s", 131 | func.__qualname__, 132 | str(e), 133 | exc_info=True, 134 | ) 135 | raise 136 | 137 | return wrapper 138 | 139 | return decorator 140 | -------------------------------------------------------------------------------- /django_logging/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .context_filter import ContextVarFilter 2 | from .log_level_filter import LoggingLevelFilter 3 | -------------------------------------------------------------------------------- /django_logging/filters/context_filter.py: -------------------------------------------------------------------------------- 1 | from logging import Filter, LogRecord 2 | from typing import Any, Dict 3 | 4 | from django_logging.contextvar import manager 5 | 6 | 7 | class ContextVarFilter(Filter): 8 | """A logging filter that merges context variables into the log record. 9 | 10 | This filter retrieves the current context variables, combines them with any existing 11 | bound logger context, and adds the merged context to the log record's 'context' attribute. 12 | 13 | Attributes: 14 | N/A 15 | 16 | Methods: 17 | filter(record: LogRecord) -> bool: 18 | Merges context variables into the log record before formatting. 19 | 20 | """ 21 | 22 | def filter(self, record: LogRecord) -> bool: 23 | """Merge context variables into the log record. 24 | 25 | Args: 26 | record (LogRecord): The logging record to be processed. 27 | 28 | Returns: 29 | bool: Always returns True to allow the record to be logged. 30 | 31 | This method checks if the log record already has a 'context' attribute. It then merges 32 | the current context variables (retrieved via `contextvar.get_merged_context`) into the log record. 33 | 34 | """ 35 | bound_logger_context: Dict[str, Any] = getattr(record, "context", {}) 36 | 37 | # Merge the context variables and set them in the record 38 | record.context = manager.get_merged_context(bound_logger_context) or "" 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /django_logging/filters/log_level_filter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class LoggingLevelFilter(logging.Filter): 5 | """Filters log records based on their logging level. 6 | 7 | This filter is used to prevent log records from being written to log 8 | files intended for lower log levels. For example, if we have 9 | separate log files for DEBUG, INFO, WARNING, and ERROR levels, this 10 | filter ensures that a log record with level ERROR is only written to 11 | the ERROR log file, and not to the DEBUG, INFO or WARNING log files. 12 | 13 | """ 14 | 15 | def __init__(self, logging_level: int): 16 | """Initializes a LoggingLevelFilter instance. 17 | 18 | Args: 19 | logging_level: The logging level to filter on (e.g. logging.DEBUG, logging.INFO, etc.). 20 | 21 | Returns: 22 | None 23 | 24 | """ 25 | super().__init__() 26 | self.logging_level = logging_level 27 | 28 | def filter(self, record: logging.LogRecord) -> bool: 29 | """Filters a log record based on its level. 30 | 31 | Args: 32 | record: The log record to filter. 33 | 34 | Returns: 35 | True if the log record's level matches the specified logging level, False otherwise. 36 | 37 | """ 38 | return record.levelno == self.logging_level 39 | -------------------------------------------------------------------------------- /django_logging/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | from .colored_formatter import ColoredFormatter 2 | from .flat_formatter import FLATFormatter 3 | from .json_formatter import JSONFormatter 4 | from .xml_formatter import XMLFormatter 5 | -------------------------------------------------------------------------------- /django_logging/formatters/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | from logging import Formatter, LogRecord 3 | from typing import Any, Dict, List, Optional, Union 4 | 5 | 6 | class BaseStructuredFormatter(Formatter): 7 | """Base class for custom formatters that extract specific fields from log 8 | records based on a format string. 9 | 10 | Attributes: 11 | ---------- 12 | specifiers (List[str]): List of specifiers extracted from the provided format string. 13 | 14 | """ 15 | 16 | def __init__( 17 | self, fmt: Optional[str] = None, datefmt: Optional[str] = None 18 | ) -> None: 19 | """Initializes the formatter by extracting the format specifiers from 20 | the format string. 21 | 22 | Args: 23 | ---- 24 | fmt (Optional[str]): The log format string, e.g., "%(levelname)s | %(asctime)s | %(message)s". 25 | datefmt (Optional[str]): The date format string for formatting 'asctime'. 26 | 27 | """ 28 | super().__init__(fmt, datefmt) 29 | self.specifiers = self._extract_specifiers(fmt) 30 | 31 | def _extract_specifiers(self, fmt: Optional[str]) -> List[str]: 32 | """Extracts format specifiers (e.g., %(levelname)s) from the format 33 | string. 34 | 35 | Args: 36 | ---- 37 | fmt (Optional[str]): The format string to extract specifiers from. 38 | 39 | Returns: 40 | ------- 41 | List[str]: A list of extracted specifier names. 42 | 43 | """ 44 | if fmt is None: 45 | return [] 46 | return re.findall(r"%\((.*?)\)", fmt) 47 | 48 | def _get_field_value(self, record: LogRecord, specifier: str) -> Optional[Any]: 49 | """Retrieves the value for a given specifier from the log record. 50 | 51 | Args: 52 | ---- 53 | record (logging.LogRecord): The log record object. 54 | specifier (str): The field name to retrieve from the log record. 55 | 56 | Returns: 57 | ------- 58 | Optional[Any]: The value of the field, or None if the field is not found. 59 | 60 | """ 61 | if specifier == "message": 62 | return record.getMessage() 63 | elif specifier == "asctime": 64 | return self.formatTime(record, self.datefmt) 65 | elif hasattr(record, specifier): 66 | return getattr(record, specifier) 67 | return None 68 | 69 | def _handle_complex_value( 70 | self, value: Any 71 | ) -> Union[str, Dict[str, Any], List[Any]]: 72 | """Recursively handles complex values such as dictionaries, lists, and 73 | datetime objects. 74 | 75 | Args: 76 | ---- 77 | value (Any): The value to process. 78 | 79 | Returns: 80 | ------- 81 | Union[str, Dict[str, Any], List[Any]]: The processed value. 82 | 83 | """ 84 | if isinstance(value, dict): 85 | return {k: self._handle_complex_value(v) for k, v in value.items()} 86 | elif isinstance(value, (list, tuple)): 87 | return [self._handle_complex_value(v) for v in value] 88 | 89 | return str(value) 90 | 91 | def _add_exception(self, record: LogRecord, data: Dict[str, Any]) -> None: 92 | """Adds exception information to the data structure, if present in the 93 | log record. 94 | 95 | Args: 96 | ---- 97 | record (logging.LogRecord): The log record object. 98 | data (Dict[str, Any]): The dictionary to which exception information will be added. 99 | 100 | """ 101 | if record.exc_info: 102 | data["exception"] = self.formatException(record.exc_info) 103 | -------------------------------------------------------------------------------- /django_logging/formatters/colored_formatter.py: -------------------------------------------------------------------------------- 1 | from logging import Formatter, LogRecord 2 | 3 | from django_logging.settings.conf import LogConfig 4 | from django_logging.utils.console_colorizer import colorize_log_format 5 | 6 | 7 | class ColoredFormatter(Formatter): 8 | def format(self, record: LogRecord) -> str: 9 | original_format = self._style._fmt 10 | 11 | # checks that the format does not have any color it's self 12 | if LogConfig.remove_ansi_escape_sequences(original_format) == original_format: 13 | colorized_format = colorize_log_format(original_format, record.levelname) 14 | self._style._fmt = colorized_format 15 | 16 | formatted_output = super().format(record) 17 | 18 | # Reset to the original format string 19 | self._style._fmt = original_format 20 | 21 | return formatted_output 22 | -------------------------------------------------------------------------------- /django_logging/formatters/flat_formatter.py: -------------------------------------------------------------------------------- 1 | from logging import LogRecord 2 | 3 | from django_logging.formatters.base import BaseStructuredFormatter 4 | 5 | 6 | class FLATFormatter(BaseStructuredFormatter): 7 | """A custom log formatter that formats log records as a single flat line 8 | string, with key-value pairs like `asctime='2019-04-13' level='INFO'`.""" 9 | 10 | def format(self, record: LogRecord) -> str: 11 | """Formats the log record as a flat line string. 12 | 13 | Args: 14 | ---- 15 | record (logging.LogRecord): The log record object. 16 | 17 | Returns: 18 | ------- 19 | str: The formatted flat line string. 20 | 21 | """ 22 | # Build the flat line string based on the specifiers 23 | flat_line = " ".join( 24 | f"{specifier}='{self._get_field_value(record, specifier)}'" 25 | for specifier in self.specifiers 26 | if self._get_field_value(record, specifier) is not None 27 | ) 28 | 29 | # Add exception information if available 30 | if record.exc_info: 31 | flat_line += f" exception='{self.formatException(record.exc_info)}'" 32 | 33 | return flat_line 34 | -------------------------------------------------------------------------------- /django_logging/formatters/json_formatter.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | import re 4 | from logging import LogRecord 5 | from typing import Any 6 | 7 | from django_logging.formatters.base import BaseStructuredFormatter 8 | 9 | 10 | class JSONFormatter(BaseStructuredFormatter): 11 | """A custom log formatter that formats log records as JSON strings, and 12 | converts 'key=value' patterns in the log message to key-value pairs in 13 | JSON. 14 | 15 | It also handles complex types such as lists, dicts, and tuples. 16 | 17 | """ 18 | 19 | key_value_pattern = re.compile( 20 | r"(?P\w+)=(?P\{.*?\}|\[.*?\]|\(.*?\)|\S+)" 21 | ) 22 | 23 | def format(self, record: LogRecord) -> str: 24 | """Formats the log record as a JSON string, and converts 'key=value' 25 | patterns in the log message to key-value pairs in JSON. 26 | 27 | Args: 28 | ---- 29 | record (logging.LogRecord): The log record object. 30 | 31 | Returns: 32 | ------- 33 | str: The formatted JSON string. 34 | 35 | """ 36 | # Format the log data based on specifiers 37 | log_data = { 38 | specifier: self._handle_complex_value( 39 | self._get_field_value(record, specifier) 40 | ) 41 | for specifier in self.specifiers 42 | } 43 | 44 | # Parse 'key=value' pairs from the message if present 45 | message = record.getMessage() 46 | key_value_pairs = self._extract_key_value_pairs(message) 47 | 48 | # If key-value pairs are extracted, update the log data and remove them from the message 49 | if key_value_pairs: 50 | log_data.update(key_value_pairs) 51 | message = self._remove_key_value_pairs(message) 52 | 53 | # Clean up the message: remove \n and \t 54 | message = self._clean_message(message) 55 | 56 | # Update the message field with the cleaned-up version 57 | log_data["message"] = message 58 | 59 | # Add any exception information if available 60 | self._add_exception(record, log_data) 61 | 62 | # Return the log data as a formatted JSON string 63 | return json.dumps(log_data, indent=2) 64 | 65 | def _extract_key_value_pairs(self, message: str) -> dict: 66 | """Extracts 'key=value' pairs from the log message and returns them as 67 | a dictionary. Supports complex structures like dict, list, and tuple. 68 | 69 | Args: 70 | ---- 71 | message (str): The log message string. 72 | 73 | Returns: 74 | ------- 75 | dict: A dictionary of extracted key-value pairs. 76 | 77 | """ 78 | key_value_dict = {} 79 | for match in self.key_value_pattern.finditer(message): 80 | key = match.group("key") 81 | value = match.group("value") 82 | 83 | # Try to interpret the value as a dict, list, tuple, or other primitive types 84 | key_value_dict[key] = self._convert_value(value) 85 | 86 | return key_value_dict 87 | 88 | def _remove_key_value_pairs(self, message: str) -> str: 89 | """Removes key=value pairs from the log message string to avoid 90 | duplication. 91 | 92 | Args: 93 | ---- 94 | message (str): The original log message string. 95 | 96 | Returns: 97 | ------- 98 | str: The cleaned-up message string without key=value pairs. 99 | 100 | """ 101 | # Replace the key=value pairs in the message with an empty string 102 | return self.key_value_pattern.sub("", message).strip() 103 | 104 | def _clean_message(self, message: str) -> str: 105 | """Cleans up the log message by removing any '\n' (newlines) and '\t' 106 | (tabs). 107 | 108 | Args: 109 | ---- 110 | message (str): The log message string to clean. 111 | 112 | Returns: 113 | ------- 114 | str: The cleaned message without newlines and tabs. 115 | 116 | """ 117 | return message.replace("\n", " ").replace("\t", " ").strip() 118 | 119 | def _convert_value(self, value: str) -> Any: 120 | """Tries to convert a string value to an appropriate type (int, float, 121 | bool, dict, list, tuple). If conversion fails, returns the value as a 122 | string. 123 | 124 | Args: 125 | ---- 126 | value (str): The string value to convert. 127 | 128 | Returns: 129 | ------- 130 | any: The converted value. 131 | 132 | """ 133 | if value.lower() in ("true", "false"): 134 | return value.lower() == "true" 135 | 136 | try: 137 | # Use ast.literal_eval to safely parse dict, list, or tuple from the string 138 | return ast.literal_eval(value) 139 | except (ValueError, SyntaxError): 140 | # If it's not a valid literal, return the original string 141 | return value 142 | -------------------------------------------------------------------------------- /django_logging/formatters/xml_formatter.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET # nosec B405 2 | from logging import LogRecord 3 | from typing import Any 4 | from xml.dom import minidom # nosec B408 5 | 6 | from django_logging.formatters.base import BaseStructuredFormatter 7 | 8 | 9 | class XMLFormatter(BaseStructuredFormatter): 10 | """A custom log formatter that formats log records as XML strings.""" 11 | 12 | def format(self, record: LogRecord) -> str: 13 | """Converts the log record to an XML string. 14 | 15 | Args: 16 | ---- 17 | record (logging.LogRecord): The log record object. 18 | 19 | Returns: 20 | ------- 21 | str: The formatted XML string. 22 | 23 | """ 24 | log_element = ET.Element("log") 25 | for specifier in self.specifiers: 26 | value = self._get_field_value(record, specifier) 27 | if value not in [None, ""]: 28 | self._add_field_to_xml( 29 | log_element, specifier, self._handle_complex_value(value) 30 | ) 31 | 32 | self._add_exception_to_xml(record, log_element) 33 | return self._pretty_print_xml(ET.tostring(log_element, encoding="unicode")) 34 | 35 | def _add_field_to_xml( 36 | self, parent_element: ET.Element, field_name: str, field_value: Any 37 | ) -> None: 38 | """Adds a field and its value to the XML structure. 39 | 40 | Args: 41 | ---- 42 | parent_element (ET.Element): The parent XML element. 43 | field_name (str): The name of the field. 44 | field_value (Any): The value of the field. 45 | 46 | """ 47 | field_element = ET.SubElement(parent_element, field_name) 48 | if isinstance(field_value, dict): 49 | for sub_key, sub_value in field_value.items(): 50 | sub_element = ET.SubElement(field_element, sub_key) 51 | sub_element.text = str(sub_value) 52 | 53 | elif isinstance(field_value, (list, tuple)): 54 | for index, item in enumerate(field_value): 55 | sub_element = ET.SubElement(field_element, f"item_{index}") 56 | sub_element.text = str(item) 57 | else: 58 | field_element.text = str(field_value) 59 | 60 | def _add_exception_to_xml( 61 | self, record: LogRecord, parent_element: ET.Element 62 | ) -> None: 63 | """Adds exception information to the XML structure, if present in the 64 | log record. 65 | 66 | Args: 67 | ---- 68 | record (logging.LogRecord): The log record object. 69 | parent_element (ET.Element): The parent XML element to which exception info will be added. 70 | 71 | """ 72 | if record.exc_info: 73 | exception_element = ET.SubElement(parent_element, "exception") 74 | exception_element.text = self.formatException(record.exc_info) 75 | 76 | def _pretty_print_xml(self, xml_string: str) -> str: 77 | """Pretty-prints the XML string. 78 | 79 | Args: 80 | ---- 81 | xml_string (str): The raw XML string. 82 | 83 | Returns: 84 | ------- 85 | str: The pretty-printed XML string. 86 | 87 | """ 88 | dom = minidom.parseString(xml_string) # nosec B318 89 | return dom.toprettyxml(indent=" ", newl="\n").split("?>", 1)[-1].strip() 90 | -------------------------------------------------------------------------------- /django_logging/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .email_handler import EmailHandler 2 | -------------------------------------------------------------------------------- /django_logging/handlers/email_handler.py: -------------------------------------------------------------------------------- 1 | from logging import Handler, LogRecord 2 | from typing import Optional 3 | 4 | from django.conf import settings 5 | from django.http import HttpRequest 6 | from django.template import engines 7 | from django.utils.timezone import localtime 8 | 9 | from django_logging.middleware import RequestLogMiddleware 10 | from django_logging.utils.get_conf import use_email_notifier_template 11 | from django_logging.utils.log_email_notifier.notifier import send_email_async 12 | 13 | 14 | class EmailHandler(Handler): 15 | """A custom logging handler that sends log records via email. 16 | 17 | This handler formats log records, optionally renders them using an 18 | email template, and sends the resulting email to the administrator's 19 | email address defined in the Django settings. 20 | 21 | Methods: 22 | ------- 23 | emit(record: LogRecord) -> None: 24 | Processes a log record and sends it via email to the administrator. 25 | 26 | render_template(log_entry: str, request: Optional[HttpRequest] = None, template_path: str = "email_notifier_template.html") -> str: 27 | Renders the email body using the provided log entry and optional request details. 28 | The rendered email includes the log message, the current date and time, 29 | the user's IP address, and browser information. 30 | 31 | """ 32 | 33 | def emit(self, record: LogRecord) -> None: 34 | """Processes a log record and sends it via email. 35 | 36 | This method retrieves the request from the log record (if available), formats 37 | the log message, optionally renders the email body using a template, and sends 38 | the email asynchronously to the administrator. 39 | 40 | Args: 41 | ---- 42 | record (LogRecord): The log record to be processed and sent via email. 43 | 44 | Raises: 45 | ------ 46 | Exception: If any error occurs while sending the email or formatting the log record. 47 | 48 | """ 49 | try: 50 | request = getattr(record, "request", None) 51 | log_entry = self.format(record) 52 | 53 | if use_email_notifier_template(): 54 | email_body = self.render_template(log_entry, request) 55 | else: 56 | email_body = log_entry 57 | 58 | subject = f"New Log Record: {record.levelname}" 59 | send_email_async(subject, email_body, [settings.ADMIN_EMAIL]) 60 | 61 | except Exception: # pylint: disable=W0718 62 | self.handleError(record) 63 | 64 | @staticmethod 65 | def render_template( 66 | log_entry: str, 67 | request: Optional[HttpRequest] = None, 68 | template_path: str = "email_notifier_template.html", 69 | ) -> str: 70 | """Renders the email body using a Django template. 71 | 72 | This method uses the provided log entry and request (if available) 73 | to generate an HTML email body. The email includes details such as the 74 | log message, current date and time, the IP address, and browser type 75 | of the user making the request. 76 | 77 | Args: 78 | ---- 79 | log_entry (str): The formatted log message to be included in the email. 80 | request (Optional[HttpRequest]): The HTTP request associated with the log entry, if available. 81 | template_path (str): The path to the Django template to be used for rendering the email. 82 | Defaults to "email_notifier_template.html". 83 | 84 | Returns: 85 | ------- 86 | str: The rendered email body as a string. 87 | 88 | """ 89 | django_engine = engines["django"] 90 | template = django_engine.get_template(template_path) 91 | 92 | # Fetch IP address and user agent using middleware methods 93 | ip_address = ( 94 | RequestLogMiddleware.get_ip_address(request) if request else "Unknown" 95 | ) 96 | user_agent = ( 97 | RequestLogMiddleware.get_user_agent(request) if request else "Unknown" 98 | ) 99 | 100 | # Get current time 101 | current_time = localtime() 102 | 103 | # Format date and time separately 104 | formatted_date = current_time.strftime("%d %B %Y").replace( 105 | current_time.strftime("%B"), current_time.strftime("%B").upper() 106 | ) 107 | formatted_time = current_time.strftime("%I:%M %p") 108 | 109 | context = { 110 | "message": log_entry, 111 | "date": formatted_date, 112 | "time": formatted_time, 113 | "browser_type": user_agent, 114 | "ip_address": ip_address, 115 | } 116 | 117 | return template.render(context) 118 | -------------------------------------------------------------------------------- /django_logging/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/management/__init__.py -------------------------------------------------------------------------------- /django_logging/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/management/commands/__init__.py -------------------------------------------------------------------------------- /django_logging/management/commands/generate_pretty_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Dict, Tuple 4 | 5 | from django.core.management.base import BaseCommand 6 | 7 | from django_logging.settings import settings_manager 8 | from django_logging.utils.command.process_file import process_files, setup_directories 9 | 10 | 11 | class Command(BaseCommand): 12 | """A Django management command to find JSON files within a specified log 13 | directory and generate pretty JSON. 14 | 15 | The command looks for `.json` files inside the `json` subdirectory of the log directory, attempts to 16 | parse multiple JSON objects from a single file, and then formats them into a valid JSON array. 17 | 18 | The reformatted JSON content is saved in a `pretty` subdirectory with the prefix `formatted_`. 19 | 20 | """ 21 | 22 | help = "Find JSON files in log directory and generates pretty JSON" 23 | 24 | def handle(self, *args: Tuple, **kwargs: Dict) -> None: 25 | """Main command handler. This method retrieves the log directory, sets 26 | up necessary directories, processes each `.json` file, and reformats 27 | the content. 28 | 29 | Args: 30 | *args: Additional positional arguments (not used). 31 | **kwargs: Additional keyword arguments (not used). 32 | 33 | """ 34 | log_dir = settings_manager.log_dir 35 | 36 | try: 37 | json_dir, pretty_dir = setup_directories(log_dir, "json") 38 | except FileNotFoundError as e: 39 | self.stdout.write(self.style.ERROR(str(e))) 40 | return 41 | 42 | for file_path, filename in process_files(json_dir, ".json", self.reformat_json): 43 | self.stdout.write(self.style.NOTICE(f"Processing file: {file_path}")) 44 | 45 | new_file_path: str = os.path.join(pretty_dir, f"formatted_{filename}") 46 | self.reformat_json(file_path, new_file_path) 47 | 48 | self.stdout.write( 49 | self.style.SUCCESS( 50 | f"File {filename} reformatted and generated new pretty file successfully." 51 | ) 52 | ) 53 | 54 | def reformat_json(self, file_path: str, new_file_path: str) -> None: 55 | """Parses multiple JSON objects from a file incrementally and writes 56 | them to a new file as a valid JSON array. 57 | 58 | Args: 59 | file_path (str): The path to the original JSON file. 60 | new_file_path (str): The path where the reformatted JSON file will be saved. 61 | 62 | """ 63 | with ( 64 | open(file_path, encoding="utf-8") as infile, 65 | open(new_file_path, "w", encoding="utf-8") as outfile, 66 | ): 67 | outfile.write("[\n") # Start the JSON array 68 | first_object = True # Flag to handle commas 69 | 70 | buffer = "" # This will accumulate the JSON content 71 | 72 | for line in infile: 73 | line = line.strip() 74 | 75 | if not line: 76 | continue # Skip empty lines 77 | 78 | buffer += line 79 | 80 | # Try to parse the current buffer as a complete JSON object 81 | try: 82 | json_object = json.loads(buffer) 83 | json_line = json.dumps(json_object, indent=4) 84 | 85 | if not first_object: 86 | outfile.write(",\n") # Add a comma before subsequent objects 87 | outfile.write(json_line) 88 | 89 | first_object = False 90 | buffer = "" # Clear the buffer after successful parsing 91 | except json.JSONDecodeError: 92 | # Keep accumulating if it's not a complete JSON object yet 93 | continue 94 | 95 | if buffer: 96 | # If any partial JSON is left in the buffer, log an error 97 | self.stdout.write(self.style.ERROR(f"Incomplete JSON object: {buffer}")) 98 | 99 | outfile.write("\n]") # End the JSON array 100 | -------------------------------------------------------------------------------- /django_logging/management/commands/generate_pretty_xml.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict, Tuple 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | from django_logging.settings import settings_manager 7 | from django_logging.utils.command.process_file import process_files, setup_directories 8 | 9 | 10 | class Command(BaseCommand): 11 | """Command to find and reformat XML files in a specified directory. 12 | 13 | This command processes all XML files in the specified log directory, reformats 14 | them by wrapping their content in a element, and saves the reformatted 15 | files to a new directory. It handles parsing errors and logs the process steps. 16 | 17 | Attributes: 18 | help (str): A brief description of the command's functionality. 19 | 20 | """ 21 | 22 | help = "Find and reformat XML files in a directory" 23 | 24 | def handle(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None: 25 | """Handles the command execution. 26 | 27 | Args: 28 | *args: Positional arguments passed to the command. 29 | **kwargs: Keyword arguments passed to the command. 30 | 31 | """ 32 | log_dir = settings_manager.log_dir 33 | 34 | try: 35 | xml_dir, pretty_dir = setup_directories(log_dir, "xml") 36 | except FileNotFoundError as e: 37 | self.stdout.write(self.style.ERROR(str(e))) 38 | return 39 | 40 | for file_path, filename in process_files( 41 | xml_dir, ".xml", self.reformat_and_write_xml 42 | ): 43 | self.stdout.write(self.style.NOTICE(f"Processing file: {file_path}")) 44 | 45 | new_file_path = os.path.join(pretty_dir, f"formatted_{filename}") 46 | self.reformat_and_write_xml(file_path, new_file_path) 47 | self.stdout.write( 48 | self.style.SUCCESS(f"File {filename} reformatted successfully.") 49 | ) 50 | 51 | def reformat_and_write_xml(self, file_path: str, new_file_path: str) -> None: 52 | """Reformats XML content by wrapping it in a element and writes 53 | it to a new file. 54 | 55 | Args: 56 | file_path (str): The path of the original XML file to be reformatted. 57 | new_file_path (str): The path where the reformatted XML file will be saved. 58 | 59 | """ 60 | with ( 61 | open(file_path, encoding="utf-8") as infile, 62 | open(new_file_path, "w", encoding="utf-8") as outfile, 63 | ): 64 | # Start the element 65 | outfile.write("\n") 66 | 67 | for line in infile: 68 | # Write each line to the formatted file 69 | if line.strip(): # Only process non-empty lines 70 | outfile.write(line) 71 | 72 | # End the element 73 | outfile.write("\n") 74 | -------------------------------------------------------------------------------- /django_logging/management/commands/logs_size_audit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Any, Dict, Tuple 4 | 5 | from django.conf import settings 6 | from django.core.management.base import BaseCommand 7 | 8 | from django_logging.handlers import EmailHandler 9 | from django_logging.management.commands.send_logs import Command as cmd 10 | from django_logging.settings import settings_manager 11 | from django_logging.utils.get_conf import ( 12 | get_log_dir_size_limit, 13 | use_email_notifier_template, 14 | ) 15 | from django_logging.utils.log_email_notifier.notifier import send_email_async 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class Command(BaseCommand): 21 | """Command to check the total size of the logs directory and send a warning 22 | if it exceeds the limit. 23 | 24 | This command calculates the total size of the log directory and sends an email notification 25 | to the admin if the size exceeds the configured limit. 26 | 27 | Attributes: 28 | help (str): A brief description of the command's functionality. 29 | 30 | """ 31 | 32 | help = "Check the total size of the logs directory and send a warning if it exceeds the limit" 33 | 34 | def handle(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None: 35 | """Handles the command execution. 36 | 37 | Args: 38 | *args: Positional arguments passed to the command. 39 | **kwargs: Keyword arguments passed to the command. 40 | 41 | """ 42 | log_dir = settings_manager.log_dir 43 | 44 | # Check if log directory exists 45 | if not os.path.exists(log_dir): 46 | self.stdout.write(self.style.ERROR(f"Log directory not found: {log_dir}")) 47 | logger.error("Log directory not found: %s", log_dir) 48 | return 49 | 50 | # pylint: disable=attribute-defined-outside-init 51 | self.size_limit: int = get_log_dir_size_limit() 52 | 53 | # Calculate the total size of the log directory 54 | total_size = self.get_directory_size(log_dir) 55 | total_size_mb = float(f"{total_size / (1024 * 1024):.2f}") 56 | 57 | logger.info("Total log directory size: %s MB", total_size_mb) 58 | 59 | if int(total_size_mb) >= self.size_limit: 60 | cmd.validate_email_settings() 61 | # Send warning email if total size exceeds the size limit 62 | self.send_warning_email(total_size_mb) 63 | self.stdout.write(self.style.SUCCESS("Warning email sent successfully.")) 64 | logger.info("Warning email sent successfully.") 65 | else: 66 | self.stdout.write( 67 | self.style.SUCCESS( 68 | f"Log directory size is under the limit: {total_size_mb} MB" 69 | ) 70 | ) 71 | logger.info("Log directory size is under the limit: %s MB", total_size_mb) 72 | 73 | # pylint: disable=unused-variable 74 | def get_directory_size(self, dir_path: str) -> int: 75 | """Calculate the total size of all files in the directory. 76 | 77 | Args: 78 | dir_path (str): The path of the directory to calculate size for. 79 | 80 | Returns: 81 | int: The total size of the directory in bytes. 82 | 83 | """ 84 | total_size = 0 85 | for root, dirs, files in os.walk(dir_path): 86 | for file in files: 87 | file_path = os.path.join(root, file) 88 | total_size += os.path.getsize(file_path) 89 | 90 | return total_size 91 | 92 | def send_warning_email(self, total_size_mb: float) -> None: 93 | """Send an email warning to the admin about the log directory size. 94 | 95 | Args: 96 | total_size_mb (int): The total size of the log directory in Megabytes. 97 | 98 | """ 99 | 100 | subject = "Logs Directory Size Warning" 101 | recipient_list = [settings.ADMIN_EMAIL] 102 | message = ( 103 | f"The size of the log files has exceeded {self.size_limit} MB.\n\n" 104 | f"Current size: {total_size_mb} MB\n" 105 | ) 106 | 107 | email_body = message 108 | 109 | if use_email_notifier_template(): 110 | email_body = EmailHandler.render_template(message) 111 | 112 | send_email_async( 113 | subject=subject, recipient_list=recipient_list, body=email_body 114 | ) 115 | logger.info( 116 | "Email has been sent to %s regarding log size warning.", 117 | settings.ADMIN_EMAIL, 118 | ) 119 | -------------------------------------------------------------------------------- /django_logging/management/commands/send_logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | import tempfile 5 | from argparse import ArgumentParser 6 | from typing import Dict, Tuple 7 | 8 | from django.conf import settings 9 | from django.core.exceptions import ImproperlyConfigured 10 | from django.core.mail import EmailMessage 11 | from django.core.management.base import BaseCommand 12 | 13 | from django_logging.settings import settings_manager 14 | from django_logging.validators.email_settings_validator import check_email_settings 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class Command(BaseCommand): 20 | """A Django management command that zips the log directory and sends it to 21 | the specified email address. 22 | 23 | This command is used to send the log files to a specified email 24 | address. It zips the log directory, creates an email with the zipped 25 | file as an attachment, and sends it to the specified email address. 26 | 27 | """ 28 | 29 | help = "Send log folder to the specified email address" 30 | 31 | def add_arguments(self, parser: ArgumentParser) -> None: 32 | """Add custom command arguments. 33 | 34 | Parameters: 35 | parser (ArgumentParser): The argument parser to add arguments to. 36 | 37 | """ 38 | parser.add_argument( 39 | "email", type=str, help="The email address to send the logs to" 40 | ) 41 | 42 | def handle(self, *args: Tuple, **kwargs: Dict) -> None: 43 | """The main entry point for the command. 44 | 45 | Parameters: 46 | args (tuple): Positional arguments. 47 | kwargs (dict): Keyword arguments. 48 | 49 | """ 50 | email: str = kwargs["email"] # type: ignore 51 | 52 | log_dir = settings_manager.log_dir 53 | 54 | if not os.path.exists(log_dir): 55 | self.stdout.write( 56 | self.style.ERROR(f'Log directory "{log_dir}" does not exist.') 57 | ) 58 | logger.error("Log directory '%s' does not exist.", log_dir) 59 | return 60 | 61 | self.validate_email_settings() 62 | 63 | # Create a temporary file to store the zipped logs 64 | with tempfile.NamedTemporaryFile(delete=False) as tmp_file: 65 | zip_path = f"{tmp_file.name}.zip" 66 | tmp_file.close() 67 | 68 | # Zip the log directory 69 | shutil.make_archive(tmp_file.name, "zip", log_dir) 70 | 71 | # Send the email with the zipped logs 72 | email_subject = "Log Files" 73 | email_body = "Please find the attached log files." 74 | email_message = EmailMessage( 75 | subject=email_subject, 76 | body=email_body, 77 | from_email=settings.DEFAULT_FROM_EMAIL, 78 | to=[email], 79 | ) 80 | email_message.attach_file(zip_path) 81 | 82 | try: 83 | email_message.send() 84 | self.stdout.write(self.style.SUCCESS(f"Logs sent successfully to {email}.")) 85 | logger.info("Logs sent successfully to %s.", email) 86 | except Exception as e: # pylint: disable=W0718 87 | self.stdout.write(self.style.ERROR(f"Failed to send logs: {e}")) 88 | logger.error("Failed to send logs: %s", e) 89 | finally: 90 | # Clean up the temporary file if exists 91 | if os.path.exists(zip_path): 92 | os.remove(zip_path) 93 | logger.info("Temporary zip file cleaned up successfully.") 94 | 95 | @staticmethod 96 | def validate_email_settings(require_admin_email: bool = False) -> None: 97 | """Check if all required email settings are present in the settings 98 | file. 99 | 100 | Raises ImproperlyConfigured if any of the required email 101 | settings are missing. 102 | 103 | """ 104 | errors = check_email_settings(require_admin_email=require_admin_email) 105 | if errors: 106 | logger.error(errors) 107 | raise ImproperlyConfigured(errors) 108 | -------------------------------------------------------------------------------- /django_logging/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from .monitor_log_size import MonitorLogSizeMiddleware 2 | from .request_middleware import RequestLogMiddleware 3 | -------------------------------------------------------------------------------- /django_logging/middleware/base.py: -------------------------------------------------------------------------------- 1 | from typing import Awaitable, Callable, Union 2 | 3 | from asgiref.sync import iscoroutinefunction, markcoroutinefunction 4 | from django.http import HttpRequest, HttpResponseBase 5 | 6 | 7 | class BaseMiddleware: 8 | sync_capable: bool = True 9 | async_capable: bool = True 10 | 11 | def __init__( 12 | self, 13 | get_response: Callable[ 14 | [HttpRequest], Union[HttpResponseBase, Awaitable[HttpResponseBase]] 15 | ], 16 | ) -> None: 17 | self.get_response = get_response 18 | self.async_mode = iscoroutinefunction(self.get_response) 19 | if self.async_mode: 20 | markcoroutinefunction(self) 21 | 22 | def __repr__(self) -> str: 23 | """Provides a string representation of the middleware.""" 24 | ger_response = getattr( 25 | self.get_response, 26 | "__qualname__", 27 | self.get_response.__class__.__name__, 28 | ) 29 | return f"<{self.__class__.__qualname__} get_response={ger_response}>" 30 | 31 | def __call__( 32 | self, request: HttpRequest 33 | ) -> Union[HttpResponseBase, Awaitable[HttpResponseBase]]: 34 | """Handles the incoming request, determining whether it's synchronous 35 | or asynchronous. 36 | 37 | Args: 38 | request (HttpRequest): The incoming HTTP request. 39 | 40 | Returns: 41 | Union[HttpResponseBase, Awaitable[HttpResponseBase]]: The HTTP response, either synchronous or asynchronous. 42 | 43 | """ 44 | if self.async_mode: 45 | return self.__acall__(request) 46 | return self.__sync_call__(request) 47 | 48 | def __sync_call__(self, request: HttpRequest) -> HttpResponseBase: 49 | raise NotImplementedError("__sync_call__ must be implemented by subclass") 50 | 51 | async def __acall__(self, request: HttpRequest) -> HttpResponseBase: 52 | raise NotImplementedError("__acall__ must be implemented by subclass") 53 | -------------------------------------------------------------------------------- /django_logging/middleware/monitor_log_size.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | 4 | from asgiref.sync import sync_to_async 5 | from django.core.cache import cache 6 | from django.core.management import call_command 7 | from django.http import HttpRequest, HttpResponseBase 8 | from django.utils.timezone import now 9 | 10 | from django_logging.middleware.base import BaseMiddleware 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class MonitorLogSizeMiddleware(BaseMiddleware): 16 | """Middleware that monitors the size of the log directory in both 17 | synchronous and asynchronous modes. 18 | 19 | This middleware checks if a week has passed since the last log size audit. If so, it runs 20 | the 'logs_size_audit' management command, which checks the log directory's total size. 21 | If the size exceeds a configured limit, a warning email is sent to the admin. 22 | 23 | Attributes: 24 | ---------- 25 | get_response (Callable[[HttpRequest], Union[HttpResponseBase, Awaitable[HttpResponseBase]]]): 26 | The next middleware or view to be called. 27 | 28 | """ 29 | 30 | def __sync_call__(self, request: HttpRequest) -> HttpResponseBase: 31 | """Synchronous request processing. 32 | 33 | Args: 34 | ---- 35 | request (HttpRequest): The current HTTP request being processed. 36 | 37 | Returns: 38 | ------- 39 | HttpResponseBase: The HTTP response returned by the next middleware or view. 40 | 41 | """ 42 | if self.should_run_task(): 43 | self.run_log_size_check() 44 | cache.set("last_run_logs_size_audit", now(), timeout=None) 45 | 46 | return self.get_response(request) 47 | 48 | async def __acall__(self, request: HttpRequest) -> HttpResponseBase: 49 | """Asynchronous request processing. 50 | 51 | Args: 52 | ---- 53 | request (HttpRequest): The current HTTP request being processed. 54 | 55 | Returns: 56 | ------- 57 | HttpResponseBase: The HTTP response returned by the next middleware or view. 58 | 59 | """ 60 | if await sync_to_async(self.should_run_task)(): 61 | await sync_to_async(self.run_log_size_check)() 62 | await sync_to_async(cache.set)( 63 | "last_run_logs_size_audit", now(), timeout=None 64 | ) 65 | 66 | return await self.get_response(request) 67 | 68 | @staticmethod 69 | def should_run_task() -> bool: 70 | """Determines if a week has passed since the last log size audit. 71 | 72 | Returns: 73 | ------- 74 | bool: True if a week has passed since the last audit, False otherwise. 75 | 76 | """ 77 | last_run = cache.get("last_run_logs_size_audit") 78 | if last_run is None or now() - last_run > timedelta(weeks=1): 79 | return True 80 | 81 | return False 82 | 83 | def run_log_size_check(self) -> None: 84 | """Runs the 'logs_size_audit' management command to check the log 85 | directory size. 86 | 87 | If an error occurs during the execution of the command, it is 88 | logged. 89 | 90 | """ 91 | logger.info("Running 'logs_size_audit' command...") 92 | try: 93 | call_command("logs_size_audit") 94 | except Exception as e: # pylint: disable=W0718 95 | logger.error("Error running 'logs_size_audit' command: %s", e) 96 | -------------------------------------------------------------------------------- /django_logging/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import settings_manager 2 | -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/css/error_response.css: -------------------------------------------------------------------------------- 1 | *{ 2 | padding: 0px; 3 | margin: 0px; 4 | box-sizing: border-box; 5 | font-family: Lora; 6 | } 7 | body{ 8 | width: 100%; 9 | height: 100vh; 10 | background-color: #D5F4FF; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | main{ 16 | width: 30%; 17 | height: 45%; 18 | border-radius: 20px; 19 | background-color: rgba(1,0,4); 20 | background: radial-gradient(circle, rgba(1,0,4,1) 40% , rgb(4, 3, 22) 60%); 21 | box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px; 22 | display: grid; 23 | grid-template-rows: 1fr 1.5fr 1fr; 24 | padding: 20px 10px; 25 | } 26 | .error-img{ 27 | width: 100%; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | } 32 | .error-img img{ 33 | margin: auto; 34 | width: 70px; 35 | height: 70px; 36 | } 37 | .error-para{ 38 | color: aliceblue; 39 | width: 100%; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | .error-para h3{ 45 | color: antiquewhite; 46 | font-size: 0.9em; 47 | } 48 | .made{ 49 | width: 100%; 50 | display: flex; 51 | flex-direction: row; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | .made div{ 56 | width: 7px; 57 | height: 7px; 58 | background-color: rgb(130, 8, 8);border-radius: 50%; 59 | margin-right: 8px; 60 | } 61 | .made p{ 62 | text-align: center; 63 | color: rgb(150, 149, 150); 64 | font-size: 0.7em; 65 | } 66 | -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/Lazarus.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/Lazarus.jpeg -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/close-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/close-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/done-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/done-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/exclamation-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/exclamation-mark.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/file-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/file-icon.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/folder-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/folder-icon.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/json-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/json-icon.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/multiply-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/multiply-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/open-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/open-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/pause-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/pause-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/search-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/search-icon.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/stop-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/stop-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/txt-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/txt-icon.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/upload-to-cloud-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/upload-to-cloud-50.png -------------------------------------------------------------------------------- /django_logging/static/LogiBoard/imgs/xml-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/static/LogiBoard/imgs/xml-icon.png -------------------------------------------------------------------------------- /django_logging/templates/email_notifier_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Log Record Notification 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 64 | 65 |
15 | 16 | 17 | 24 | 25 | 26 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | 61 | 62 |
18 |
19 | 20 | LAZARUS 21 |
22 | 23 |
27 | 28 | 29 | 32 | 37 | 38 |
30 | {{date}} 31 | 33 |

34 | Email prepared at {{time}} 35 |

36 |
39 |
44 |

Message:

45 |

{{ message }}

46 | {% if ip_address != "Unknown" %} 47 |

IP Address:

48 |

{{ ip_address }}

49 | {% endif %} 50 | {% if browser_type != "Unknown" %} 51 |

Browser Type:

52 |

{{ browser_type }}

53 | {% endif %} 54 |
58 |

This is an automated message.

59 |

Please do not reply.

60 |
63 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /django_logging/templates/error_response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | exclamation-mark 23 |
24 | 25 |
26 |

{{ message }}

27 |
28 | 29 |
30 |
31 |

Made by Lazarus

32 |
33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /django_logging/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/commands/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/commands/test_generate_pretty_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from io import StringIO 5 | from pathlib import Path 6 | from typing import Any 7 | from unittest.mock import patch, Mock 8 | 9 | import pytest 10 | from django.core.management import call_command 11 | 12 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 13 | 14 | pytestmark = [ 15 | pytest.mark.commands, 16 | pytest.mark.commands_generate_pretty_json, 17 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 18 | ] 19 | 20 | 21 | class TestJsonReformatCommand: 22 | """ 23 | Test suite for the Django management command that reformats JSON files in a log directory. 24 | 25 | This test suite verifies the functionality of the command, which searches for `.json` files, 26 | parses multiple JSON objects, and saves them in a 'pretty' subdirectory in a valid JSON array format. 27 | """ 28 | 29 | @patch( 30 | "django_logging.management.commands.generate_pretty_json.settings_manager" 31 | ) 32 | def test_command_successful_processing( 33 | self, settings: Mock, temp_json_log_directory: str 34 | ) -> None: 35 | """ 36 | Test the successful processing and pretty-printing of JSON files. 37 | 38 | This test verifies that the command: 39 | 1. Processes JSON files in the 'json' directory. 40 | 2. Writes pretty JSON arrays into the 'pretty' subdirectory. 41 | 3. Logs the successful processing of files. 42 | 43 | Args: 44 | temp_json_log_directory (str): Path to the temporary log directory. 45 | settings (django.conf.Settings): Django settings. 46 | """ 47 | settings.log_dir = temp_json_log_directory 48 | 49 | out = StringIO() 50 | call_command("generate_pretty_json", stdout=out) 51 | 52 | # Check output 53 | assert "Processing file" in out.getvalue(), f"Output: {out.getvalue()}" 54 | assert ( 55 | "reformatted and generated new pretty file successfully" in out.getvalue() 56 | ), f"Output: {out.getvalue()}" 57 | 58 | # Verify that the formatted JSON file exists in the pretty directory 59 | pretty_dir = os.path.join(settings.log_dir, "json", "pretty") 60 | formatted_file = os.path.join(pretty_dir, "formatted_test.json") 61 | assert os.path.exists(formatted_file), f"File {formatted_file} not found." 62 | 63 | # Load and verify the content of the generated pretty file 64 | with open(formatted_file) as f: 65 | data = json.load(f) 66 | assert isinstance(data, list) 67 | assert len(data) == 2 68 | assert data[0]["key"] == "value" 69 | 70 | @patch( 71 | "django_logging.management.commands.generate_pretty_json.settings_manager" 72 | ) 73 | def test_command_file_not_found_error(self, settings: Any) -> None: 74 | """ 75 | Test handling of FileNotFoundError when the log directory does not exist. 76 | 77 | This test checks that the command logs an error when it fails to find the specified log directory. 78 | 79 | Args: 80 | settings (django.conf.Settings): Django settings. 81 | """ 82 | out = StringIO() 83 | call_command("generate_pretty_json", stdout=out) 84 | 85 | # Check if the command logs the directory not found error 86 | assert "does not exist." in out.getvalue() 87 | 88 | @patch( 89 | "django_logging.management.commands.generate_pretty_json.settings_manager" 90 | ) 91 | def test_command_invalid_json(self, settings: Any, temp_json_log_directory: str) -> None: 92 | """ 93 | Test the command's handling of invalid JSON files. 94 | 95 | This test verifies that the command logs a JSONDecodeError when it encounters invalid JSON content. 96 | 97 | Args: 98 | temp_json_log_directory (str): Path to the temporary log directory. 99 | settings (django.conf.Settings): Django settings. 100 | """ 101 | settings.log_dir = temp_json_log_directory 102 | 103 | # Create a faulty JSON file with invalid syntax. 104 | faulty_json_file = Path(temp_json_log_directory) / "json" / "faulty.json" 105 | faulty_json_file.write_text( 106 | """ 107 | {"key": "value", \n "key2" } 108 | 109 | """ 110 | ) # Invalid JSON 111 | 112 | out = StringIO() 113 | call_command("generate_pretty_json", stdout=out) 114 | 115 | assert "faulty.json" in out.getvalue() 116 | assert 'Incomplete JSON object: {"key": "value","key2" }' in out.getvalue() 117 | -------------------------------------------------------------------------------- /django_logging/tests/commands/test_generate_pretty_xml.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from io import StringIO 4 | from typing import Any 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | from django.core.management import call_command 9 | 10 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 11 | 12 | pytestmark = [ 13 | pytest.mark.commands, 14 | pytest.mark.commands_generate_pretty_xml, 15 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 16 | ] 17 | 18 | 19 | class TestGeneratePrettyXMLCommand: 20 | """ 21 | Test suite for the `generate_pretty_xml` management command. 22 | 23 | This test suite verifies the functionality of the command, which searches for `.xml` files, 24 | reformats them by wrapping their content in a element, and saves them in a 'pretty' subdirectory. 25 | """ 26 | 27 | @patch( 28 | "django_logging.management.commands.generate_pretty_xml.settings_manager" 29 | ) 30 | def test_command_successful_processing( 31 | self, settings: Any, temp_xml_log_directory: str 32 | ) -> None: 33 | """ 34 | Test the successful processing and reformatting of XML files. 35 | 36 | This test verifies that the command: 37 | 1. Processes XML files in the 'xml' directory. 38 | 2. Writes reformatted XML files into the 'pretty' subdirectory. 39 | 3. Logs the successful processing of files. 40 | 41 | Args: 42 | ---- 43 | temp_xml_log_directory (str): Path to the temporary log directory. 44 | settings (django.conf.Settings): Django settings. 45 | """ 46 | # Update the settings to point to the temp log directory 47 | settings.log_dir = temp_xml_log_directory 48 | 49 | out = StringIO() 50 | call_command("generate_pretty_xml", stdout=out) 51 | 52 | # Check command output for success message 53 | assert "Processing file" in out.getvalue() 54 | assert "File test.xml reformatted successfully." in out.getvalue() 55 | 56 | # Verify that the reformatted XML file exists in the pretty directory 57 | pretty_dir = os.path.join( 58 | settings.log_dir, "xml", "pretty" 59 | ) 60 | formatted_file = os.path.join(pretty_dir, "formatted_test.xml") 61 | assert os.path.exists(formatted_file), "Reformatted file was not created." 62 | 63 | # Check the content of the generated pretty XML file 64 | with open(formatted_file) as f: 65 | content = f.read() 66 | assert "" in content 67 | assert "Test Entry" in content 68 | assert "" in content 69 | 70 | @patch( 71 | "django_logging.management.commands.generate_pretty_xml.settings_manager" 72 | ) 73 | def test_command_directory_not_found(self, settings: Any) -> None: 74 | """ 75 | Test that the command handles the case when the XML directory is missing. 76 | 77 | This test checks that the command outputs an appropriate error message when the directory does not exist. 78 | 79 | Args: 80 | ---- 81 | settings (django.conf.Settings): Django settings. 82 | """ 83 | settings.log_dir = "/non_existent_dir" 84 | out = StringIO() 85 | call_command("generate_pretty_xml", stdout=out) 86 | 87 | # Verify error output 88 | assert "does not exist." in out.getvalue() 89 | -------------------------------------------------------------------------------- /django_logging/tests/commands/test_logs_size_audit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import StringIO 3 | from unittest.mock import MagicMock, patch 4 | 5 | import pytest 6 | from django.core.management import call_command 7 | 8 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 9 | 10 | pytestmark = [ 11 | pytest.mark.commands, 12 | pytest.mark.commands_logs_size_audit, 13 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 14 | ] 15 | 16 | 17 | class TestCheckLogSizeCommand: 18 | """ 19 | Test suite for the `check_log_size` management command. 20 | """ 21 | 22 | @patch("os.path.exists", return_value=True) 23 | @patch("os.walk") 24 | def test_command_log_directory_size_under_limit( 25 | self, mock_os_walk: MagicMock, temp_log_directory: str 26 | ) -> None: 27 | """ 28 | Test that the command correctly handles the case when the log directory size is under the limit. 29 | 30 | This test verifies that the command calculates the log directory size correctly and does not send 31 | an email when the size is below the limit. 32 | 33 | Args: 34 | mock_os_walk (MagicMock): Mock for `os.walk`. 35 | temp_log_directory (str): Temporary log directory fixture. 36 | """ 37 | # Mock the os.walk to return an empty directory 38 | mock_os_walk.return_value = [(temp_log_directory, [], [])] 39 | 40 | # Execute the command and capture the output 41 | out = StringIO() 42 | with patch("django.conf.settings.DJANGO_LOGGING", {"LOG_DIR_SIZE_LIMIT": 100}): 43 | call_command("logs_size_audit", stdout=out) 44 | 45 | assert "Log directory size is under the limit" in out.getvalue() 46 | 47 | @patch("os.path.exists", return_value=True) 48 | @patch("os.walk") 49 | @patch("django_logging.management.commands.logs_size_audit.send_email_async") 50 | def test_command_log_directory_size_exceeds_limit( 51 | self, 52 | mock_send_email: MagicMock, 53 | mock_os_walk: MagicMock, 54 | temp_log_directory: str, 55 | ) -> None: 56 | """ 57 | Test that the command sends a warning email when the log directory size exceeds the limit. 58 | 59 | This test verifies that the command calculates the log directory size correctly and sends 60 | an email notification when the size exceeds the limit. 61 | 62 | Args: 63 | ---- 64 | mock_send_email (MagicMock): Mock for the `send_warning_email` method. 65 | mock_os_walk (MagicMock): Mock for `os.walk`. 66 | temp_log_directory (str): Temporary log directory fixture. 67 | """ 68 | # Mock the os.walk to simulate a large directory 69 | mock_os_walk.return_value = [ 70 | (temp_log_directory, [], ["log1.txt", "log2.txt"]), 71 | ] 72 | # Mock the file sizes to exceed the limit 73 | with patch("os.path.getsize", side_effect=[60 * 1024 * 1024, 50 * 1024 * 1024]): 74 | out = StringIO() 75 | with patch("django.conf.settings.ADMIN_EMAIL", "admin@example.com"): 76 | with patch("django_logging.management.commands.logs_size_audit.settings_manager.log_dir_size_limit", 100): 77 | call_command("logs_size_audit", stdout=out) 78 | 79 | # Verify that the warning email was sent 80 | mock_send_email.assert_called_once() 81 | assert "Warning email sent successfully" in out.getvalue() 82 | 83 | @patch("os.path.exists", return_value=False) 84 | def test_command_log_directory_not_found(self, temp_log_directory: str) -> None: 85 | """ 86 | Test that the command handles the case where the log directory does not exist. 87 | 88 | This test verifies that the command logs an error message and exits gracefully 89 | when the log directory is missing. 90 | 91 | Args: 92 | ---- 93 | temp_log_directory (str): Temporary log directory fixture. 94 | """ 95 | out = StringIO() 96 | call_command("logs_size_audit", stdout=out) 97 | 98 | assert "Log directory not found" in out.getvalue() 99 | -------------------------------------------------------------------------------- /django_logging/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from django_logging.tests.setup import configure_django_settings 2 | from django_logging.tests.fixtures import ( 3 | admin_email_mock_settings, 4 | colored_formatter, 5 | debug_log_record, 6 | email_handler, 7 | email_mock_settings, 8 | error_log_record, 9 | error_with_exc_log_record, 10 | flat_formatter, 11 | get_response, 12 | json_formatter, 13 | log_config, 14 | log_manager, 15 | magic_mock_logger, 16 | mock_email_settings, 17 | mock_logger, 18 | mock_settings, 19 | mock_smtp, 20 | notifier_mock_logger, 21 | request_factory, 22 | request_middleware, 23 | reset_settings, 24 | temp_json_log_directory, 25 | temp_log_directory, 26 | temp_xml_log_directory, 27 | xml_formatter, 28 | setup_users, 29 | client 30 | ) 31 | -------------------------------------------------------------------------------- /django_logging/tests/constants.py: -------------------------------------------------------------------------------- 1 | PYTHON_VERSION = (3, 9) 2 | PYTHON_VERSION_REASON = "Requires Python 3.9 or higher" 3 | -------------------------------------------------------------------------------- /django_logging/tests/contextvar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/contextvar/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/contextvar/test_contextvar_manager.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from django_logging.contextvar.contextvar_manager import ContextVarManager 6 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 7 | 8 | pytestmark = [ 9 | pytest.mark.contextvar, 10 | pytest.mark.contextvar_manager, 11 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 12 | ] 13 | 14 | 15 | class TestContextVarManager: 16 | """Tests for the ContextVarManager class.""" 17 | 18 | def setup_method(self) -> None: 19 | """Set up a new ContextVarManager instance before each test.""" 20 | self.manager = ContextVarManager() 21 | 22 | def test_bind_and_get_contextvars(self) -> None: 23 | """ 24 | Test that variables can be bound and retrieved. 25 | """ 26 | self.manager.bind(user_id=42, request_id="abc123") 27 | 28 | context_vars = self.manager.get_contextvars() 29 | assert context_vars["user_id"] == 42 30 | assert context_vars["request_id"] == "abc123" 31 | self.manager.clear() 32 | 33 | def test_batch_bind_and_reset(self) -> None: 34 | """ 35 | Test batch binding context variables and resetting them using tokens. 36 | """ 37 | tokens = self.manager.batch_bind(user_id=42, request_id="abc123") 38 | 39 | context_vars = self.manager.get_contextvars() 40 | assert context_vars["user_id"] == 42 41 | assert context_vars["request_id"] == "abc123" 42 | 43 | self.manager.reset(tokens) 44 | context_vars = self.manager.get_contextvars() 45 | assert "user_id" not in context_vars 46 | assert "request_id" not in context_vars 47 | 48 | def test_unbind(self) -> None: 49 | """ 50 | Test unbinding a context variable. 51 | """ 52 | self.manager.bind(user_id=42) 53 | self.manager.unbind("user_id") 54 | 55 | context_vars = self.manager.get_contextvars() 56 | assert "user_id" not in context_vars 57 | 58 | def test_clear(self) -> None: 59 | """ 60 | Test clearing all context variables. 61 | """ 62 | self.manager.bind(user_id=42, request_id="abc123") 63 | self.manager.clear() 64 | 65 | context_vars = self.manager.get_contextvars() 66 | assert "user_id" not in context_vars 67 | assert "request_id" not in context_vars 68 | 69 | def test_merge_contexts(self) -> None: 70 | """ 71 | Test merging context variables with priority given to bound context. 72 | """ 73 | local_context = {"user_id": 42, "request_id": "abc123"} 74 | bound_context = {"user_id": 99, "role": "admin"} 75 | 76 | merged_context = self.manager.merge_contexts(bound_context, local_context) 77 | 78 | assert merged_context["user_id"] == 99 # bound context should override 79 | assert merged_context["request_id"] == "abc123" 80 | assert merged_context["role"] == "admin" 81 | self.manager.clear() 82 | 83 | def test_get_merged_context(self) -> None: 84 | """ 85 | Test getting the merged context from both logger-bound and local context variables. 86 | """ 87 | self.manager.bind(user_id=42, request_id="abc123") 88 | bound_logger_context = {"user_id": 99, "role": "admin"} 89 | 90 | merged_context = self.manager.get_merged_context(bound_logger_context) 91 | 92 | assert merged_context["user_id"] == 99 # bound context should override 93 | assert merged_context["request_id"] == "abc123" 94 | assert merged_context["role"] == "admin" 95 | self.manager.clear() 96 | 97 | def test_scoped_context(self) -> None: 98 | """ 99 | Test using the context manager to temporarily bind and reset context variables. 100 | """ 101 | with self.manager.scoped_context(user_id=42): 102 | context_vars = self.manager.get_contextvars() 103 | assert context_vars["user_id"] == 42 104 | 105 | context_vars = self.manager.get_contextvars() 106 | assert "user_id" not in context_vars 107 | -------------------------------------------------------------------------------- /django_logging/tests/decorators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/decorators/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/filters/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/filters/test_log_level_filter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | import pytest 5 | 6 | from django_logging.filters import LoggingLevelFilter 7 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 8 | 9 | pytestmark = [ 10 | pytest.mark.filters, 11 | pytest.mark.filters_level_filter, 12 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 13 | ] 14 | 15 | 16 | class TestLoggingLevelFilter: 17 | """ 18 | Test suite for the `LoggingLevelFilter` class. 19 | """ 20 | 21 | def test_logging_level_filter_initialization(self) -> None: 22 | """ 23 | Test that the `LoggingLevelFilter` initializes with the correct logging level. 24 | 25 | Asserts: 26 | ------- 27 | - The `logging_level` attribute of the filter instance is set to the provided logging level. 28 | """ 29 | filter_instance = LoggingLevelFilter(logging.INFO) 30 | assert filter_instance.logging_level == logging.INFO, ( 31 | f"Expected logging_level to be {logging.INFO}, " 32 | f"got {filter_instance.logging_level}" 33 | ) 34 | 35 | def test_logging_level_filter_passes_matching_level( 36 | self, debug_log_record: logging.LogRecord 37 | ) -> None: 38 | """ 39 | Test that the `LoggingLevelFilter` passes log records with the matching logging level. 40 | 41 | Args: 42 | ---- 43 | debug_log_record (logging.LogRecord): A dummy log record created by the fixture. 44 | 45 | Asserts: 46 | ------- 47 | - The filter method returns True when the log record's level matches the filter's level. 48 | """ 49 | debug_log_record.levelno = logging.DEBUG 50 | filter_instance = LoggingLevelFilter(logging.DEBUG) 51 | 52 | assert filter_instance.filter(debug_log_record), ( 53 | f"Expected filter to return True for log level {logging.DEBUG}, " 54 | f"got False" 55 | ) 56 | 57 | def test_logging_level_filter_blocks_non_matching_level( 58 | self, debug_log_record: logging.LogRecord 59 | ) -> None: 60 | """ 61 | Test that the `LoggingLevelFilter` blocks log records with a non-matching logging level. 62 | 63 | Args: 64 | ---- 65 | debug_log_record (logging.LogRecord): A dummy log record created by the fixture. 66 | 67 | Asserts: 68 | ------- 69 | - The filter method returns False when the log record's level does not match the filter's level. 70 | """ 71 | debug_log_record.levelno = logging.WARNING 72 | filter_instance = LoggingLevelFilter(logging.ERROR) 73 | 74 | assert not filter_instance.filter(debug_log_record), ( 75 | f"Expected filter to return False for log level {logging.WARNING}, " 76 | f"got True" 77 | ) 78 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .commands_fixture import ( 2 | temp_json_log_directory, 3 | temp_log_directory, 4 | temp_xml_log_directory, 5 | ) 6 | from .conf_fixture import log_config, log_manager 7 | from .email_handler_fixture import email_handler 8 | from .email_notifier_fixture import email_mock_settings, mock_smtp, notifier_mock_logger 9 | from .email_settings_fixture import mock_email_settings 10 | from .formatters import colored_formatter, flat_formatter, json_formatter, xml_formatter 11 | from .log_and_notify_fixture import admin_email_mock_settings, magic_mock_logger 12 | from .log_record_fixture import ( 13 | debug_log_record, 14 | error_log_record, 15 | error_with_exc_log_record, 16 | ) 17 | from .logger_fixture import mock_logger 18 | from .request_middleware_fixture import ( 19 | get_response, 20 | request_factory, 21 | request_middleware, 22 | ) 23 | from .settings_fixture import mock_settings, reset_settings 24 | from .views_fixture import setup_users, client 25 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/commands_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from _pytest._py.path import LocalPath 5 | from _pytest.tmpdir import TempPathFactory 6 | 7 | 8 | @pytest.fixture 9 | def temp_json_log_directory(tmpdir: Any) -> str: 10 | """ 11 | Fixture to create a temporary log directory with sample JSON files for testing. 12 | 13 | Args: 14 | tmpdir (TempPathFactory): Temporary directory fixture provided by pytest. 15 | 16 | Returns: 17 | str: The path to the temporary log directory. 18 | """ 19 | json_dir = tmpdir.mkdir("json") 20 | 21 | # Create a valid JSON file with multiple JSON objects. 22 | json_file = json_dir.join("test.json") 23 | json_file.write('{"key": "value"}\n{"key": "value2"}') 24 | 25 | return str(tmpdir) 26 | 27 | 28 | @pytest.fixture 29 | def temp_log_directory(tmpdir: LocalPath) -> str: 30 | """ 31 | Fixture to create a temporary log directory for testing. 32 | 33 | Args: 34 | tmpdir (LocalPath): Temporary directory fixture provided by pytest. 35 | 36 | Returns: 37 | str: Path to the temporary log directory. 38 | """ 39 | log_dir = tmpdir.mkdir("logs") 40 | return str(log_dir) 41 | 42 | 43 | @pytest.fixture 44 | def temp_xml_log_directory(tmpdir: Any) -> str: 45 | """ 46 | Fixture to create a temporary log directory with sample XML files for testing. 47 | 48 | Args: 49 | ---- 50 | tmpdir (TempPathFactory): Temporary directory fixture provided by pytest. 51 | 52 | Returns: 53 | ------- 54 | str: The path to the temporary log directory. 55 | """ 56 | # Create the directory structure for logs/xml and logs/pretty 57 | xml_dir = tmpdir.mkdir("xml") 58 | 59 | # Create a valid XML file for testing 60 | xml_file = xml_dir.join("test.xml") 61 | xml_file.write("Test Entry") 62 | 63 | return str(tmpdir) 64 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/conf_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_logging.settings.conf import LogConfig, LogManager 4 | 5 | 6 | @pytest.fixture 7 | def log_config() -> LogConfig: 8 | """ 9 | Fixture to provide a default LogConfig instance. 10 | 11 | This fixture sets up a LogConfig object with sample values for various logging 12 | configuration options, including log levels, log directory, formats, and email notifier settings. 13 | 14 | Returns: 15 | ------- 16 | LogConfig 17 | An instance of the LogConfig class initialized with sample values. 18 | """ 19 | return LogConfig( 20 | log_levels=["INFO", "WARNING", "ERROR"], 21 | log_dir="/tmp/logs", 22 | log_file_formats={"INFO": 1, "WARNING": None, "ERROR": "%(message)s"}, # type: ignore 23 | log_file_format_types={"INFO": "NORMAL"}, 24 | extra_log_files={"INFO": False}, 25 | console_level="INFO", 26 | console_format=1, 27 | colorize_console=False, 28 | log_date_format="%Y-%m-%d %H:%M:%S", 29 | log_email_notifier_enable=True, 30 | log_email_notifier_log_levels=["ERROR"], 31 | log_email_notifier_log_format=1, 32 | ) 33 | 34 | 35 | @pytest.fixture 36 | def log_manager(log_config: LogConfig) -> LogManager: 37 | """ 38 | Fixture to provide a LogManager instance initialized with a LogConfig. 39 | 40 | This fixture sets up a LogManager object using the provided LogConfig instance 41 | for managing logging configurations and operations. 42 | 43 | Returns: 44 | ------- 45 | LogManager 46 | An instance of the LogManager class initialized with the provided LogConfig. 47 | """ 48 | return LogManager(log_config) 49 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/email_handler_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_logging.handlers import EmailHandler 4 | 5 | 6 | @pytest.fixture 7 | def email_handler() -> EmailHandler: 8 | """ 9 | Fixture to create an EmailHandler instance. 10 | 11 | Returns: 12 | ------- 13 | EmailHandler 14 | An instance of the EmailHandler class. 15 | """ 16 | return EmailHandler() 17 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/email_notifier_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Tuple 2 | from unittest.mock import MagicMock, patch 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def mock_smtp() -> Generator[MagicMock, None, None]: 9 | """ 10 | Fixture to mock the SMTP object used for sending emails. 11 | 12 | This fixture patches the `SMTP` class from `smtplib` to prevent actual email sending and 13 | allows testing the interactions with the mock SMTP object. 14 | 15 | Yields: 16 | ------- 17 | unittest.mock.MagicMock 18 | A mock object representing the SMTP class. 19 | """ 20 | with patch("django_logging.utils.log_email_notifier.notifier.SMTP") as mock_smtp: 21 | yield mock_smtp 22 | 23 | 24 | @pytest.fixture 25 | def email_mock_settings() -> Generator[MagicMock, None, None]: 26 | """ 27 | Fixture to mock the Django settings used for email configuration. 28 | 29 | This fixture patches the `settings` object to provide fake email configuration values 30 | without needing to access the actual Django settings. 31 | 32 | Yields: 33 | ------- 34 | unittest.mock.MagicMock 35 | A mock object representing the Django settings with predefined email configurations. 36 | """ 37 | with patch( 38 | "django_logging.utils.log_email_notifier.notifier.settings" 39 | ) as mock_settings: 40 | mock_settings.DEFAULT_FROM_EMAIL = "from@example.com" 41 | mock_settings.EMAIL_HOST = "smtp.example.com" 42 | mock_settings.EMAIL_PORT = 587 43 | mock_settings.EMAIL_HOST_USER = "user@example.com" 44 | mock_settings.EMAIL_HOST_PASSWORD = "password" 45 | yield mock_settings 46 | 47 | 48 | @pytest.fixture 49 | def notifier_mock_logger() -> Generator[Tuple[MagicMock, MagicMock], None, None]: 50 | """ 51 | Fixture to mock the logger used for logging messages. 52 | 53 | This fixture patches the `logger.info` and `logger.warning` methods to intercept and test 54 | logging calls without affecting the actual logging configuration. 55 | 56 | Yields: 57 | ------- 58 | tuple 59 | A tuple containing mock objects for `logger.info` and `logger.warning`. 60 | """ 61 | with patch( 62 | "django_logging.utils.log_email_notifier.notifier.logger.info" 63 | ) as mock_info, patch( 64 | "django_logging.utils.log_email_notifier.notifier.logger.warning" 65 | ) as mock_warning: 66 | yield mock_info, mock_warning 67 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/email_settings_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def mock_email_settings() -> Dict: 8 | """ 9 | Fixture to mock Django email settings. 10 | 11 | Returns: 12 | -------- 13 | dict: A dictionary containing mock email settings: 14 | - EMAIL_HOST: "smtp.example.com" 15 | - EMAIL_PORT: 587 16 | - EMAIL_HOST_USER: "user@example.com" 17 | - EMAIL_HOST_PASSWORD: "password" 18 | - DEFAULT_FROM_EMAIL: "from@example.com" 19 | - ADMIN_EMAIL: "to@example.com" 20 | """ 21 | return { 22 | "EMAIL_HOST": "smtp.example.com", 23 | "EMAIL_PORT": 587, 24 | "EMAIL_HOST_USER": "user@example.com", 25 | "EMAIL_HOST_PASSWORD": "password", 26 | "DEFAULT_FROM_EMAIL": "from@example.com", 27 | "ADMIN_EMAIL": "to@example.com", 28 | } 29 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/formatters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_logging.formatters import ( 4 | ColoredFormatter, 5 | FLATFormatter, 6 | JSONFormatter, 7 | XMLFormatter, 8 | ) 9 | 10 | 11 | @pytest.fixture 12 | def colored_formatter() -> ColoredFormatter: 13 | """ 14 | Fixture to create a `ColoredFormatter` instance with a specific format. 15 | 16 | Returns: 17 | ------- 18 | ColoredFormatter 19 | An instance of `ColoredFormatter` with a predefined format. 20 | """ 21 | return ColoredFormatter(fmt="%(levelname)s: %(message)s") 22 | 23 | 24 | @pytest.fixture 25 | def json_formatter() -> JSONFormatter: 26 | """ 27 | Fixture to provide an instance of `JSONFormatter`. 28 | 29 | Returns: 30 | ------- 31 | JSONFormatter: An instance of the `JSONFormatter` with a predefined format. 32 | """ 33 | return JSONFormatter(fmt="%(levelname)s: %(message)s") 34 | 35 | 36 | @pytest.fixture 37 | def xml_formatter() -> XMLFormatter: 38 | """ 39 | Fixture to provide an instance of `XMLFormatter`. 40 | 41 | Returns: 42 | ------- 43 | XMLFormatter: An instance of the `XMLFormatter` with predefined format. 44 | """ 45 | return XMLFormatter(fmt="%(levelname)s: %(message)s") 46 | 47 | 48 | @pytest.fixture 49 | def flat_formatter() -> FLATFormatter: 50 | """ 51 | Fixture to provide an instance of `FLATFormatter`. 52 | 53 | Returns: 54 | ------- 55 | FLATFormatter: An instance of the `FLATLineFormatter` with predefined format. 56 | """ 57 | return FLATFormatter(fmt="%(levelname)s: %(message)s") 58 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/log_and_notify_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from unittest.mock import MagicMock 3 | 4 | import pytest 5 | from django.conf import settings 6 | 7 | 8 | @pytest.fixture 9 | def admin_email_mock_settings() -> Generator[None, None, None]: 10 | """ 11 | Fixture to mock Django settings related to email notifications. 12 | 13 | This fixture sets up a mock ADMIN_EMAIL setting for testing and cleans up 14 | by deleting the setting after the test. 15 | 16 | Yields: 17 | ------- 18 | None 19 | """ 20 | settings.ADMIN_EMAIL = "admin@example.com" 21 | yield 22 | del settings.ADMIN_EMAIL 23 | 24 | 25 | @pytest.fixture 26 | def magic_mock_logger() -> MagicMock: 27 | """ 28 | Fixture to create a mock logger object for testing. 29 | 30 | Returns: 31 | -------- 32 | MagicMock 33 | A mock logger object used in the tests. 34 | """ 35 | return MagicMock() 36 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/log_record_fixture.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from logging import DEBUG, ERROR, LogRecord 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def debug_log_record() -> LogRecord: 9 | """ 10 | Fixture to create a dummy log record for testing. 11 | 12 | Returns: 13 | ------- 14 | logging.LogRecord: A dummy log record with predefined attributes. 15 | """ 16 | return LogRecord( 17 | name="test", 18 | level=DEBUG, 19 | pathname=__file__, 20 | lineno=10, 21 | msg="Test message", 22 | args=(), 23 | exc_info=None, 24 | ) 25 | 26 | 27 | @pytest.fixture 28 | def error_log_record() -> LogRecord: 29 | """ 30 | Fixture to create a dummy log record for testing. 31 | 32 | Returns: 33 | ------- 34 | logging.LogRecord: A dummy log record with predefined attributes. 35 | """ 36 | return LogRecord( 37 | name="test", 38 | level=ERROR, 39 | pathname=__file__, 40 | lineno=10, 41 | msg="Test message", 42 | args=(), 43 | exc_info=None, 44 | ) 45 | 46 | @pytest.fixture 47 | def error_with_exc_log_record() -> LogRecord: 48 | """ 49 | Fixture to create a dummy log record for testing. 50 | 51 | Returns: 52 | ------- 53 | logging.LogRecord: A dummy log record with predefined attributes. 54 | """ 55 | return LogRecord( 56 | name="test", 57 | level=ERROR, 58 | pathname=__file__, 59 | lineno=10, 60 | msg="Test message", 61 | args=(), 62 | exc_info=sys.exc_info(), 63 | ) 64 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/logger_fixture.py: -------------------------------------------------------------------------------- 1 | from logging import Logger, getLogger 2 | from typing import Generator 3 | from unittest.mock import Mock, patch 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def mock_logger() -> Generator[Logger, None, None]: 10 | """ 11 | Fixture to create a mock logger for testing. 12 | 13 | This fixture creates a mock logger object, which is used to test logging-related 14 | functionality without affecting the actual logging configuration. 15 | 16 | Yields: 17 | ------- 18 | logging.Logger 19 | A mock logger instance with its manager mocked. 20 | """ 21 | logger = getLogger() 22 | with patch.object(logger, "manager", new_callable=Mock): 23 | yield logger 24 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/request_middleware_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import pytest 4 | from django.http import HttpResponse 5 | from django.test import RequestFactory 6 | 7 | from django_logging.middleware import RequestLogMiddleware 8 | 9 | 10 | @pytest.fixture 11 | def request_factory() -> RequestFactory: 12 | """ 13 | Fixture to create a RequestFactory instance for generating request objects. 14 | 15 | Returns: 16 | ------- 17 | RequestFactory 18 | An instance of RequestFactory for creating mock requests. 19 | """ 20 | return RequestFactory() 21 | 22 | 23 | @pytest.fixture 24 | def get_response() -> Callable: 25 | """ 26 | Fixture to create a mock get_response function. 27 | 28 | Returns: 29 | ------- 30 | function 31 | A function that returns an HttpResponse with a dummy response. 32 | """ 33 | 34 | def _get_response(request: RequestFactory) -> HttpResponse: 35 | return HttpResponse("Test Response") 36 | 37 | return _get_response 38 | 39 | 40 | @pytest.fixture 41 | def request_middleware() -> RequestLogMiddleware: 42 | """ 43 | Fixture to create an instance of RequestLogMiddleware. 44 | 45 | Returns: 46 | ------- 47 | RequestLogMiddleware 48 | An instance of RequestLogMiddleware with the sample HttpResponse. 49 | """ 50 | middleware = RequestLogMiddleware(lambda request: HttpResponse("OK")) 51 | middleware.log_sql = True 52 | 53 | return middleware 54 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/settings_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from unittest.mock import patch 3 | import pytest 4 | from django.conf import settings 5 | 6 | from django_logging.settings.manager import SettingsManager 7 | 8 | 9 | @pytest.fixture 10 | def mock_settings() -> SettingsManager: 11 | """ 12 | Fixture to mock Django settings. 13 | 14 | This fixture sets up mock settings for `DJANGO_LOGGING` to provide controlled values 15 | for testing the configuration functions. The settings are patched into the Django settings 16 | object during the test. 17 | 18 | Yields: 19 | ------- 20 | dict 21 | A dictionary containing the mock settings used in the tests. 22 | """ 23 | mock_settings = { 24 | "DJANGO_LOGGING": { 25 | "LOG_FILE_LEVELS": ["DEBUG", "INFO"], 26 | "LOG_DIR": "/custom/log/dir", 27 | "LOG_FILE_FORMATS": { 28 | "DEBUG": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 29 | }, 30 | "LOG_FILE_FORMAT_TYPES": { 31 | "DEBUG": "JSON", 32 | "INFO": "XML", 33 | }, 34 | "EXTRA_LOG_FILES": { 35 | "DEBUG": False, 36 | "INFO": True, 37 | }, 38 | "LOG_CONSOLE_LEVEL": "WARNING", 39 | "LOG_CONSOLE_FORMAT": "%(levelname)s - %(message)s", 40 | "LOG_CONSOLE_COLORIZE": True, 41 | "LOG_DATE_FORMAT": "%Y-%m-%d", 42 | "LOG_EMAIL_NOTIFIER": { 43 | "ENABLE": True, 44 | "NOTIFY_ERROR": True, 45 | "NOTIFY_CRITICAL": False, 46 | "LOG_FORMAT": "custom_format", 47 | }, 48 | } 49 | } 50 | 51 | with patch.object(settings, "DJANGO_LOGGING", mock_settings["DJANGO_LOGGING"]): 52 | # Initialize SettingsManager after patching the settings 53 | return SettingsManager() 54 | 55 | 56 | @pytest.fixture 57 | def reset_settings() -> Generator[None, None, None]: 58 | """ 59 | Fixture to reset Django settings after each test. 60 | 61 | This ensures that any modifications to the settings during a test are reverted after the test completes. 62 | 63 | Yields: 64 | ------- 65 | None 66 | """ 67 | original_settings = settings.DJANGO_LOGGING 68 | yield 69 | settings.DJANGO_LOGGING = original_settings 70 | -------------------------------------------------------------------------------- /django_logging/tests/fixtures/views_fixture.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import pytest 4 | from django.contrib.auth.models import User 5 | from django.test import Client 6 | 7 | 8 | @pytest.fixture 9 | def setup_users() -> Dict[str, User]: 10 | """ 11 | Fixture to create a superuser and a normal user for testing purposes. 12 | Returns a dictionary with `superuser` and `non_superuser` keys. 13 | """ 14 | superuser = User.objects.create_superuser( 15 | username="admin", password="adminpassword", email="admin@example.com" 16 | ) 17 | non_superuser = User.objects.create_user( 18 | username="user", password="userpassword", email="user@example.com" 19 | ) 20 | return {"superuser": superuser, "non_superuser": non_superuser} 21 | 22 | 23 | @pytest.fixture 24 | def client() -> Client: 25 | """ 26 | Fixture to provide a test client. 27 | """ 28 | return Client() 29 | -------------------------------------------------------------------------------- /django_logging/tests/formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/formatters/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/formatters/test_base_formatter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from logging import LogRecord 3 | 4 | import pytest 5 | 6 | from django_logging.formatters.base import BaseStructuredFormatter 7 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 8 | 9 | pytestmark = [ 10 | pytest.mark.formatters, 11 | pytest.mark.base_formatter, 12 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 13 | ] 14 | 15 | 16 | class TestBaseStructuredFormatter: 17 | 18 | def test_extract_specifiers(self) -> None: 19 | """ 20 | Test that the `_extract_specifiers` method correctly extracts format specifiers from the provided format string. 21 | 22 | Asserts: 23 | ------- 24 | - The extracted specifiers match the expected list. 25 | """ 26 | fmt_string = "%(levelname)s | %(asctime)s | %(message)s | %(custom_field)s" 27 | formatter = BaseStructuredFormatter(fmt=fmt_string) 28 | 29 | expected_specifiers = ["levelname", "asctime", "message", "custom_field"] 30 | assert formatter.specifiers == expected_specifiers, ( 31 | f"Expected specifiers {expected_specifiers}, " 32 | f"but got {formatter.specifiers}" 33 | ) 34 | 35 | def test_extract_specifiers_empty_format(self) -> None: 36 | """ 37 | Test that `_extract_specifiers` returns an empty list when no format string is provided. 38 | 39 | Asserts: 40 | ------- 41 | - The specifiers list is empty. 42 | """ 43 | formatter = BaseStructuredFormatter(fmt=None) 44 | assert ( 45 | formatter.specifiers == [] 46 | ), "Specifiers list should be empty when no format string is provided." 47 | 48 | def test_get_field_value(self, debug_log_record: LogRecord) -> None: 49 | """ 50 | Test that `_get_field_value` correctly retrieves field values from the log record. 51 | 52 | Args: 53 | ---- 54 | debug_log_record (logging.LogRecord): The log record instance with known fields. 55 | 56 | Asserts: 57 | ------- 58 | - The `levelname` field value matches 'INFO'. 59 | - The `message` field value matches 'Test log message'. 60 | - The `custom_field` value matches 'CustomValue'. 61 | """ 62 | fmt_string = "%(levelname)s | %(asctime)s | %(message)s | %(custom_field)s" 63 | formatter = BaseStructuredFormatter(fmt=fmt_string) 64 | 65 | # Test known fields from log record 66 | assert formatter._get_field_value(debug_log_record, "levelname") == "DEBUG" 67 | assert formatter._get_field_value(debug_log_record, "message") == "Test message" 68 | 69 | # Test custom field 70 | debug_log_record.custom_field = "CustomValue" 71 | assert formatter._get_field_value(debug_log_record, "custom_field") == "CustomValue" 72 | 73 | def test_get_field_value_unknown_field(self, error_with_exc_log_record: LogRecord) -> None: 74 | """ 75 | Test that `_get_field_value` returns None when an unknown field is requested. 76 | 77 | Args: 78 | ---- 79 | error_with_exc_log_record (logging.LogRecord): The log record instance with no such field. 80 | 81 | Asserts: 82 | ------- 83 | - The method returns None for an unknown field. 84 | """ 85 | fmt_string = "%(unknown_field)s" 86 | formatter = BaseStructuredFormatter(fmt=fmt_string) 87 | formatter._add_exception(error_with_exc_log_record, {}) 88 | 89 | assert ( 90 | formatter._get_field_value(error_with_exc_log_record, "unknown_field") is None 91 | ), "Should return None for unknown field." 92 | -------------------------------------------------------------------------------- /django_logging/tests/formatters/test_colored_formatter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from unittest.mock import MagicMock, patch 4 | 5 | import pytest 6 | 7 | from django_logging.formatters import ColoredFormatter 8 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 9 | 10 | pytestmark = [ 11 | pytest.mark.formatters, 12 | pytest.mark.colored_formatter, 13 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 14 | ] 15 | 16 | 17 | class TestColoredFormatter: 18 | 19 | @patch( 20 | "django_logging.formatters.colored_formatter.colorize_log_format", autospec=True 21 | ) 22 | @patch( 23 | "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", 24 | side_effect=lambda fmt: fmt, 25 | ) 26 | def test_format_applies_colorization( 27 | self, 28 | mock_remove_ansi: MagicMock, 29 | mock_colorize: MagicMock, 30 | colored_formatter: ColoredFormatter, 31 | debug_log_record: logging.LogRecord, 32 | ) -> None: 33 | """ 34 | Test that the `format` method of `ColoredFormatter` applies colorization. 35 | 36 | This test verifies that the `format` method calls the `colorize_log_format` 37 | function to apply colorization based on the log level. 38 | 39 | Parameters: 40 | ---------- 41 | mock_remove_ansi : MagicMock 42 | Mock for `remove_ansi_escape_sequences`. 43 | mock_colorize : MagicMock 44 | Mock for `colorize_log_format`. 45 | colored_formatter : ColoredFormatter 46 | The formatter instance being tested. 47 | debug_log_record : logging.LogRecord 48 | The dummy log record created by the fixture. 49 | 50 | Asserts: 51 | ------- 52 | - The `colorize_log_format` function is called once with the correct arguments. 53 | """ 54 | # Mock the colorize_log_format to return a predictable format 55 | mock_colorize.return_value = "%(levelname)s: %(message)s" 56 | 57 | colored_formatter.format(debug_log_record) 58 | 59 | # Ensuring colorization should have been triggered 60 | mock_colorize.assert_called_once_with( 61 | "%(levelname)s: %(message)s", debug_log_record.levelname 62 | ) 63 | 64 | @patch( 65 | "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", 66 | side_effect=lambda fmt: fmt, 67 | ) 68 | def test_format_resets_to_original_format( 69 | self, 70 | mock_remove_ansi: MagicMock, 71 | colored_formatter: ColoredFormatter, 72 | debug_log_record: logging.LogRecord, 73 | ) -> None: 74 | """ 75 | Test that the `format` method resets the format string to its original state after formatting. 76 | 77 | This test ensures that the formatter's internal format string is not permanently modified 78 | by the colorization process and is reset to its original value after each log record is formatted. 79 | 80 | Parameters: 81 | ---------- 82 | mock_remove_ansi : MagicMock 83 | Mock for `remove_ansi_escape_sequences`. 84 | colored_formatter : ColoredFormatter 85 | The formatter instance being tested. 86 | debug_log_record : logging.LogRecord 87 | The dummy log record created by the fixture. 88 | 89 | Asserts: 90 | ------- 91 | - The formatter's internal format string (`_style._fmt`) matches the original format after formatting. 92 | """ 93 | original_format = colored_formatter._style._fmt 94 | colored_formatter.format(debug_log_record) 95 | assert ( 96 | colored_formatter._style._fmt == original_format 97 | ), f"Expected format string to reset to original format, but got {colored_formatter._style._fmt}" 98 | 99 | @patch( 100 | "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", 101 | side_effect=lambda fmt: fmt, 102 | ) 103 | def test_format_returns_formatted_output( 104 | self, colored_formatter: ColoredFormatter, debug_log_record: logging.LogRecord 105 | ) -> None: 106 | """ 107 | Test that the `format` method returns the correctly formatted log output. 108 | 109 | This test verifies that the formatted output matches the expected structure, 110 | including the log level name and the log message. 111 | 112 | Parameters: 113 | ---------- 114 | colored_formatter : ColoredFormatter 115 | The formatter instance being tested. 116 | debug_log_record : logging.LogRecord 117 | The dummy log record created by the fixture. 118 | 119 | Asserts: 120 | ------- 121 | - The formatted output starts with the expected log level and message. 122 | """ 123 | expected_output = ( 124 | f"{logging.getLevelName(debug_log_record.levelno)}: {debug_log_record.msg}" 125 | ) 126 | formatted_output = colored_formatter.format(debug_log_record) 127 | 128 | # Directly comparing the output assuming it might include color codes 129 | assert formatted_output.startswith( 130 | f"DEBUG: Test message" 131 | ), f"Expected formatted output to start with '{expected_output}', but got '{formatted_output}'" 132 | -------------------------------------------------------------------------------- /django_logging/tests/formatters/test_flat_line_formatter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | import pytest 5 | 6 | from django_logging.formatters import FLATFormatter 7 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 8 | 9 | pytestmark = [ 10 | pytest.mark.formatters, 11 | pytest.mark.flat_formatter, 12 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 13 | ] 14 | 15 | 16 | class TestFLATFormatter: 17 | 18 | def test_format_flat_record( 19 | self, flat_formatter: FLATFormatter, debug_log_record: logging.LogRecord 20 | ) -> None: 21 | """ 22 | Test that the `format` method formats a log record into a single flat line. 23 | 24 | Args: 25 | ---- 26 | flat_formatter (FLATFormatter): The formatter instance being tested. 27 | debug_log_record (logging.LogRecord): The dummy log record to format. 28 | 29 | Asserts: 30 | ------- 31 | - The flat line contains key-value pairs for each specifier. 32 | - There is no 'None' or empty fields in the formatted log. 33 | """ 34 | debug_log_record.custom_field = "custom_value" 35 | flat_formatter.specifiers = ["asctime", "levelname", "message", "custom_field"] 36 | 37 | formatted_output = flat_formatter.format(debug_log_record) 38 | 39 | # Check for presence of the fields as key-value pairs in the output 40 | assert "asctime" in formatted_output, "asctime field not present." 41 | assert ( 42 | "levelname='DEBUG'" in formatted_output 43 | ), "levelname field incorrectly formatted." 44 | assert ( 45 | "custom_field='custom_value'" in formatted_output 46 | ), "custom_field incorrectly formatted." 47 | assert "message" in formatted_output, "message field not present." 48 | 49 | def test_format_with_exception( 50 | self, flat_formatter: FLATFormatter, error_with_exc_log_record: logging.LogRecord 51 | ) -> None: 52 | """ 53 | Test that the `format` method correctly adds exception debugrmation to the flat line string. 54 | 55 | Args: 56 | ---- 57 | flat_formatter (FLATFormatter): The formatter instance being tested. 58 | error_with_exc_log_record (logging.LogRecord): The dummy log record with an exception. 59 | 60 | Asserts: 61 | ------- 62 | - The exception debug is included in the flat line. 63 | """ 64 | flat_formatter.specifiers = ["asctime", "levelname", "message"] 65 | 66 | formatted_output = flat_formatter.format(error_with_exc_log_record) 67 | 68 | # Check that exception debug is included in the output 69 | assert "exception" in formatted_output, "Exception info not present in the formatted log." 70 | -------------------------------------------------------------------------------- /django_logging/tests/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/handlers/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/middleware/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/middleware/test_base_middleware.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Callable 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | from asgiref.sync import iscoroutinefunction 7 | from django.http import HttpRequest, HttpResponseBase 8 | 9 | from django_logging.middleware.base import BaseMiddleware 10 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 11 | 12 | pytestmark = [ 13 | pytest.mark.middleware, 14 | pytest.mark.base_middleware, 15 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 16 | ] 17 | 18 | 19 | class TestBaseMiddleware: 20 | """ 21 | Test suite for the BaseMiddleware class. 22 | """ 23 | 24 | def test_sync_mode(self) -> None: 25 | """ 26 | Test that the middleware correctly identifies and handles synchronous requests. 27 | This test verifies that when the `get_response` function is synchronous, 28 | the middleware calls the `__sync_call__` method. 29 | """ 30 | # Mock synchronous get_response 31 | mock_get_response = Mock(spec=Callable[[HttpRequest], HttpResponseBase]) 32 | 33 | # Create an instance of the middleware 34 | middleware = BaseMiddleware(mock_get_response) 35 | 36 | # Ensure that it is in synchronous mode 37 | assert not iscoroutinefunction(middleware.get_response) 38 | assert not middleware.async_mode 39 | 40 | # Test that calling the middleware raises NotImplementedError (since __sync_call__ is not implemented) 41 | with pytest.raises( 42 | NotImplementedError, match="__sync_call__ must be implemented by subclass" 43 | ): 44 | request = HttpRequest() 45 | middleware(request) 46 | 47 | @pytest.mark.asyncio 48 | async def test_async_mode(self) -> None: 49 | """ 50 | Test that the middleware correctly identifies and handles asynchronous requests. 51 | This test verifies that when the `get_response` function is asynchronous, 52 | the middleware calls the `__acall__` method. 53 | """ 54 | 55 | # Mock asynchronous get_response 56 | async def mock_get_response(request: HttpRequest) -> HttpResponseBase: 57 | return Mock(spec=HttpResponseBase) 58 | 59 | # Create an instance of the middleware 60 | middleware = BaseMiddleware(mock_get_response) 61 | 62 | # Ensure that it is in asynchronous mode 63 | assert iscoroutinefunction(middleware.get_response) 64 | assert middleware.async_mode 65 | 66 | # Test that calling the middleware raises NotImplementedError (since __acall__ is not implemented) 67 | with pytest.raises( 68 | NotImplementedError, match="__acall__ must be implemented by subclass" 69 | ): 70 | request = HttpRequest() 71 | await middleware(request) 72 | -------------------------------------------------------------------------------- /django_logging/tests/middleware/test_monitor_log_size.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import timedelta 3 | from unittest.mock import Mock, patch 4 | 5 | import pytest 6 | from django.core.cache import cache 7 | from django.http import HttpRequest, HttpResponse 8 | from django.utils.timezone import now 9 | 10 | from django_logging.middleware.monitor_log_size import MonitorLogSizeMiddleware 11 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 12 | 13 | pytestmark = [ 14 | pytest.mark.middleware, 15 | pytest.mark.monitor_log_size_middleware, 16 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 17 | ] 18 | 19 | 20 | class TestMonitorLogSizeMiddleware: 21 | """ 22 | Test suite for the MonitorLogSizeMiddleware class. 23 | """ 24 | 25 | @pytest.fixture(autouse=True) 26 | def setup(self) -> None: 27 | """ 28 | Clears cache before each test. 29 | """ 30 | cache.clear() 31 | 32 | def test_should_run_task_no_cache(self) -> None: 33 | """ 34 | Test that the task should run when there is no cache entry for 'last_run_logs_size_audit'. 35 | """ 36 | assert MonitorLogSizeMiddleware.should_run_task() is True 37 | 38 | def test_should_run_task_with_recent_cache(self) -> None: 39 | """ 40 | Test that the task should not run if the cache indicates the last run was within a week. 41 | """ 42 | last_run_time = now() - timedelta(days=2) 43 | cache.set("last_run_logs_size_audit", last_run_time) 44 | 45 | assert MonitorLogSizeMiddleware.should_run_task() is False 46 | 47 | def test_should_run_task_with_old_cache(self) -> None: 48 | """ 49 | Test that the task should run if the cache indicates the last run was more than a week ago. 50 | """ 51 | last_run_time = now() - timedelta(weeks=2) 52 | cache.set("last_run_logs_size_audit", last_run_time) 53 | 54 | assert MonitorLogSizeMiddleware.should_run_task() is True 55 | 56 | @patch("django_logging.middleware.monitor_log_size.call_command") 57 | def test_sync_run_log_size_check(self, mock_call_command: Mock) -> None: 58 | """ 59 | Test the synchronous execution of the log size check. 60 | """ 61 | mock_get_response = Mock(return_value=HttpResponse()) 62 | middleware = MonitorLogSizeMiddleware(mock_get_response) 63 | 64 | request = HttpRequest() 65 | 66 | # Simulate no recent audit, so the task should run 67 | cache.set("last_run_logs_size_audit", now() - timedelta(weeks=2)) 68 | 69 | response = middleware.__sync_call__(request) 70 | 71 | mock_call_command.assert_called_once_with("logs_size_audit") 72 | assert cache.get("last_run_logs_size_audit") is not None 73 | assert response.status_code == 200 74 | 75 | @pytest.mark.asyncio 76 | @patch("django_logging.middleware.monitor_log_size.call_command") 77 | async def test_async_run_log_size_check(self, mock_call_command: Mock) -> None: 78 | """ 79 | Test the asynchronous execution of the log size check. 80 | """ 81 | 82 | async def mock_get_response(request: HttpRequest) -> HttpResponse: 83 | return HttpResponse() 84 | 85 | middleware = MonitorLogSizeMiddleware(mock_get_response) 86 | 87 | request = HttpRequest() 88 | 89 | # Simulate no recent audit, so the task should run 90 | cache.set("last_run_logs_size_audit", now() - timedelta(weeks=2)) 91 | 92 | response = await middleware.__acall__(request) 93 | 94 | mock_call_command.assert_called_once_with("logs_size_audit") 95 | assert cache.get("last_run_logs_size_audit") is not None 96 | assert response.status_code == 200 97 | 98 | @patch( 99 | "django_logging.middleware.monitor_log_size.call_command", side_effect=Exception("Command failed") 100 | ) 101 | def test_sync_run_log_size_check_failure(self, mock_call_command: Mock) -> None: 102 | """ 103 | Test error handling in the synchronous log size check. 104 | """ 105 | mock_get_response = Mock(return_value=HttpResponse()) 106 | middleware = MonitorLogSizeMiddleware(mock_get_response) 107 | 108 | request = HttpRequest() 109 | 110 | with patch( 111 | "django_logging.middleware.monitor_log_size.logger.error" 112 | ) as mock_logger: 113 | middleware.__sync_call__(request) 114 | 115 | mock_call_command.assert_called_once_with("logs_size_audit") 116 | mock_logger.assert_called_once() 117 | 118 | @pytest.mark.asyncio 119 | @patch( 120 | "django_logging.middleware.monitor_log_size.call_command", side_effect=Exception("Command failed") 121 | ) 122 | async def test_async_run_log_size_check_failure(self, mock_call_command: Mock) -> None: 123 | """ 124 | Test error handling in the asynchronous log size check. 125 | """ 126 | 127 | async def mock_get_response(request): 128 | return HttpResponse() 129 | 130 | middleware = MonitorLogSizeMiddleware(mock_get_response) 131 | 132 | request = HttpRequest() 133 | 134 | with patch( 135 | "django_logging.middleware.monitor_log_size.logger.error" 136 | ) as mock_logger: 137 | await middleware.__acall__(request) 138 | 139 | mock_call_command.assert_called_once_with("logs_size_audit") 140 | mock_logger.assert_called_once() 141 | 142 | -------------------------------------------------------------------------------- /django_logging/tests/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/settings/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/setup.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | import string 4 | import random 5 | 6 | 7 | def generate_secret_key(length: int = 50) -> str: 8 | """ 9 | Generates a random secret key for Django settings. 10 | 11 | Args: 12 | length (int): The length of the secret key. Default is 50 characters. 13 | 14 | Returns: 15 | str: A randomly generated secret key. 16 | """ 17 | characters = string.ascii_letters + string.digits + string.punctuation 18 | return "".join(random.choice(characters) for _ in range(length)) 19 | 20 | 21 | def configure_django_settings() -> None: 22 | if not settings.configured: 23 | settings.configure( 24 | DEBUG=True, 25 | SECRET_KEY=generate_secret_key(), # Add a secret key for testing 26 | DATABASES={ 27 | "default": { 28 | "ENGINE": "django.db.backends.sqlite3", 29 | "NAME": ":memory:", 30 | } 31 | }, 32 | INSTALLED_APPS=[ 33 | "django.contrib.auth", 34 | "django.contrib.contenttypes", 35 | "django.contrib.sessions", 36 | "django.contrib.messages", 37 | "django.contrib.staticfiles", 38 | "django_logging", 39 | ], 40 | MIDDLEWARE=[ 41 | "django.middleware.security.SecurityMiddleware", 42 | "django.contrib.sessions.middleware.SessionMiddleware", 43 | "django.middleware.common.CommonMiddleware", 44 | "django.middleware.csrf.CsrfViewMiddleware", 45 | "django.contrib.auth.middleware.AuthenticationMiddleware", 46 | "django.contrib.messages.middleware.MessageMiddleware", 47 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 48 | ], 49 | TEMPLATES=[ 50 | { 51 | "BACKEND": "django.template.backends.django.DjangoTemplates", 52 | "DIRS": [], 53 | "APP_DIRS": True, 54 | "OPTIONS": { 55 | "context_processors": [ 56 | "django.template.context_processors.debug", 57 | "django.template.context_processors.request", 58 | "django.contrib.auth.context_processors.auth", 59 | "django.contrib.messages.context_processors.messages", 60 | ], 61 | }, 62 | }, 63 | ], 64 | DJANGO_LOGGING={ 65 | "INCLUDE_LOG_iBOARD": True, 66 | "AUTO_INITIALIZATION_ENABLE": True, 67 | "INITIALIZATION_MESSAGE_ENABLE": True, 68 | "LOG_FILE_LEVELS": ["DEBUG", "INFO"], 69 | "LOG_DIR": "logs", 70 | "LOG_FILE_FORMATS": { 71 | "DEBUG": 1, 72 | "INFO": 1, 73 | }, 74 | "LOG_FILE_FORMAT_TYPES": { 75 | "DEBUG": "JSON", 76 | }, 77 | "EXTRA_LOG_FILES": { 78 | "DEBUG": True, 79 | }, 80 | "LOG_CONSOLE_LEVEL": "DEBUG", 81 | "LOG_CONSOLE_FORMAT": 1, 82 | "LOG_CONSOLE_COLORIZE": True, 83 | "LOG_DATE_FORMAT": "%Y-%m-%d %H:%M:%S", 84 | "LOG_EMAIL_NOTIFIER": { 85 | "ENABLE": False, 86 | "NOTIFY_ERROR": True, 87 | "NOTIFY_CRITICAL": False, 88 | "LOG_FORMAT": True, 89 | "USE_TEMPLATE": True, 90 | }, 91 | }, 92 | EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend", 93 | EMAIL_HOST="smtp.example.com", 94 | EMAIL_PORT=587, 95 | EMAIL_USE_TLS=True, 96 | EMAIL_HOST_USER="example@test.com", 97 | EMAIL_HOST_PASSWORD="the_password", 98 | DEFAULT_FROM_EMAIL="example@test.com", 99 | ADMIN_EMAIL="admin@test.com", 100 | LANGUAGE_CODE="en-us", 101 | TIME_ZONE="UTC", 102 | USE_I18N=True, 103 | USE_TZ=True, 104 | ROOT_URLCONF="django_logging.urls", 105 | STATIC_URL="static/" 106 | ) 107 | django.setup() 108 | 109 | configure_django_settings() -------------------------------------------------------------------------------- /django_logging/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/utils/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/utils/test_email_notifier.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from smtplib import SMTPException 4 | from typing import Tuple 5 | from unittest.mock import ANY, MagicMock 6 | 7 | import pytest 8 | 9 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 10 | from django_logging.utils.log_email_notifier.notifier import send_email_async 11 | 12 | pytestmark = [ 13 | pytest.mark.utils, 14 | pytest.mark.utils_email_notifier, 15 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 16 | ] 17 | 18 | 19 | class TestEmailNotifier: 20 | 21 | def test_send_email_async_success( 22 | self, 23 | mock_smtp: MagicMock, 24 | email_mock_settings: MagicMock, 25 | notifier_mock_logger: Tuple[MagicMock, MagicMock], 26 | ) -> None: 27 | """ 28 | Test that the send_email_async function successfully sends an email. 29 | 30 | This test verifies that when `send_email_async` is called with valid parameters: 31 | - The SMTP server is correctly initialized with the specified host and port. 32 | - The login method is called with the correct credentials. 33 | - The email is sent with the expected 'From', 'To', 'Subject', and 'Body' fields. 34 | - The SMTP connection is properly terminated with a call to `quit`. 35 | - The success log message is correctly written. 36 | 37 | Mocks: 38 | ------ 39 | - `django_logging.utils.log_email_notifier.notifier.SMTP` to simulate SMTP interactions. 40 | - `django_logging.utils.log_email_notifier.notifier.settings` to provide mock email settings. 41 | - `django_logging.utils.log_email_notifier.notifier.logger.info` and `logger.warning` to test logging behavior. 42 | 43 | Asserts: 44 | ------- 45 | - The `SMTP` object was called with the correct host and port. 46 | - The `login` method was called with the correct credentials. 47 | - The `sendmail` method was called with the correct email fields. 48 | - The `quit` method was called on the SMTP instance. 49 | - The success message was logged and no warning message was logged. 50 | """ 51 | mock_info, mock_warning = notifier_mock_logger 52 | 53 | email_sent_event = threading.Event() 54 | 55 | send_email_async( 56 | "Test Subject", "Test Body", ["to@example.com"], event=email_sent_event 57 | ) 58 | 59 | email_sent_event.wait() 60 | 61 | mock_smtp.assert_called_once_with( 62 | email_mock_settings.EMAIL_HOST, email_mock_settings.EMAIL_PORT 63 | ) 64 | mock_smtp_instance = mock_smtp.return_value 65 | 66 | mock_smtp_instance.login.assert_called_once_with( 67 | email_mock_settings.EMAIL_HOST_USER, email_mock_settings.EMAIL_HOST_PASSWORD 68 | ) 69 | 70 | sendmail_args = mock_smtp_instance.sendmail.call_args[0] 71 | 72 | expected_from = email_mock_settings.DEFAULT_FROM_EMAIL 73 | expected_to = ["to@example.com"] 74 | expected_subject = "Test Subject" 75 | expected_body = "Test Body" 76 | 77 | assert sendmail_args[0] == expected_from 78 | assert sendmail_args[1] == expected_to 79 | 80 | actual_email_content = sendmail_args[2] 81 | assert f"Subject: {expected_subject}" in actual_email_content 82 | assert expected_body in actual_email_content 83 | 84 | mock_smtp_instance.quit.assert_called_once() 85 | 86 | mock_info.assert_called_once_with( 87 | "The Record has been sent to ADMIN EMAIL successfully." 88 | ) 89 | mock_warning.assert_not_called() 90 | 91 | def test_send_email_async_failure( 92 | self, 93 | mock_smtp: MagicMock, 94 | email_mock_settings: MagicMock, 95 | notifier_mock_logger: Tuple[MagicMock, MagicMock], 96 | ) -> None: 97 | """ 98 | Test that the send_email_async function handles SMTP failures. 99 | 100 | This test verifies that when `send_email_async` encounters an SMTP exception: 101 | - The failure is logged with an appropriate error message. 102 | - The success message is not logged. 103 | 104 | Mocks: 105 | ------ 106 | - `django_logging.utils.log_email_notifier.notifier.SMTP` to simulate an SMTP failure. 107 | 108 | Asserts: 109 | ------- 110 | - The warning message was logged indicating the failure. 111 | - The success message was not logged. 112 | """ 113 | mock_info, mock_warning = notifier_mock_logger 114 | mock_smtp.side_effect = SMTPException("SMTP failure") 115 | 116 | email_sent_event = threading.Event() 117 | 118 | send_email_async( 119 | "Test Subject", "Test Body", ["to@example.com"], event=email_sent_event 120 | ) 121 | 122 | email_sent_event.wait() 123 | 124 | mock_warning.assert_called_once_with( 125 | "Email Notifier failed to send the Record: %s", ANY 126 | ) 127 | mock_info.assert_not_called() 128 | -------------------------------------------------------------------------------- /django_logging/tests/utils/test_get_conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Dict 3 | 4 | import pytest 5 | from django.conf import settings 6 | 7 | from django_logging.settings.manager import SettingsManager, settings_manager 8 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 9 | from django_logging.utils.get_conf import ( 10 | is_initialization_message_enabled, 11 | ) 12 | 13 | pytestmark = [ 14 | pytest.mark.utils, 15 | pytest.mark.utils_get_conf, 16 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 17 | ] 18 | 19 | 20 | class TestGetConf: 21 | 22 | def test_is_initialization_message_enabled(self, mock_settings: Dict) -> None: 23 | """ 24 | Test that the `is_initialization_message_enabled` function correctly reads the `INITIALIZATION_MESSAGE_ENABLE` setting. 25 | 26 | This test verifies that the `is_initialization_message_enabled` function returns `True` by default. 27 | 28 | Mocks: 29 | ------ 30 | - `django.conf.settings` to provide mock configuration values. 31 | 32 | Asserts: 33 | ------- 34 | - The default return value of `is_initialization_message_enabled` is `True`. 35 | """ 36 | # By default, INITIALIZATION_MESSAGE_ENABLE is not none 37 | assert is_initialization_message_enabled() is not None 38 | 39 | def test_logging_settings_none(self) -> None: 40 | """ 41 | Test that logging settings is None and raise `ValueError`. 42 | 43 | This test verifies that when logging settings (DJANGO_LOGGING) is None, 44 | the `get_config` function raises `ValueError`. 45 | 46 | Asserts: 47 | ------- 48 | - ValueError raised by `check_logging_settings`. 49 | """ 50 | settings.DJANGO_LOGGING = None 51 | with pytest.raises(ValueError, match="DJANGO_LOGGING must be a dictionary with configs as keys"): 52 | SettingsManager() 53 | -------------------------------------------------------------------------------- /django_logging/tests/utils/test_process_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest.mock import Mock, patch 4 | 5 | import pytest 6 | 7 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 8 | from django_logging.utils.command.process_file import setup_directories 9 | 10 | pytestmark = [ 11 | pytest.mark.utils, 12 | pytest.mark.utils_process_file, 13 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 14 | ] 15 | 16 | 17 | class TestProcessFile: 18 | """ 19 | Test suite for the process_file module focusing on FileNotFoundError. 20 | """ 21 | 22 | @patch("os.path.exists", return_value=False) 23 | def test_log_directory_not_found(self, mock_exists: Mock) -> None: 24 | """ 25 | Test if setup_directories raises FileNotFoundError when the log directory does not exist. 26 | 27 | Args: 28 | ---- 29 | mock_exists (Callable): Mock for os.path.exists. 30 | """ 31 | log_dir = "/non/existent/log_dir" 32 | sub_dir = "sub_dir" 33 | 34 | with pytest.raises(FileNotFoundError, match=f"does not exist."): 35 | setup_directories(log_dir, sub_dir) 36 | 37 | mock_exists.assert_called_once_with(log_dir) 38 | 39 | @patch("os.path.exists", side_effect=[True, False]) 40 | def test_sub_directory_not_found(self, mock_exists: Mock) -> None: 41 | """ 42 | Test if setup_directories raises FileNotFoundError when the subdirectory does not exist. 43 | 44 | Args: 45 | ---- 46 | mock_exists (Callable): Mock for os.path.exists. 47 | """ 48 | log_dir = "/existent/log_dir" 49 | sub_dir = "sub_dir" 50 | sub_directory = os.path.join(log_dir, sub_dir) 51 | 52 | with pytest.raises(FileNotFoundError, match=f"does not exist."): 53 | setup_directories(log_dir, sub_dir) 54 | 55 | mock_exists.assert_any_call(log_dir) 56 | mock_exists.assert_any_call(sub_directory) 57 | -------------------------------------------------------------------------------- /django_logging/tests/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/validators/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/tests/views/__init__.py -------------------------------------------------------------------------------- /django_logging/tests/views/test_log_iboard.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from django.urls import reverse 5 | from django.test import Client 6 | from django.contrib.auth.models import User 7 | from typing import Dict 8 | 9 | from django_logging.tests.constants import PYTHON_VERSION, PYTHON_VERSION_REASON 10 | 11 | pytestmark = [ 12 | pytest.mark.views, 13 | pytest.mark.views_log_iboard, 14 | pytest.mark.skipif(sys.version_info < PYTHON_VERSION, reason=PYTHON_VERSION_REASON), 15 | ] 16 | 17 | 18 | @pytest.mark.django_db 19 | class TestLogiBoardView: 20 | """ 21 | Test suite for the `LogiBoardView` class-based view. 22 | 23 | This test suite covers: 24 | - Access control for superuser and non-superuser. 25 | - Rendering the correct template for superuser. 26 | - Correct response and content type for non-superuser. 27 | 28 | Methods: 29 | - test_superuser_access: Ensures superusers can access the LogiBoard page. 30 | - test_non_superuser_access: Ensures non-superusers are forbidden from accessing the LogiBoard page. 31 | """ 32 | 33 | def test_superuser_access( 34 | self, client: Client, setup_users: Dict[str, User] 35 | ) -> None: 36 | """ 37 | Test that a superuser can access the `LogiBoardView` and the correct template is rendered. 38 | """ 39 | client.login(username="admin", password="adminpassword") 40 | response = client.get(reverse("log-iboard")) 41 | assert response.status_code == 200, "Superuser should have access to the page." 42 | assert ( 43 | "log_iboard.html" in response.template_name 44 | ), "Should render the correct template for superuser." 45 | 46 | def test_non_superuser_access( 47 | self, client: Client, setup_users: Dict[str, User] 48 | ) -> None: 49 | """ 50 | Test that a non-superuser receives a 403 Forbidden response when accessing the `LogiBoardView`. 51 | """ 52 | client.login(username="user", password="userpassword") 53 | response = client.get(reverse("log-iboard")) 54 | assert ( 55 | response.status_code == 403 56 | ), "Non-superuser should not have access to the page." 57 | assert ( 58 | "text/html" in response["Content-Type"] 59 | ), "'text/html' should be in Content type for forbidden access." 60 | -------------------------------------------------------------------------------- /django_logging/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from django_logging.utils.get_conf import include_log_iboard 4 | from django_logging.views.log_iboard import LogiBoardView 5 | 6 | urlpatterns = [] 7 | 8 | if include_log_iboard(): 9 | urlpatterns.append(path("log-iboard/", LogiBoardView.as_view(), name="log-iboard")) 10 | -------------------------------------------------------------------------------- /django_logging/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/utils/__init__.py -------------------------------------------------------------------------------- /django_logging/utils/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/utils/command/__init__.py -------------------------------------------------------------------------------- /django_logging/utils/command/process_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Callable, Generator, Tuple 3 | 4 | 5 | def setup_directories(log_dir: str, sub_dir: str) -> Tuple[str, str]: 6 | """Set up the directories for processing files. 7 | 8 | Args: 9 | log_dir (str): The path to the main log directory. 10 | sub_dir (str): The name of the subdirectory to check for. 11 | 12 | Returns: 13 | Tuple[str, str]: A tuple containing the paths to the subdirectory and the pretty directory. 14 | 15 | Raises: 16 | FileNotFoundError: If the log directory or subdirectory does not exist. 17 | 18 | """ 19 | if not os.path.exists(log_dir): 20 | raise FileNotFoundError(f"Directory {log_dir} does not exist.") 21 | 22 | sub_directory = os.path.join(log_dir, sub_dir) 23 | if not os.path.exists(sub_directory): 24 | raise FileNotFoundError(f"Directory {sub_directory} does not exist.") 25 | 26 | pretty_dir = os.path.join(sub_directory, "pretty") 27 | os.makedirs(pretty_dir, exist_ok=True) 28 | 29 | return sub_directory, pretty_dir 30 | 31 | 32 | def process_files( 33 | directory: str, file_extension: str, handler_function: Callable 34 | ) -> Generator[Tuple[str, str], None, None]: 35 | """Process files in a directory and apply a handler function to each. 36 | 37 | Args: 38 | directory (str): The path to the directory containing files. 39 | file_extension (str): The file extension to filter files by. 40 | handler_function (Callable): The function to apply to each processed file. 41 | 42 | Yields: 43 | Generator[Tuple[str, str], None, None]: Yields tuples of file paths and filenames for each processed file. 44 | 45 | """ 46 | for filename in os.listdir(directory): 47 | if filename.endswith(file_extension): 48 | file_path = os.path.join(directory, filename) 49 | yield file_path, filename 50 | -------------------------------------------------------------------------------- /django_logging/utils/console_colorizer.py: -------------------------------------------------------------------------------- 1 | from django_logging.constants.ansi_colors import LOG_LEVEL_COLORS, AnsiColors 2 | 3 | 4 | def colorize_log_format(log_format: str, levelname: str) -> str: 5 | colors = AnsiColors() 6 | color_mapping = { 7 | "%(asctime)s": f"{colors.BRIGHT_BLUE}%(asctime)s{colors.RESET}", 8 | "%(created)f": f"{colors.BRIGHT_BLUE}%(created)f{colors.RESET}", 9 | "%(relativeCreated)d": f"{colors.MAGENTA}%(relativeCreated)d{colors.RESET}", 10 | "%(msecs)d": f"{colors.YELLOW}%(msecs)d{colors.RESET}", 11 | "%(levelname)s": f"{LOG_LEVEL_COLORS.get(levelname, '')}%(levelname)s{colors.RESET}", 12 | "%(levelno)d": f"{colors.RED}%(levelno)d{colors.RESET}", 13 | "%(exc_info)s": f"{colors.RED}%(exc_info)s{colors.RESET}", 14 | "%(exc_text)s": f"{colors.RED}%(exc_text)s{colors.RESET}", 15 | "%(name)s": f"{colors.BRIGHT_MAGENTA}%(name)s{colors.RESET}", 16 | "%(module)s": f"{colors.PINK}%(module)s{colors.RESET}", 17 | "%(stack_info)s": f"{colors.YELLOW}%(stack_info)s{colors.RESET}", 18 | "%(filename)s": f"{colors.YELLOW}%(filename)s{colors.RESET}", 19 | "%(pathname)s": f"{colors.CYAN}%(pathname)s{colors.RESET}", 20 | "%(lineno)d": f"{colors.LIGHT_PURPLE}%(lineno)d{colors.RESET}", 21 | "%(funcName)s": f"{colors.BRIGHT_BLUE}%(funcName)s{colors.RESET}", 22 | "%(process)d": f"{colors.MAGENTA}%(process)d{colors.RESET}", 23 | "%(processName)s": f"{colors.MAGENTA}%(processName)s{colors.RESET}", 24 | "%(thread)d": f"{colors.CYAN}%(thread)d{colors.RESET}", 25 | "%(threadName)s": f"{colors.BRIGHT_MAGENTA}%(threadName)s{colors.RESET}", 26 | "%(message)s": f"{colors.ITALIC}%(message)s{colors.RESET}", 27 | "%(context)s": f"{colors.MAGENTA}%(context)s{colors.RESET}", 28 | } 29 | 30 | for placeholder, colorized in color_mapping.items(): 31 | log_format = log_format.replace(placeholder, colorized) 32 | 33 | return log_format 34 | -------------------------------------------------------------------------------- /django_logging/utils/context_manager.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from logging import Logger, PlaceHolder, getLogger 3 | from typing import Dict, Iterator, Union 4 | 5 | from django_logging.settings.conf import LogConfig, LogManager 6 | from django_logging.utils.get_conf import get_config, is_auto_initialization_enabled 7 | 8 | 9 | @contextmanager 10 | def config_setup() -> Iterator[LogManager]: 11 | """Context manager to temporarily apply a custom logging configuration. 12 | 13 | Raises: 14 | ValueError: If 'AUTO_INITIALIZATION_ENABLE' in DJNAGO_LOGGING is set to True. 15 | 16 | Yields: 17 | LogManager: The log manager instance with the custom configuration. 18 | 19 | """ 20 | if is_auto_initialization_enabled(): 21 | raise ValueError( 22 | "you most set 'AUTO_INITIALIZATION_ENABLE' to False in DJANGO_LOGGING in your settings" 23 | ) 24 | 25 | logger = getLogger() 26 | original_config = logger.manager.loggerDict.copy() 27 | original_level = logger.level 28 | original_handlers = logger.handlers.copy() 29 | 30 | try: 31 | conf = get_config() 32 | log_config = LogConfig(**conf) 33 | log_manager = LogManager(log_config) 34 | log_manager.create_log_files() 35 | log_manager.set_conf() 36 | 37 | yield log_manager 38 | finally: 39 | _restore_logging_config( 40 | logger, original_config, original_level, original_handlers 41 | ) 42 | 43 | 44 | def _restore_logging_config( 45 | logger: Logger, 46 | original_config: Dict[str, Union[Logger, PlaceHolder]], 47 | original_level: int, 48 | original_handlers: list, 49 | ) -> None: 50 | """Restore the original logging configuration. 51 | 52 | Args: 53 | logger (Logger): The root logger instance. 54 | original_config (Dict[str, Logger | PlaceHolder]): The original logger dictionary. 55 | original_level (int): The original root logger level. 56 | original_handlers (list): The original root logger handlers. 57 | 58 | """ 59 | logger.manager.loggerDict.clear() 60 | logger.manager.loggerDict.update(original_config) 61 | logger.level = original_level 62 | logger.handlers.clear() 63 | logger.handlers.extend(original_handlers) 64 | -------------------------------------------------------------------------------- /django_logging/utils/get_conf.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from django_logging.settings import settings_manager 4 | 5 | 6 | def get_config() -> Dict: 7 | """Retrieve logging configuration from the SettingsManager. 8 | 9 | Returns: 10 | Dict: A dictionary containing all necessary configurations for logging. 11 | 12 | """ 13 | 14 | config = { 15 | "log_levels": settings_manager.log_levels, 16 | "log_dir": settings_manager.log_dir, 17 | "log_file_formats": settings_manager.log_file_formats, 18 | "log_file_format_types": settings_manager.log_file_format_types, 19 | "extra_log_files": settings_manager.extra_log_files, 20 | "console_level": settings_manager.console_level, 21 | "console_format": settings_manager.console_format, 22 | "colorize_console": settings_manager.colorize_console, 23 | "log_date_format": settings_manager.log_date_format, 24 | "log_email_notifier_enable": settings_manager.email_notifier_enabled, 25 | "log_email_notifier_log_levels": settings_manager.email_notifier_log_levels, 26 | "log_email_notifier_log_format": settings_manager.email_notifier_log_format, 27 | } 28 | 29 | return config 30 | 31 | 32 | def use_email_notifier_template() -> bool: 33 | """Check whether the email notifier should use a template based on Django 34 | settings. 35 | 36 | Returns: 37 | bool: True if the email notifier should use a template, False otherwise. 38 | 39 | """ 40 | return settings_manager.use_email_notifier_template 41 | 42 | 43 | def is_auto_initialization_enabled() -> bool: 44 | """Check if the AUTO_INITIALIZATION_ENABLE for the logging system is set to 45 | True. 46 | 47 | Returns: 48 | bool: True if AUTO_INITIALIZATION_ENABLE, False otherwise. Defaults to True if not specified. 49 | 50 | """ 51 | return settings_manager.auto_initialization_enabled 52 | 53 | 54 | def is_initialization_message_enabled() -> bool: 55 | """Check if the INITIALIZATION_MESSAGE_ENABLE is set to True. 56 | 57 | Returns: 58 | bool: True if INITIALIZATION_MESSAGE_ENABLE is True, False otherwise. 59 | 60 | """ 61 | return settings_manager.initialization_message_enabled 62 | 63 | 64 | def is_log_sql_queries_enabled() -> bool: 65 | """Check if the LOG_SQL_QUERIES_ENABLE for the logging system is set to 66 | True. 67 | 68 | Returns: 69 | bool: True if LOG_SQL_QUERIES_ENABLE, False otherwise. 70 | 71 | """ 72 | return settings_manager.log_sql_queries_enabled 73 | 74 | 75 | def get_log_dir_size_limit() -> int: 76 | """Check for the LOG_DIR_SIZE_LIMIT for managing the log directory size. 77 | 78 | Returns: 79 | int: the limit of log directory size in MB. Defaults to 1024 MB if not specified. 80 | 81 | """ 82 | return settings_manager.log_dir_size_limit 83 | 84 | 85 | def include_log_iboard() -> bool: 86 | """Check if the INCLUDE_LOG_iBOARD for the logging system is set to True. 87 | 88 | Returns: 89 | bool: True if INCLUDE_LOG_iBOARD, False otherwise. 90 | 91 | """ 92 | return settings_manager.include_log_iboard 93 | -------------------------------------------------------------------------------- /django_logging/utils/log_email_notifier/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/utils/log_email_notifier/__init__.py -------------------------------------------------------------------------------- /django_logging/utils/log_email_notifier/log_and_notify.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | from typing import Any, Dict, Optional 4 | 5 | from django.conf import settings 6 | 7 | from django_logging.constants.log_format_options import FORMAT_OPTIONS 8 | from django_logging.handlers import EmailHandler 9 | from django_logging.settings.conf import LogConfig 10 | from django_logging.utils.get_conf import get_config 11 | from django_logging.utils.log_email_notifier.notifier import send_email_async 12 | 13 | 14 | def log_and_notify_admin( 15 | logger: logging.Logger, 16 | level: int, 17 | message: str, 18 | extra: Optional[Dict[str, Any]] = None, 19 | ) -> None: 20 | # Get the caller's frame to capture the correct module, file, and line number 21 | frame = inspect.currentframe().f_back # type: ignore 22 | email_notifier_enable = get_config().get("log_email_notifier_enable", False) 23 | 24 | if not email_notifier_enable: 25 | raise ValueError( 26 | "Email notifier is disabled. Please set the 'ENABLE' option to True in the 'LOG_EMAIL_NOTIFIER'" 27 | " in DJANGO_LOGGING in your settings to activate email notifications." 28 | ) 29 | 30 | _format = get_config().get("log_email_notifier_log_format", FORMAT_OPTIONS[1]) 31 | 32 | try: 33 | # create a LogRecord 34 | log_record = logger.makeRecord( 35 | name=logger.name, 36 | level=level, 37 | fn=frame.f_code.co_filename, # type: ignore 38 | lno=frame.f_lineno, # type: ignore 39 | msg=message, 40 | args=(), 41 | exc_info=None, 42 | func=frame.f_code.co_name, # type: ignore 43 | extra=extra, 44 | ) 45 | 46 | # Pass the LogRecord to the logger's handlers 47 | logger.handle(log_record) 48 | except (TypeError, AttributeError) as e: 49 | raise ValueError( 50 | f"Failed to log message due to invalid param. Original error: {e}" 51 | ) from e 52 | # Create a formatter instance and pass the email_notifier format 53 | formatter = logging.Formatter(LogConfig.resolve_format(_format)) 54 | 55 | # Format the log message using the formatter 56 | formatted_message = formatter.format(log_record) 57 | 58 | request = extra.get("request") if extra else None 59 | 60 | # Render the email template with the formatted message 61 | email_body = EmailHandler.render_template(formatted_message, request) 62 | 63 | subject = f"New Log Record: {logging.getLevelName(level)}" 64 | admin_email = getattr(settings, "ADMIN_EMAIL") 65 | if not admin_email: 66 | raise ValueError( 67 | "'ADMIN EMAIL' not provided, please provide 'ADMIN_EMAIL' in your settings" 68 | ) 69 | 70 | send_email_async(subject, email_body, [admin_email]) 71 | -------------------------------------------------------------------------------- /django_logging/utils/log_email_notifier/notifier.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from email.mime.multipart import MIMEMultipart 4 | from email.mime.text import MIMEText 5 | from smtplib import SMTP 6 | from typing import List, Optional 7 | 8 | from django.conf import settings 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def send_email_async( 14 | subject: str, 15 | body: str, 16 | recipient_list: List[str], 17 | event: Optional[threading.Event] = None, 18 | ) -> None: 19 | def send_email() -> None: 20 | msg = MIMEMultipart() 21 | msg["From"] = settings.DEFAULT_FROM_EMAIL 22 | msg["To"] = ", ".join(recipient_list) 23 | msg["Subject"] = subject 24 | 25 | msg.attach(MIMEText(body, "html")) 26 | 27 | try: 28 | server = SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT) 29 | server.starttls() 30 | server.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD) 31 | server.sendmail( 32 | settings.DEFAULT_FROM_EMAIL, recipient_list, msg.as_string() 33 | ) 34 | server.quit() 35 | logger.info("The Record has been sent to ADMIN EMAIL successfully.") 36 | 37 | except Exception as e: # pylint: disable=broad-exception-caught 38 | logger.warning("Email Notifier failed to send the Record: %s", e) 39 | 40 | finally: 41 | if event: 42 | event.set() # set event that waits until email send. (used for Tests) 43 | 44 | # Start a new thread to send the email asynchronously 45 | email_thread = threading.Thread(target=send_email) 46 | email_thread.start() 47 | -------------------------------------------------------------------------------- /django_logging/utils/time.py: -------------------------------------------------------------------------------- 1 | def format_elapsed_time(elapsed_time: float) -> str: 2 | """Formats the elapsed time into a human-readable string. 3 | 4 | If the time is less than a minute, returns only seconds. Otherwise, 5 | returns the time in minutes and seconds. 6 | 7 | Args: 8 | elapsed_time: Time in seconds as a float. 9 | 10 | Returns: 11 | A string representing the formatted time. 12 | 13 | """ 14 | minutes, seconds = divmod(elapsed_time, 60) 15 | 16 | if minutes > 0: 17 | return f"{int(minutes)} minute(s) and {seconds:.2f} second(s)" 18 | return f"{seconds:.2f} second(s)" 19 | -------------------------------------------------------------------------------- /django_logging/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/validators/__init__.py -------------------------------------------------------------------------------- /django_logging/validators/email_settings_validator.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from django.conf import settings 4 | from django.core.checks import Error 5 | 6 | from django_logging.constants.required_email_settings import ( 7 | EMAIL_REQUIRED_SETTINGS, 8 | NOTIFIER_EXTRA_REQUIRED_SETTING, 9 | ) 10 | 11 | 12 | def check_email_settings(require_admin_email: bool = True) -> List[Error]: 13 | """Check if all required email settings are present in the settings file. 14 | 15 | Returns a list of errors if any of the required email settings are 16 | missing. 17 | 18 | """ 19 | errors: List[Error] = [] 20 | if require_admin_email: 21 | EMAIL_REQUIRED_SETTINGS.append(NOTIFIER_EXTRA_REQUIRED_SETTING) 22 | 23 | missed_settings = [ 24 | setting 25 | for setting in EMAIL_REQUIRED_SETTINGS 26 | if not getattr(settings, setting, None) 27 | ] 28 | 29 | if missed_settings: 30 | missing = ", ".join(missed_settings) 31 | errors.append( 32 | Error( 33 | f"Missing required email settings: {missing}", 34 | hint="Email settings required because you trying to send an email," 35 | "Ensure all required email settings are properly configured in your settings file.", 36 | id="django_logging.E021", 37 | ) 38 | ) 39 | 40 | return errors 41 | -------------------------------------------------------------------------------- /django_logging/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazarus-org/django_logging/528b4a6618abb0bf4b7977ae38429170d6fb55d9/django_logging/views/__init__.py -------------------------------------------------------------------------------- /django_logging/views/log_iboard.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from django.http import HttpRequest, HttpResponse 4 | from django.shortcuts import render 5 | from django.views.generic import TemplateView 6 | 7 | 8 | class LogiBoardView(TemplateView): 9 | """View to render the LogiBoard page for superusers. 10 | 11 | Non-superusers are denied access and shown an error response page 12 | with a 403 status code. 13 | 14 | """ 15 | 16 | template_name = "log_iboard.html" 17 | 18 | def get( 19 | self, request: HttpRequest, *args: Any, **kwargs: Dict[str, Any] 20 | ) -> HttpResponse: 21 | """Handles GET requests. Renders the LogiBoard page for superusers, 22 | otherwise returns a 403 error response for non-superusers. 23 | 24 | Args: 25 | request (HttpRequest): The HTTP request object. 26 | *args (Any): Additional positional arguments. 27 | **kwargs (Dict[str, Any]): Additional keyword arguments. 28 | 29 | Returns: 30 | HttpResponse: The rendered LogiBoard page for superusers or an error response page for non-superusers. 31 | 32 | """ 33 | if request.user.is_superuser: 34 | return super().get(request, *args, **kwargs) 35 | 36 | return render( 37 | request, 38 | "error_response.html", 39 | { 40 | "title": "Access Denied", 41 | "message": "You do not have permission to view this page.", 42 | }, 43 | status=403, 44 | ) 45 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /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 | import os 10 | import sys 11 | sys.path.insert(0, os.path.abspath('.')) 12 | 13 | # -- Project information ----------------------------------------------------- 14 | 15 | project = 'django_logging' 16 | copyright = '2024, Lazarus' 17 | author = 'ARYAN-NIKNEZHAD, MEHRSHAD-MIRSHEKARY' 18 | 19 | # The full version, including alpha/beta/rc tags 20 | release = '2.0.0' 21 | 22 | master_doc = 'index' 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', # Auto-generate API documentation 31 | 'sphinx.ext.napoleon', # Support for NumPy and Google style docstrings 32 | 'sphinx.ext.viewcode', # Add links to highlighted source code 33 | 'sphinx.ext.githubpages' # Publish documentation on GitHub Pages 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | # The theme to use for HTML and HTML Help pages. See the documentation for 47 | # a list of builtin themes. 48 | html_theme = "sphinx_rtd_theme" 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] 54 | 55 | # Custom static files 56 | html_logo = '_static/logo.png' 57 | html_favicon = '_static/favicon.ico' 58 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============================== 3 | 4 | We’re excited that you’re interested in contributing to `django_logging`! Whether you’re fixing a bug, adding a feature, or improving the project, your help is appreciated. 5 | 6 | Overview 7 | -------- 8 | 9 | - **Setting Up Your Environment** 10 | - **Testing Your Changes** 11 | - **Code Style Guidelines** 12 | - **Utilizing Pre-commit Hooks** 13 | - **Creating a Pull Request** 14 | - **Reporting Issues** 15 | - **Resources** 16 | 17 | Setting Up Your Environment 18 | --------------------------- 19 | 20 | 1. **Fork the Repository:** 21 | 22 | Begin by forking the `django_logging` repository on GitHub. This creates your own copy where you can make changes. 23 | 24 | 2. **Clone Your Fork:** 25 | 26 | Use the following command to clone your fork locally: 27 | 28 | .. code-block:: bash 29 | 30 | git clone https://github.com/your-username/django_logging.git 31 | cd django_logging 32 | 33 | 3. **Install Dependencies:** 34 | 35 | Install the necessary dependencies using ``Poetry``. If Poetry isn't installed on your machine, you can find installation instructions on the `Poetry website `_. 36 | 37 | .. code-block:: bash 38 | 39 | poetry install 40 | 41 | 4. **Create a Feature Branch:** 42 | 43 | It’s a good practice to create a new branch for your work: 44 | 45 | .. code-block:: bash 46 | 47 | git checkout -b feature/your-feature-name 48 | 49 | Testing Your Changes 50 | -------------------- 51 | 52 | We use ``pytest`` for running tests. Before submitting your changes, ensure that all tests pass: 53 | 54 | .. code-block:: bash 55 | 56 | poetry run pytest 57 | 58 | If you’re adding a new feature or fixing a bug, don’t forget to write tests to cover your changes. 59 | 60 | Code Style Guidelines 61 | ---------------------- 62 | 63 | Maintaining a consistent code style is crucial. We use ``black`` for code formatting and ``isort`` for import sorting. Make sure your code adheres to these styles: 64 | 65 | .. code-block:: bash 66 | 67 | poetry run black . 68 | poetry run isort . 69 | 70 | For linting, ``pylint`` is used to enforce style and catch potential errors: 71 | 72 | .. code-block:: bash 73 | 74 | poetry run pylint django_logging 75 | 76 | Utilizing Pre-commit Hooks 77 | -------------------------- 78 | 79 | Pre-commit hooks are used to automatically check and format code before you make a commit. This ensures consistency and quality in the codebase. 80 | 81 | 1. **Install Pre-commit:** 82 | 83 | .. code-block:: bash 84 | 85 | poetry add --group dev pre-commit 86 | 87 | 2. **Set Up the Hooks:** 88 | 89 | Install the pre-commit hooks by running: 90 | 91 | .. code-block:: bash 92 | 93 | poetry run pre-commit install 94 | 95 | 3. **Manual Hook Execution (Optional):** 96 | 97 | To run all hooks manually on your codebase: 98 | 99 | .. code-block:: bash 100 | 101 | poetry run pre-commit run --all-files 102 | 103 | Creating a Pull Request 104 | ----------------------- 105 | 106 | Once your changes are ready, follow these steps to submit them: 107 | 108 | 1. **Commit Your Changes:** 109 | 110 | Write clear and concise commit messages. Following the `Conventional Commits `_ format is recommended: 111 | 112 | .. code-block:: bash 113 | 114 | git commit -am 'feat: add custom logging formatter' 115 | 116 | 2. **Push Your Branch:** 117 | 118 | Push your branch to your fork on GitHub: 119 | 120 | .. code-block:: bash 121 | 122 | git push origin feature/your-feature-name 123 | 124 | 3. **Open a Pull Request:** 125 | 126 | Go to the original `django_logging `_ repository and open a pull request. Include a detailed description of your changes and link any related issues. 127 | 128 | 4. **Respond to Feedback:** 129 | 130 | After submitting, a maintainer will review your pull request. Be prepared to make revisions based on their feedback. 131 | 132 | Reporting Issues 133 | ---------------- 134 | 135 | Found a bug or have a feature request? We’d love to hear from you! 136 | 137 | 1. **Open an Issue:** 138 | 139 | Head over to the `Issues` section of the `django_logging` repository and click "New Issue". 140 | 141 | 2. **Describe the Problem:** 142 | 143 | Fill out the issue template with as much detail as possible. This helps us understand and address the issue more effectively. 144 | 145 | Resources 146 | --------- 147 | 148 | Here are some additional resources that might be helpful: 149 | 150 | - `Poetry Documentation `_ 151 | - `Black Documentation `_ 152 | - `isort Documentation `_ 153 | - `pytest Documentation `_ 154 | - `pylint Documentation `_ 155 | - `Pre-commit Documentation `_ 156 | 157 | ---- 158 | 159 | Thank you for your interest in contributing to `django_logging`! We look forward to your contributions. 160 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django_logging Documentation! 2 | ======================================== 3 | 4 | .. |br| raw:: html 5 | 6 |
7 | 8 | .. image:: https://img.shields.io/github/license/lazarus-org/django_logging 9 | :target: https://github.com/lazarus-org/django_logging/blob/main/LICENSE 10 | :alt: License 11 | 12 | .. image:: https://img.shields.io/pypi/v/dj-logging 13 | :target: https://pypi.org/project/dj-logging/ 14 | :alt: PyPI release 15 | 16 | .. image:: https://img.shields.io/readthedocs/django-logging 17 | :target: https://django-logging.readthedocs.io/en/latest/ 18 | :alt: Documentation 19 | 20 | .. image:: https://img.shields.io/badge/pylint-10/10-brightgreen?logo=python&logoColor=blue 21 | :target: https://www.pylint.org/ 22 | :alt: Pylint 23 | 24 | .. image:: https://img.shields.io/pypi/pyversions/dj-logging 25 | :target: https://pypi.org/project/dj-logging/ 26 | :alt: Supported Python versions 27 | 28 | .. image:: https://img.shields.io/pypi/djversions/dj-logging 29 | :target: https://pypi.org/project/dj-logging/ 30 | :alt: Supported Django versions 31 | 32 | .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=yellow 33 | :target: https://github.com/pre-commit/pre-commit 34 | :alt: pre-commit 35 | 36 | .. image:: https://img.shields.io/github/issues/lazarus-org/django_logging 37 | :target: https://github.com/lazarus-org/django_logging/issues 38 | :alt: Open Issues 39 | 40 | .. image:: https://img.shields.io/github/last-commit/lazarus-org/django_logging 41 | :target: https://github.com/lazarus-org/django_logging/commits/main 42 | :alt: Last Commit 43 | 44 | .. image:: https://img.shields.io/github/languages/top/lazarus-org/django_logging 45 | :target: https://github.com/lazarus-org/django_logging 46 | :alt: Languages 47 | 48 | .. image:: https://img.shields.io/codecov/c/github/lazarus-org/django_logging/main 49 | :target: https://codecov.io/gh/lazarus-org/django_logging 50 | :alt: Coverage 51 | 52 | 53 | 54 | |br| 55 | 56 | `django_logging` is a powerful yet simple Django package that extends and enhances Python's built-in ``logging`` without relying on any **third-party** libraries. Our goal is to keep things straightforward while providing flexible and customizable logging solutions that are specifically designed for Django applications. 57 | 58 | One of the key advantages of ``django_logging`` is its seamless integration. Get started with django_logging in your existing projects without refactoring any code. Even if you're already using the **default logging setup**, you can instantly upgrade to advanced features with just a simple installation. No extra changes or complicated setup required! 59 | 60 | imagine you have a Django package that was developed a few years ago and already uses Python's built-in ``logging``. Refactoring the entire codebase to use another logging package would be a daunting task. But with ``django_logging``, you don't have to worry about that. Simply install django_logging and enjoy all its advanced features with logging each ``LEVEL`` in separate files with three extra formats (``json``, ``xml``, ``flat``) **without having to make any changes** to your existing code. 61 | 62 | Supported Versions 63 | ------------------ 64 | 65 | `django_logging` supports the following combinations of Django and Python versions: 66 | 67 | ========== =========================== 68 | Django Python 69 | ========== =========================== 70 | 4.2 3.9, 3.10, 3.11, 3.12, 3.13 71 | 5.0 3.10, 3.11, 3.12, 3.13 72 | 5.1 3.10, 3.11, 3.12, 3.13 73 | ========== =========================== 74 | 75 | Documentation 76 | ------------- 77 | 78 | The documentation is organized into the following sections: 79 | 80 | .. toctree:: 81 | :maxdepth: 2 82 | 83 | quick_start 84 | usage 85 | log_iboard 86 | settings 87 | contributing 88 | rules 89 | 90 | Issues 91 | ------ 92 | If you have questions or have trouble using the app please file a bug report at: 93 | 94 | https://github.com/lazarus-org/django_logging/issues 95 | 96 | 97 | Indices and tables 98 | ================== 99 | 100 | * :ref:`search` 101 | * :ref:`genindex` 102 | * :ref:`modindex` -------------------------------------------------------------------------------- /docs/log_iboard.rst: -------------------------------------------------------------------------------- 1 | LogiBoard Integration 2 | ===================== 3 | 4 | The ``LogiBoard`` in the ``django_logging`` package provides an interface for uploading, extracting, and exploring log files that have been zipped and shared via email. This allows for easier log management. 5 | 6 | .. note:: 7 | 8 | **Superuser Access Only**: Only superusers have access to the LogiBoard URL. If accessed by a non-superuser, they will get Access Denied page made by Lazarus. 9 | 10 | Setup Instructions 11 | ------------------ 12 | 13 | 1. **Add to URLs**: 14 | Include the following in your URL configuration to enable access to LogiBoard: 15 | 16 | .. code-block:: python 17 | 18 | from django.urls import path, include 19 | 20 | 21 | urlpatterns = [ 22 | # ... 23 | path("django-logging/", include("django_logging.urls")), 24 | # ... 25 | ] 26 | 27 | LogiBoard will be accessible at the following link in your project after setting it up: 28 | ``/django-logging/log-iboard/`` 29 | 30 | 2. **Static Files**: 31 | Run the following command to collect and prepare the static files necessary for LogiBoard's interface: 32 | 33 | .. code-block:: bash 34 | 35 | python manage.py collectstatic 36 | 37 | The `collectstatic` command is required to gather and serve static assets (such as JavaScript, CSS, and images) used by LogiBoard. This ensures the front-end of the log upload and browsing interface works correctly. 38 | 39 | 3. **Enable LogiBoard**: 40 | In your settings file, ensure the following setting is added under ``DJANGO_LOGGING``: 41 | 42 | .. code-block:: python 43 | 44 | DJANGO_LOGGING = { 45 | # ... 46 | "INCLUDE_LOG_iBOARD": True, 47 | # ... 48 | } 49 | 50 | This setting ensures that LogiBoard is available in your project. 51 | 52 | 53 | Using LogiBoard 54 | --------------- 55 | 56 | Logiboard is designed to help administrators easily review log files that have been zipped and sent via email (generated by the ``send_logs`` management command). This is particularly useful for remotely retrieving log files from production systems or shared environments. 57 | 58 | - **Access Logiboard**: Go to the link ``/django-logging/log-iboard/`` in your project to open the LogiBoard interface. 59 | - **Upload ZIP Files**: Click the upload icon or drag and drop ZIP files into the upload area. Only ZIP files are supported for upload. 60 | - **Explore Log Files**: After uploading, Logiboard automatically extracts the log files and displays their structure. You can browse through directories and open log files in supported formats, such as ``.log``, ``.txt``, ``.json``, and ``.xml``. 61 | - **Upload New Files**: Once you're done reviewing, click the "Send Another" button to upload and explore more logs. 62 | 63 | LogiBoard makes it simple to manage and review logs, ensuring you can quickly access and analyze critical log data. 64 | -------------------------------------------------------------------------------- /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 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | Getting Started with `django_logging` is simple. Follow these steps to get up and running quickly: 5 | 6 | 1. **Install the Package** 7 | 8 | first, Install `django_logging` via pip: 9 | 10 | .. code-block:: shell 11 | 12 | $ pip install dj-logging 13 | 14 | 2. **Add to Installed Apps** 15 | 16 | Add `django_logging` to your ``INSTALLED_APPS`` in your Django settings file: 17 | 18 | .. code-block:: python 19 | 20 | INSTALLED_APPS = [ 21 | # ... 22 | "django_logging", 23 | # ... 24 | ] 25 | 26 | 3. **Run Your Server** 27 | 28 | Start your Django Development server to verify the installation: 29 | 30 | .. code-block:: shell 31 | 32 | python manage.py runserver 33 | 34 | when the server starts, you'll see an initialization message like this in your *console*: 35 | 36 | .. code-block:: text 37 | 38 | INFO | django_logging | Logging initialized with the following configurations: 39 | Log File levels: ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']. 40 | Log files are being written to: logs. 41 | Console output level: DEBUG. 42 | Colorize console: True. 43 | Log date format: %Y-%m-%d %H:%M:%S. 44 | Email notifier enabled: False. 45 | 46 | By default, django_logging will log each level to its own file: 47 | 48 | - DEBUG : ``logs/debug.log`` 49 | - INFO : ``logs/info.log`` 50 | - WARNING : ``logs/warning.log`` 51 | - ERROR : ``logs/error.log`` 52 | - CRITICAL : ``logs/critical.log`` 53 | 54 | In addition, logs will be displayed in **colorized** mode in the ``console``, making it easier to distinguish between different log levels. 55 | 56 | That's it! `django_logging` is ready to use. For further customization, refer to the :doc:`Settings `. -------------------------------------------------------------------------------- /docs/rules.rst: -------------------------------------------------------------------------------- 1 | Roles 2 | ===== 3 | 4 | The `django_logging` package provides several logging roles to cater to different logging needs and contexts. These roles define specific behaviors and configurations, making it easier to manage logging in a Django application. 5 | 6 | Available Roles 7 | --------------- 8 | 9 | 1. **Basic Logger Role** 10 | 11 | The Basic Logger Role is designed for standard logging use cases. It handles logging messages of different severity levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) and outputs them to both the console and log files, depending on the settings. 12 | 13 | **Key Features:** 14 | - Supports all standard logging levels. 15 | - Configurable to write logs to a file, the console, or both. 16 | - Can be integrated easily across different modules in your Django project. 17 | 18 | 2. **Request Logging Role** 19 | 20 | The Request Logging Role is specifically designed to log HTTP request details. This role captures information such as the ``request path``, ``user``, ``IP address``, and ``user agent``. 21 | 22 | **Key Features:** 23 | - Logs details of incoming requests. 24 | - Can be customized to include additional request metadata. 25 | - Useful for monitoring and debugging purposes. 26 | 27 | **Example Configuration:** 28 | 29 | .. code-block:: 30 | 31 | MIDDLEWARE = [ 32 | ... 33 | 'django_logging.middleware.RequestLogMiddleware', 34 | ... 35 | ] 36 | 37 | 3. **Email Notifier Role** 38 | 39 | The Email Notifier Role enables sending logs via email. This is particularly useful for critical errors that require immediate attention. 40 | 41 | **Key Features:** 42 | - Sends log messages via email to designated recipients. 43 | - Supports customizable log levels for triggering email notifications. 44 | - Requires proper email configuration in the Django settings. 45 | 46 | **Example Usage:** 47 | 48 | .. code-block:: python 49 | 50 | from django_logging.utils.log_email_notifier.log_and_notify import log_and_notify_admin 51 | import logging 52 | 53 | logger = logging.getLogger(__name__) 54 | 55 | log_and_notify_admin(logger, logging.CRITICAL, "Critical error occurred!") 56 | 57 | **Note:** To use this role, ensure that ``ENABLE`` option of ``LOG_EMAIL_NOTIFIER`` is set to ``True``. 58 | 59 | 4. **Context Manager Role** 60 | 61 | The Context Manager Role provides a way to temporarily apply different logging configurations within a specific block of code. This is useful when you need to adjust logging behavior for certain operations without affecting the global configuration. 62 | 63 | **Key Features:** 64 | - Allows temporary configuration changes within a context. 65 | - Automatically reverts to the original configuration after exiting the context. 66 | - Useful for testing or handling specific scenarios where different logging behavior is required. 67 | 68 | **Example Usage:** 69 | 70 | .. code-block:: python 71 | 72 | from django_logging.utils.context_manager import config_setup 73 | import logging 74 | 75 | logger = logging.getLogger(__name__) 76 | 77 | with config_setup(): 78 | logger.info("This log uses temporary configurations") 79 | 80 | 81 | -------------------------------------------------------------------------------- /packages/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 ; python_version >= "3.9" and python_version < "4.0" 2 | django==4.2.20 ; python_version == "3.9" 3 | django==5.2 ; python_version >= "3.10" and python_version < "4.0" 4 | sqlparse==0.5.3 ; python_version >= "3.9" and python_version < "4.0" 5 | typing-extensions==4.13.2 ; python_version >= "3.9" and python_version < "3.11" 6 | tzdata==2025.2 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | py313-django{52, 51, 50, 40} 6 | py312-django{52, 51, 50, 40} 7 | py311-django{52, 51, 50, 40} 8 | py310-django{52, 51, 50, 40} 9 | py39-django40 10 | 11 | [testenv] 12 | description = Run Pytest tests with multiple django versions 13 | deps = 14 | pytest 15 | pytest-asyncio 16 | pytest-cov 17 | pytest-django 18 | django40: django<5.0,>=4.2 19 | django50: django<5.1,>=5 20 | django51: django<5.2,>=5.1 21 | django52: django<5.3,>=5.2 22 | commands = 23 | pytest --cov=django_logging --cov-report=html 24 | develop = True 25 | 26 | [testenv:bandit] 27 | description = Run security checks 28 | skip_install = true 29 | deps = 30 | bandit 31 | commands = 32 | bandit -r django_logging 33 | 34 | [testenv:pre-commit] 35 | description = Run pre-commit hooks 36 | skip_install = true 37 | deps = 38 | pre-commit 39 | commands = 40 | pre-commit run --all-files 41 | 42 | [gh-actions] 43 | python = 44 | 3.9: py39 45 | 3.10: py310 46 | 3.11: py311 47 | 3.12: py312 48 | 3.13: py313 49 | --------------------------------------------------------------------------------