Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement async support #209

Merged
merged 6 commits into from Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.rst
Expand Up @@ -339,6 +339,40 @@ Json file (\ ``logs/json.log``\ )
Upgrade Guide
=============

.. _upgrade_5.0:

Upgrading to 5.0+
^^^^^^^^^^^^^^^^^

Changes you may need to do
~~~~~~~~~~~~~~~~~~~~~~~~~~

1. Make sure you use ``django_structlog.middlewares.RequestMiddleware``
-----------------------------------------------------------------------

If you used any of the experimental async or sync middlewares, you do not need to anymore.
Make sure you use ``django_structlog.middlewares.RequestMiddleware`` instead of any of the other request middlewares commented below:

.. code-block:: python

MIDDLEWARE += [
# "django_structlog.middlewares.request_middleware_router", # <- remove
# "django_structlog.middlewares.requests.SyncRequestMiddleware", # <- remove
# "django_structlog.middlewares.requests.AsyncRequestMiddleware", # <- remove
"django_structlog.middlewares.RequestMiddleware", # <- make sure you use this one
"django_structlog.middlewares.CeleryMiddleware",
]

They will be removed in another major version.

2. ``django_structlog.signals.bind_extra_request_failed_metadata`` was removed
------------------------------------------------------------------------------

The signal ``bind_extra_request_failed_metadata`` was removed since it was never called.

Remove your custom signal.


.. _upgrade_4.0:

Upgrading to 4.0+
Expand Down
14 changes: 1 addition & 13 deletions config/settings/local.py
@@ -1,7 +1,7 @@
import structlog

from .base import * # noqa: F403
from .base import env
from .base import env, MIDDLEWARE

# GENERAL
# ------------------------------------------------------------------------------
Expand Down Expand Up @@ -41,18 +41,6 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025

# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
"SHOW_TEMPLATE_CONTEXT": True,
}
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
if env("USE_DOCKER") == "yes":
import socket
Expand Down
6 changes: 0 additions & 6 deletions config/urls.py
Expand Up @@ -62,9 +62,3 @@ def uncaught_exception_view(request):
re_path(r"^500/", default_views.server_error),
re_path(r"^uncaught_exception/", uncaught_exception_view),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar

urlpatterns = [
re_path(r"^__debug__/", include(debug_toolbar.urls))
] + urlpatterns
2 changes: 1 addition & 1 deletion django_structlog/__init__.py
Expand Up @@ -4,6 +4,6 @@

name = "django_structlog"

VERSION = (4, 1, 1)
VERSION = (5, 0, 0)

__version__ = ".".join(str(v) for v in VERSION)
11 changes: 11 additions & 0 deletions django_structlog/celery/middlewares.py
@@ -1,7 +1,11 @@
from asgiref.sync import iscoroutinefunction, markcoroutinefunction

from ..celery.receivers import receiver_before_task_publish, receiver_after_task_publish


class CeleryMiddleware:
sync_capable = True
async_capable = True
"""
``CeleryMiddleware`` initializes ``celery`` signals to pass ``django``'s request information to ``celery`` worker's logger.

Expand All @@ -19,6 +23,13 @@ def __init__(self, get_response=None):

before_task_publish.connect(receiver_before_task_publish)
after_task_publish.connect(receiver_after_task_publish)
if iscoroutinefunction(self.get_response):
markcoroutinefunction(self)

def __call__(self, request):
if iscoroutinefunction(self):
return self.__acall__(request)
return self.get_response(request)

async def __acall__(self, request):
return await self.get_response(request)
132 changes: 60 additions & 72 deletions django_structlog/middlewares/request.py
@@ -1,11 +1,10 @@
import asyncio
import uuid

import structlog
from django.core.exceptions import PermissionDenied
from django.http import Http404
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.utils.decorators import sync_and_async_middleware
from asgiref import sync
from django.utils.deprecation import warn_about_renamed_method

from .. import signals

Expand All @@ -22,22 +21,20 @@ def get_request_header(request, header_key, meta_key):
class BaseRequestMiddleWare:
def __init__(self, get_response):
self.get_response = get_response
self._raised_exception = False

def handle_response(self, request, response):
if not self._raised_exception:
self.bind_user_id(request)
signals.bind_extra_request_finished_metadata.send(
sender=self.__class__,
request=request,
logger=logger,
response=response,
)
logger.info(
"request_finished",
code=response.status_code,
request=self.format_request(request),
)
self.bind_user_id(request)
signals.bind_extra_request_finished_metadata.send(
sender=self.__class__,
request=request,
logger=logger,
response=response,
)
logger.info(
"request_finished",
code=response.status_code,
request=self.format_request(request),
)
structlog.contextvars.clear_contextvars()

def prepare(self, request):
Expand All @@ -63,34 +60,11 @@ def prepare(self, request):
request=self.format_request(request),
user_agent=request.META.get("HTTP_USER_AGENT"),
)
self._raised_exception = False

@staticmethod
def format_request(request):
return "%s %s" % (request.method, request.get_full_path())

def process_exception(self, request, exception):
if isinstance(exception, (Http404, PermissionDenied)):
# We don't log an exception here, and we don't set that we handled
# an error as we want the standard `request_finished` log message
# to be emitted.
return

self._raised_exception = True

self.bind_user_id(request)
signals.bind_extra_request_failed_metadata.send(
sender=self.__class__,
request=request,
logger=logger,
exception=exception,
)
logger.exception(
"request_failed",
code=500,
request=self.format_request(request),
)

@staticmethod
def bind_user_id(request):
if hasattr(request, "user") and request.user is not None:
Expand All @@ -102,51 +76,65 @@ def bind_user_id(request):
structlog.contextvars.bind_contextvars(user_id=user_id)


class SyncRequestMiddleware(BaseRequestMiddleWare):
class RequestMiddleware(BaseRequestMiddleWare):
"""``RequestMiddleware`` adds request metadata to ``structlog``'s logger context automatically.

>>> MIDDLEWARE = [
... # ...
... 'django_structlog.middlewares.RequestMiddleware',
... ]

"""

sync_capable = True
async_capable = False
async_capable = True

def __init__(self, get_response):
super().__init__(get_response)
if iscoroutinefunction(self.get_response):
markcoroutinefunction(self)

def __call__(self, request):
if iscoroutinefunction(self):
return self.__acall__(request)
self.prepare(request)
response = self.get_response(request)
self.handle_response(request, response)
return response


class AsyncRequestMiddleware(BaseRequestMiddleWare):
sync_capable = False
async_capable = True

async def __call__(self, request):
async def __acall__(self, request):
await sync.sync_to_async(self.prepare)(request)
response = await self.get_response(request)
await sync.sync_to_async(self.handle_response)(request, response)
return response


class RequestMiddleware(SyncRequestMiddleware):
"""``RequestMiddleware`` adds request metadata to ``structlog``'s logger context automatically.

>>> MIDDLEWARE = [
... # ...
... 'django_structlog.middlewares.RequestMiddleware',
... ]

"""


@warn_about_renamed_method(
class_name="django-structlog.middlewares",
old_method_name="SyncRequestMiddleware",
new_method_name="RequestMiddleware",
deprecation_warning=DeprecationWarning,
)
class SyncRequestMiddleware(RequestMiddleware):
pass


@warn_about_renamed_method(
class_name="django-structlog.middlewares",
old_method_name="AsyncRequestMiddleware",
new_method_name="RequestMiddleware",
deprecation_warning=DeprecationWarning,
)
class AsyncRequestMiddleware(RequestMiddleware):
pass


@warn_about_renamed_method(
class_name="django-structlog.middlewares",
old_method_name="request_middleware_router",
new_method_name="RequestMiddleware",
deprecation_warning=DeprecationWarning,
)
@sync_and_async_middleware
def request_middleware_router(get_response):
"""``request_middleware_router`` select automatically between async or sync middleware.

Use as a replacement for `django_structlog.middlewares.RequestMiddleware`

>>> MIDDLEWARE = [
... # ...
... 'django_structlog.middlewares.request_middleware_router',
... ]

"""
if asyncio.iscoroutinefunction(get_response):
return AsyncRequestMiddleware(get_response)
return SyncRequestMiddleware(get_response)
return RequestMiddleware(get_response) # pragma: no cover
16 changes: 0 additions & 16 deletions django_structlog/signals.py
Expand Up @@ -31,19 +31,3 @@
... structlog.contextvars.bind_contextvars(user_email=getattr(request.user, 'email', ''))

"""

bind_extra_request_failed_metadata = django.dispatch.Signal()
""" Signal to add extra ``structlog`` bindings from ``django``'s failed request and exception.

:param logger: the logger to bind more metadata or override existing bound metadata
:param exception: the exception resulting of the request

>>> from django.dispatch import receiver
>>> from django_structlog import signals
>>> import structlog
>>>
>>> @receiver(signals.bind_extra_request_failed_metadata)
... def bind_user_email(request, logger, exception, **kwargs):
... structlog.contextvars.bind_contextvars(user_email=getattr(request.user, 'email', ''))

"""
2 changes: 1 addition & 1 deletion docs/api_documentation.rst
Expand Up @@ -10,7 +10,7 @@ django_structlog
:show-inheritance:

.. automodule:: django_structlog.middlewares
:members: RequestMiddleware, request_middleware_router
:members: RequestMiddleware
:undoc-members:
:show-inheritance:

Expand Down
17 changes: 17 additions & 0 deletions docs/changelog.rst
@@ -1,6 +1,23 @@
Change Log
==========

5.0.0 (March 23th, 2023)
------------------------

See: :ref:`upgrade_5.0`

*Changes:*
- ``RequestMiddleware`` and ``CeleryMiddleware`` now properly support async views

*Removed:*
- ``django_structlog.signals.bind_extra_request_failed_metadata``

*Deprecates:*
- :class:`django_structlog.middlewares.request_middleware_router`
- :class:`django_structlog.middlewares.requests.AsyncRequestMiddleware`
- :class:`django_structlog.middlewares.requests.SyncRequestMiddleware`


4.1.1 (February 7th, 2023)
--------------------------

Expand Down
6 changes: 0 additions & 6 deletions docs/events.rst
Expand Up @@ -14,8 +14,6 @@ Request Events
+------------------+---------+------------------------------+
| request_finished | INFO | request completed normally |
+------------------+---------+------------------------------+
| request_failed | ERROR | unhandled exception occurred |
+------------------+---------+------------------------------+

Request Bound Metadata
^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -60,10 +58,6 @@ These metadata appear once along with their associated event
+------------------+------------------+--------------------------------------------------------------+
| request_finished | code | request's status code |
+------------------+------------------+--------------------------------------------------------------+
| request_failed | exception | exception traceback (requires format_exc_info_) |
+------------------+------------------+--------------------------------------------------------------+

.. _format_exc_info: https://www.structlog.org/en/stable/api.html#structlog.processors.format_exc_info

CeleryMiddleware
----------------
Expand Down
1 change: 0 additions & 1 deletion requirements/local-base.txt
Expand Up @@ -42,7 +42,6 @@ pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
# ------------------------------------------------------------------------------
factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy

django-debug-toolbar==3.8.1 # https://github.com/jazzband/django-debug-toolbar
django-extensions==3.2.1 # https://github.com/django-extensions/django-extensions
django-coverage-plugin==3.0.0 # https://github.com/nedbat/django_coverage_plugin
pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django
Expand Down