├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_rest_logger ├── __init__.py ├── handlers.py ├── log.py └── settings.py ├── requirements.txt ├── requirements ├── codestyle.txt ├── development.txt ├── documentation.txt ├── optionals.txt ├── packaging.txt └── testing.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *~ 4 | .* 5 | 6 | html/ 7 | htmlcov/ 8 | coverage/ 9 | build/ 10 | dist/ 11 | *.egg-info/ 12 | MANIFEST 13 | 14 | bin/ 15 | include/ 16 | lib/ 17 | local/ 18 | 19 | !.gitignore 20 | !.travis.yml 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Seedstars 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | recursive-exclude * __pycache__ 3 | recursive-exclude * *.py[co] 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | REST Logger for Django 2 | ======================= 3 | 4 | |Travis| 5 | 6 | .. |Travis| image:: https://travis-ci.org/pedrorodriguesgomes/django-rest-logger.svg 7 | :target: https://travis-ci.org/pedrorodriguesgomes/django-rest-logger 8 | 9 | 10 | **REST logger for Django** 11 | 12 | Overview 13 | -------- 14 | 15 | This package provides a logger specifically designed to REST APIs using Django. 16 | 17 | Requirements 18 | ------------ 19 | 20 | - Python (2.7, 3.3, 3.4, 3.5, 3.6) 21 | - Django (1.8, 1.11) 22 | - Django REST Framework (3.8) 23 | 24 | Installation 25 | ------------ 26 | 27 | Install using ``pip``\ ... 28 | 29 | .. code:: bash 30 | 31 | $ pip install django-rest-logger 32 | 33 | Usage 34 | ------------ 35 | 36 | Add default exception handler to your Django REST Framework settings: 37 | 38 | .. code:: python 39 | 40 | REST_FRAMEWORK = { 41 | 'EXCEPTION_HANDLER': 'django_rest_logger.handlers.rest_exception_handler', 42 | } 43 | 44 | 45 | Add logger configuration to your development settings.py: 46 | 47 | .. code:: python 48 | 49 | LOGGING = { 50 | 'version': 1, 51 | 'disable_existing_loggers': True, 52 | 'root': { 53 | 'level': 'DEBUG', 54 | 'handlers': ['django_rest_logger_handler'], 55 | }, 56 | 'formatters': { 57 | 'verbose': { 58 | 'format': '%(levelname)s %(asctime)s %(module)s ' 59 | '%(process)d %(thread)d %(message)s' 60 | }, 61 | }, 62 | 'handlers': { 63 | 'django_rest_logger_handler': { 64 | 'level': 'DEBUG', 65 | 'class': 'logging.StreamHandler', 66 | 'formatter': 'verbose' 67 | } 68 | }, 69 | 'loggers': { 70 | 'django.db.backends': { 71 | 'level': 'ERROR', 72 | 'handlers': ['django_rest_logger_handler'], 73 | 'propagate': False, 74 | }, 75 | 'django_rest_logger': { 76 | 'level': 'DEBUG', 77 | 'handlers': ['django_rest_logger_handler'], 78 | 'propagate': False, 79 | }, 80 | }, 81 | } 82 | 83 | DEFAULT_LOGGER = 'django_rest_logger' 84 | 85 | LOGGER_EXCEPTION = DEFAULT_LOGGER 86 | LOGGER_ERROR = DEFAULT_LOGGER 87 | LOGGER_WARNING = DEFAULT_LOGGER 88 | 89 | 90 | Example for logger configuration using Sentry to be used in your production settings.py: 91 | 92 | .. code:: python 93 | 94 | LOGGING = { 95 | 'version': 1, 96 | 'disable_existing_loggers': True, 97 | 'root': { 98 | 'level': 'WARNING', 99 | 'handlers': ['sentry'], 100 | }, 101 | 'formatters': { 102 | 'verbose': { 103 | 'format': '%(levelname)s %(asctime)s %(module)s ' 104 | '%(process)d %(thread)d %(message)s' 105 | }, 106 | }, 107 | 'handlers': { 108 | 'sentry': { 109 | 'level': 'ERROR', 110 | 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 111 | }, 112 | 'console': { 113 | 'level': 'DEBUG', 114 | 'class': 'logging.StreamHandler', 115 | 'formatter': 'verbose' 116 | } 117 | }, 118 | 'loggers': { 119 | 'django.db.backends': { 120 | 'level': 'ERROR', 121 | 'handlers': ['console'], 122 | 'propagate': False, 123 | }, 124 | 'raven': { 125 | 'level': 'DEBUG', 126 | 'handlers': ['sentry'], 127 | 'propagate': False, 128 | }, 129 | 'sentry.errors': { 130 | 'level': 'DEBUG', 131 | 'handlers': ['sentry'], 132 | 'propagate': False, 133 | }, 134 | }, 135 | } 136 | 137 | DEFAULT_LOGGER = 'raven' 138 | 139 | LOGGER_EXCEPTION = DEFAULT_LOGGER 140 | LOGGER_ERROR = DEFAULT_LOGGER 141 | LOGGER_WARNING = DEFAULT_LOGGER 142 | 143 | -------------------------------------------------------------------------------- /django_rest_logger/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'django_rest_logger' 4 | __version__ = '1.0.4' 5 | __author__ = 'Pedro Gomes' 6 | __license__ = 'MIT' 7 | __copyright__ = 'Copyright 2020 Seedstars' 8 | 9 | # Version synonym 10 | VERSION = __version__ 11 | -------------------------------------------------------------------------------- /django_rest_logger/handlers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import PermissionDenied 2 | from django.http import Http404 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | try: 6 | from django.utils import six 7 | except ImportError: 8 | import six 9 | 10 | from rest_framework import status, exceptions 11 | from rest_framework.response import Response 12 | 13 | from .log import exception as log_exception 14 | 15 | 16 | def rest_exception_handler(exc, context): 17 | """ 18 | Return the response that should be used for any given exception. 19 | 20 | By default we handle the REST framework `APIException`, and also 21 | Django's built-in `ValidationError`, `Http404` and `PermissionDenied` 22 | exceptions. 23 | 24 | Any unhandled exceptions may return `None`, which will cause a 500 error 25 | to be raised. 26 | """ 27 | if isinstance(exc, exceptions.APIException): 28 | headers = {} 29 | if getattr(exc, 'auth_header', None): 30 | headers['WWW-Authenticate'] = exc.auth_header 31 | if getattr(exc, 'wait', None): 32 | headers['Retry-After'] = '%d' % exc.wait 33 | 34 | if isinstance(exc.detail, dict): 35 | data = exc.detail 36 | elif isinstance(exc.detail, list): 37 | data = {'non_field_errors': exc.detail} 38 | else: 39 | data = {'non_field_errors': [exc.detail]} 40 | 41 | return Response(data, status=exc.status_code, headers=headers) 42 | 43 | elif isinstance(exc, Http404): 44 | msg = _('Not found.') 45 | data = {'non_field_errors': [six.text_type(msg)]} 46 | return Response(data, status=status.HTTP_404_NOT_FOUND) 47 | 48 | elif isinstance(exc, PermissionDenied): 49 | msg = _('Permission denied.') 50 | data = {'non_field_errors': [six.text_type(msg)]} 51 | return Response(data, status=status.HTTP_403_FORBIDDEN) 52 | else: 53 | log_exception() 54 | msg = _('Server Error. Please try again later.') 55 | data = {'non_field_errors': [six.text_type(msg)]} 56 | return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 57 | -------------------------------------------------------------------------------- /django_rest_logger/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from typing import Any 4 | 5 | from django.conf import settings 6 | 7 | 8 | def exception(msg='', details={}, exc_info=True): 9 | """ 10 | Log an exception during execution. 11 | 12 | This method should be used whenever an user wants to log a 13 | generic exception that is not properly managed. 14 | 15 | Args: 16 | message: message identifying the error 17 | details: dict with context details 18 | status_code: http status code associated with the error 19 | exc_info: boolean to indicate if exception information should be added to the logging message 20 | 21 | :return: 22 | """ 23 | logger = logging.getLogger(settings.LOGGER_EXCEPTION) 24 | logger.exception(msg=msg or sys.exc_info(), extra=details, exc_info=exc_info) 25 | 26 | 27 | def error(message, details={}, status_code=400, exc_info=False): 28 | """ 29 | Log an error occurred during execution. 30 | 31 | This method should be used when an exception is properly managed but it 32 | shouldn't occur. 33 | 34 | Args: 35 | message: message identifying the error 36 | details: dict with context details 37 | status_code: http status code associated with the error 38 | exc_info: boolean to indicate if exception information should be added to the logging message 39 | Returns: 40 | 41 | """ 42 | 43 | details['http_status_code'] = status_code 44 | 45 | logger = logging.getLogger(settings.LOGGER_ERROR) 46 | logger.exception(msg=message, extra=details, exc_info=exc_info) 47 | 48 | 49 | def warning(message, details={}, exc_info=False): 50 | """ 51 | Log a warning message during execution. 52 | 53 | This method is recommended for cases when behaviour isn't the appropriate. 54 | 55 | Args: 56 | message: message identifying the error 57 | details: dict with context details 58 | exc_info: boolean to indicate if exception information should be added to the logging message 59 | 60 | Returns: 61 | 62 | """ 63 | 64 | logger = logging.getLogger(settings.LOGGER_WARNING) 65 | logger.warning(msg=message, extra=details, exc_info=exc_info) 66 | 67 | 68 | def info(message, details={}, exc_info=False): 69 | """ 70 | Log a info message during execution. 71 | 72 | This method is recommended for cases when activity tracking is needed. 73 | 74 | Args: 75 | message: message identifying the error 76 | details: dict with context details 77 | exc_info: boolean to indicate if exception information should be added to the logging message 78 | 79 | Returns: 80 | 81 | """ 82 | 83 | logger = logging.getLogger(settings.LOGGER_INFO) 84 | logger.info(msg=message, extra=details, exc_info=exc_info) 85 | -------------------------------------------------------------------------------- /django_rest_logger/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | LOGGING = { 4 | 'disable_existing_loggers': True, 5 | 'root': { 6 | 'level': 'WARNING', 7 | 'handlers': ['rest_logger_handler'], 8 | }, 9 | 'formatters': { 10 | 'verbose': { 11 | 'format': '%(levelname)s %(asctime)s %(module)s ' 12 | '%(process)d %(thread)d %(message)s' 13 | }, 14 | }, 15 | 'handlers': { 16 | 'rest_logger_handler': { 17 | 'level': 'DEBUG', 18 | 'class': 'logging.StreamHandler', 19 | 'formatter': 'verbose' 20 | }, 21 | }, 22 | 'loggers': { 23 | 'django.db.backends': { 24 | 'level': 'ERROR', 25 | 'handlers': ['rest_logger_handler'], 26 | 'propagate': False, 27 | }, 28 | 'django_rest_logger': { 29 | 'level': 'DEBUG', 30 | 'handlers': ['rest_logger_handler'], 31 | 'propagate': False, 32 | }, 33 | }, 34 | } 35 | 36 | LOGGING_SETTINGS = getattr(settings, 'LOGGING', LOGGING) 37 | DEFAULT_LOGGER = getattr(settings, 'DEFAULT_LOGGER', 'django_rest_logger') 38 | 39 | LOGGER_EXCEPTION = DEFAULT_LOGGER 40 | LOGGER_ERROR = DEFAULT_LOGGER 41 | LOGGER_WARNING = DEFAULT_LOGGER 42 | LOGGER_INFO = DEFAULT_LOGGER 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Laying these out as separate requirements files, allows us to 2 | # only included the relevant sets when running tox, and ensures 3 | # we are only ever declaring out dependencies in one place. 4 | 5 | -r requirements/optionals.txt 6 | -r requirements/testing.txt 7 | -r requirements/documentation.txt 8 | -r requirements/codestyle.txt 9 | -r requirements/packaging.txt 10 | -------------------------------------------------------------------------------- /requirements/codestyle.txt: -------------------------------------------------------------------------------- 1 | # PEP8 code linting, which we run on all commits. 2 | flake8==2.4.0 3 | pep8==1.5.7 4 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.12 2 | djangorestframework==3.11.0 3 | -------------------------------------------------------------------------------- /requirements/documentation.txt: -------------------------------------------------------------------------------- 1 | # MkDocs to build our documentation. 2 | mkdocs==0.15.1 3 | -------------------------------------------------------------------------------- /requirements/optionals.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements/packaging.txt: -------------------------------------------------------------------------------- 1 | # Wheel for PyPI installs. 2 | wheel==0.24.0 3 | 4 | # Twine for secured PyPI uploads. 5 | twine==1.4.0 6 | 7 | # django.utils.six was removed 8 | six==1.14.0 -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | # PyTest for running the tests. 2 | pytest==2.6.4 3 | pytest-django==2.8.0 4 | pytest-cov==1.6 5 | 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | import shutil 7 | import sys 8 | 9 | from setuptools import setup 10 | 11 | 12 | def get_version(package): 13 | """ 14 | Return package version as listed in `__version__` in `init.py`. 15 | """ 16 | with open(os.path.join(package, '__init__.py'), 'rb') as init_py: 17 | src = init_py.read().decode('utf-8') 18 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", src).group(1) 19 | 20 | 21 | name = 'django-rest-logger' 22 | version = get_version('django_rest_logger') 23 | package = 'django_rest_logger' 24 | description = 'REST logger for Django' 25 | url = 'https://github.com/Seedstars/django-rest-logger' 26 | author = 'Pedro Gomes' 27 | author_email = 'pedro@seedstars.com' 28 | license = 'MIT' 29 | install_requires = [ 30 | 'Django>=1.11', 31 | ] 32 | 33 | 34 | def read(*paths): 35 | """ 36 | Build a file path from paths and return the contents. 37 | """ 38 | with open(os.path.join(*paths), 'r') as f: 39 | return f.read() 40 | 41 | 42 | def get_packages(package): 43 | """ 44 | Return root package and all sub-packages. 45 | """ 46 | return [ 47 | dirpath 48 | for dirpath, dirnames, filenames in os.walk(package) 49 | if os.path.exists(os.path.join(dirpath, '__init__.py')) 50 | ] 51 | 52 | 53 | def get_package_data(package): 54 | """ 55 | Return all files under the root package, that are not in a 56 | package themselves. 57 | """ 58 | walk = [ 59 | (dirpath.replace(package + os.sep, '', 1), filenames) 60 | for dirpath, dirnames, filenames in os.walk(package) 61 | if not os.path.exists(os.path.join(dirpath, '__init__.py')) 62 | ] 63 | 64 | filepaths = [] 65 | for base, filenames in walk: 66 | filepaths.extend([os.path.join(base, filename) for filename in filenames]) 67 | return {package: filepaths} 68 | 69 | 70 | if sys.argv[-1] == 'publish': 71 | if os.system('pip freeze | grep wheel'): 72 | print('wheel not installed.\nUse `pip install wheel`.\nExiting.') 73 | sys.exit() 74 | if os.system('pip freeze | grep twine'): 75 | print('twine not installed.\nUse `pip install twine`.\nExiting.') 76 | sys.exit() 77 | os.system('python setup.py sdist bdist_wheel') 78 | os.system('twine upload dist/*') 79 | shutil.rmtree('dist') 80 | shutil.rmtree('build') 81 | shutil.rmtree('django_rest_logger.egg-info') 82 | print('You probably want to also tag the version now:') 83 | print(" git tag -a {0} -m 'version {0}'".format(version)) 84 | print(' git push --tags') 85 | sys.exit() 86 | 87 | 88 | setup( 89 | name=name, 90 | version=version, 91 | url=url, 92 | license=license, 93 | description=description, 94 | long_description=read('README.rst'), 95 | author=author, 96 | author_email=author_email, 97 | packages=get_packages(package), 98 | package_data=get_package_data(package), 99 | install_requires=install_requires, 100 | classifiers=[ 101 | 'Development Status :: 5 - Production/Stable', 102 | 'Environment :: Web Environment', 103 | 'Framework :: Django', 104 | 'Intended Audience :: Developers', 105 | 'License :: OSI Approved :: MIT License', 106 | 'Operating System :: OS Independent', 107 | 'Programming Language :: Python', 108 | 'Programming Language :: Python :: 2', 109 | 'Programming Language :: Python :: 2.7', 110 | 'Programming Language :: Python :: 3', 111 | 'Programming Language :: Python :: 3.3', 112 | 'Programming Language :: Python :: 3.4', 113 | 'Programming Language :: Python :: 3.5', 114 | 'Programming Language :: Python :: 3.6', 115 | 'Programming Language :: Python :: 3.7', 116 | 'Programming Language :: Python :: 3.8', 117 | 'Topic :: Internet :: WWW/HTTP', 118 | ], 119 | ) 120 | --------------------------------------------------------------------------------