diff --git a/django_structlog/app_settings.py b/django_structlog/app_settings.py index 72e0dd3b..8c4aa715 100644 --- a/django_structlog/app_settings.py +++ b/django_structlog/app_settings.py @@ -11,6 +11,50 @@ class AppSettings: def CELERY_ENABLED(self) -> bool: return getattr(settings, self.PREFIX + "CELERY_ENABLED", False) + @property + def CELERY_DEFAULT_LOG_LEVEL(self) -> int: + return getattr(settings, self.PREFIX + "CELERY_DEFAULT_LOG_LEVEL", logging.INFO) + + @property + def CELERY_TASK_START_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "CELERY_TASK_START_LOG_LEVEL", + self.CELERY_DEFAULT_LOG_LEVEL, + ) + + @property + def CELERY_TASK_SUCCESS_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "CELERY_TASK_SUCCESS_LOG_LEVEL", + self.CELERY_DEFAULT_LOG_LEVEL, + ) + + @property + def CELERY_TASK_NOTICE_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "CELERY_TASK_RETRY_LOG_LEVEL", + logging.WARNING, + ) + + @property + def CELERY_TASK_FAILURE_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "CELERY_TASK_FAILURE_LOG_LEVEL", + logging.INFO, + ) + + @property + def CELERY_TASK_ERROR_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "CELERY_TASK_ERROR_LOG_LEVEL", + logging.ERROR, + ) + @property def IP_LOGGING_ENABLED(self) -> bool: return getattr(settings, self.PREFIX + "IP_LOGGING_ENABLED", True) @@ -21,6 +65,26 @@ def REQUEST_CANCELLED_LOG_LEVEL(self) -> int: settings, self.PREFIX + "REQUEST_CANCELLED_LOG_LEVEL", logging.WARNING ) + @property + def STATUS_DEFAULT_LOG_LEVEL(self) -> int: + return getattr(settings, self.PREFIX + "STATUS_DEFAULT_LOG_LEVEL", logging.INFO) + + @property + def STATUS_START_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "STATUS_START_LOG_LEVEL", + self.STATUS_DEFAULT_LOG_LEVEL, + ) + + @property + def STATUS_2XX_LOG_LEVEL(self) -> int: + return getattr( + settings, + self.PREFIX + "STATUS_2XX_LOG_LEVEL", + self.STATUS_DEFAULT_LOG_LEVEL, + ) + @property def STATUS_4XX_LOG_LEVEL(self) -> int: return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING) diff --git a/django_structlog/celery/receivers.py b/django_structlog/celery/receivers.py index 9d07f321..0556adf7 100644 --- a/django_structlog/celery/receivers.py +++ b/django_structlog/celery/receivers.py @@ -15,6 +15,7 @@ task_unknown, ) +from ..app_settings import app_settings from . import signals if TYPE_CHECKING: # pragma: no cover @@ -68,7 +69,8 @@ def receiver_after_task_publish( properties["priority"] = self._priority self._priority = None - logger.info( + logger.log( + app_settings.CELERY_TASK_START_LOG_LEVEL, "task_enqueued", child_task_id=( headers.get("id") @@ -96,7 +98,9 @@ def receiver_task_prerun( ) # Record the start time so we can log the task duration later. task.request._django_structlog_started_at = time.monotonic_ns() - logger.info("task_started", task=task.name) + logger.log( + app_settings.CELERY_TASK_START_LOG_LEVEL, "task_started", task=task.name + ) def receiver_task_retry( self, @@ -105,7 +109,9 @@ def receiver_task_retry( einfo: Optional[Any] = None, **kwargs: Any, ) -> None: - logger.warning("task_retrying", reason=reason) + logger.log( + app_settings.CELERY_TASK_NOTICE_LOG_LEVEL, "task_retrying", reason=reason + ) def receiver_task_success( self, result: Optional[str] = None, sender: Optional[Any] = None, **kwargs: Any @@ -116,7 +122,9 @@ def receiver_task_success( log_vars: dict[str, Any] = {} self.add_duration_ms(sender, log_vars) - logger.info("task_succeeded", **log_vars) + logger.log( + app_settings.CELERY_TASK_SUCCESS_LOG_LEVEL, "task_succeeded", **log_vars + ) def receiver_task_failure( self, @@ -132,7 +140,8 @@ def receiver_task_failure( self.add_duration_ms(sender, log_vars) throws = getattr(sender, "throws", ()) if isinstance(exception, throws): - logger.info( + logger.log( + app_settings.CELERY_TASK_FAILURE_LOG_LEVEL, "task_failed", error=str(exception), **log_vars, @@ -167,7 +176,8 @@ def receiver_task_revoked( metadata["task_id"] = request.id metadata["task"] = request.task - logger.warning( + logger.log( + app_settings.CELERY_TASK_NOTICE_LOG_LEVEL, "task_revoked", terminated=terminated, signum=signum.value if signum is not None else None, @@ -184,7 +194,8 @@ def receiver_task_unknown( id: Optional[str] = None, **kwargs: Any, ) -> None: - logger.error( + logger.log( + app_settings.CELERY_TASK_ERROR_LOG_LEVEL, "task_not_found", task=name, task_id=id, diff --git a/django_structlog/middlewares/request.py b/django_structlog/middlewares/request.py index 1258bbde..990bc78b 100644 --- a/django_structlog/middlewares/request.py +++ b/django_structlog/middlewares/request.py @@ -1,5 +1,4 @@ import asyncio -import logging import sys import uuid from typing import ( @@ -57,7 +56,7 @@ def sync_streaming_content_wrapper( streaming_content: Iterator[bytes], context: Any ) -> Generator[bytes, None, None]: with structlog.contextvars.bound_contextvars(**context): - logger.info("streaming_started") + logger.log(app_settings.STATUS_START_LOG_LEVEL, "streaming_started") try: for chunk in streaming_content: yield chunk @@ -65,25 +64,25 @@ def sync_streaming_content_wrapper( logger.exception("streaming_failed") raise else: - logger.info("streaming_finished") + logger.log(app_settings.STATUS_2XX_LOG_LEVEL, "streaming_finished") async def async_streaming_content_wrapper( streaming_content: AsyncIterator[bytes], context: Any ) -> AsyncGenerator[bytes, Any]: with structlog.contextvars.bound_contextvars(**context): - logger.info("streaming_started") + logger.log(app_settings.STATUS_START_LOG_LEVEL, "streaming_started") try: async for chunk in streaming_content: yield chunk except asyncio.CancelledError: - logger.warning("streaming_cancelled") + logger.log(app_settings.REQUEST_CANCELLED_LOG_LEVEL, "streaming_cancelled") raise except Exception: logger.exception("streaming_failed") raise else: - logger.info("streaming_finished") + logger.log(app_settings.STATUS_2XX_LOG_LEVEL, "streaming_finished") class RequestMiddleware: @@ -105,6 +104,7 @@ def __init__( ["HttpRequest"], Union["HttpResponse", Awaitable["HttpResponse"]] ], ) -> None: + self.get_response = get_response if iscoroutinefunction(self.get_response): markcoroutinefunction(self) @@ -130,6 +130,18 @@ async def __acall__(self, request: "HttpRequest") -> "HttpResponse": await sync.sync_to_async(self.handle_response)(request, response) return response + def _log_level_for_status_code(self, status_code: int) -> int: + match status_code // 100: + case 2: + level = app_settings.STATUS_2XX_LOG_LEVEL + case 4: + level = app_settings.STATUS_4XX_LOG_LEVEL + case 5: + level = app_settings.STATUS_5XX_LOG_LEVEL + case _: + level = app_settings.STATUS_DEFAULT_LOG_LEVEL + return level + def handle_response(self, request: "HttpRequest", response: "HttpResponse") -> None: if not hasattr(request, "_raised_exception"): self.bind_user_id(request) @@ -146,12 +158,7 @@ def handle_response(self, request: "HttpRequest", response: "HttpResponse") -> N response=response, log_kwargs=log_kwargs, ) - if response.status_code >= 500: - level = app_settings.STATUS_5XX_LOG_LEVEL - elif response.status_code >= 400: - level = app_settings.STATUS_4XX_LOG_LEVEL - else: - level = logging.INFO + level = self._log_level_for_status_code(response.status_code) logger.log( level, "request_finished", @@ -200,7 +207,8 @@ def prepare(self, request: "HttpRequest") -> None: signals.bind_extra_request_metadata.send( sender=self.__class__, request=request, logger=logger, log_kwargs=log_kwargs ) - logger.info("request_started", **log_kwargs) + level = app_settings.STATUS_START_LOG_LEVEL + logger.log(level, "request_started", **log_kwargs) @classmethod def bind_ip(cls, request: "HttpRequest") -> None: diff --git a/docs/changelog.rst b/docs/changelog.rst index 17c1eb43..0123a1e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Change Log ========== +Unreleased +---------- + +*New:* + - Add settings to configure the logging levels for the request middleware and celery task events. See `#1022 `_. + 10.0.0 (October 22, 2025) ------------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index 22c96d0b..dc4c1bf1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -18,20 +18,38 @@ Example: Settings -------- -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| Key | Type | Default | Description | -+==========================================+=========+=================+===============================================================================+ -| DJANGO_STRUCTLOG_CELERY_ENABLED | boolean | False | See :ref:`celery_integration` | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_IP_LOGGING_ENABLED | boolean | True | automatically bind user ip using `django-ipware` | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL | int | logging.WARNING | Log level of 4XX status codes | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_STATUS_5XX_LOG_LEVEL | int | logging.ERROR | Log level of 5XX status codes | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_REQUEST_CANCELLED_LOG_LEVEL | int | logging.WARNING | Log level of request_cancelled messages | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False | See :ref:`commands` | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ -| DJANGO_STRUCTLOG_USER_ID_FIELD | string | ``"pk"`` | Change field used to identify user in logs, ``None`` to disable user binding | -+------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| Key | Type | Default | Description | ++==================================================+=========+=================+==============================================================================+ +| DJANGO_STRUCTLOG_CELERY_ENABLED | boolean | False | See :ref:`celery_integration` | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_DEFAULT_LOG_LEVEL | int | logging.INFO | The default log level for celery task events | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_TASK_START_LOG_LEVEL | int | logging.INFO | Log level for task_enqueued and task_started events | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_TASK_SUCCESS_LOG_LEVEL | int | logging.INFO | Log level for task_succeeded events | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_TASK_NOTICE_LOG_LEVEL | int | logging.WARNING | Log level for task_retrying and task_revoked events | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_TASK_FAILURE_LOG_LEVEL | int | logging.INFO | Log level for task_failed | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_CELERY_TASK_ERROR_LOG_LEVEL | int | logging.ERROR | Log level for true errors using Celery | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_IP_LOGGING_ENABLED | boolean | True | automatically bind user ip using `django-ipware` | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_DEFAULT_LOG_LEVEL | int | logging.INFO | The default log level for non-error statuses | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_START_LOG_LEVEL | int | logging.INFO | The level at which request starts are logged | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_STATUS_2XX_LOG_LEVEL | int | logging.INFO | The level of 2XX status codes | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL | int | logging.WARNING | Log level of 4XX status codes | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_STATUS_5XX_LOG_LEVEL | int | logging.ERROR | Log level of 5XX status codes | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_REQUEST_CANCELLED_LOG_LEVEL | int | logging.WARNING | Log level of request_cancelled messages | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False | See :ref:`commands` | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ +| DJANGO_STRUCTLOG_USER_ID_FIELD | string | ``"pk"`` | Change field used to identify user in logs, ``None`` to disable user binding | ++--------------------------------------------------+---------+-----------------+------------------------------------------------------------------------------+ diff --git a/test_app/tests/middlewares/test_request.py b/test_app/tests/middlewares/test_request.py index 251d3352..927bc45c 100644 --- a/test_app/tests/middlewares/test_request.py +++ b/test_app/tests/middlewares/test_request.py @@ -1369,3 +1369,35 @@ async def streaming_content() -> AsyncGenerator[Any, None]: self.assertEqual("streaming_cancelled", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) + + +class TestLogLevelMappings(TestCase): + def test_log_level_for_status_code(self) -> None: + middleware = RequestMiddleware(lambda r: HttpResponse()) + from django_structlog.app_settings import app_settings + + # 2xx + self.assertEqual( + middleware._log_level_for_status_code(200), + app_settings.STATUS_2XX_LOG_LEVEL, + ) + # 4xx + self.assertEqual( + middleware._log_level_for_status_code(404), + app_settings.STATUS_4XX_LOG_LEVEL, + ) + # 5xx + self.assertEqual( + middleware._log_level_for_status_code(500), + app_settings.STATUS_5XX_LOG_LEVEL, + ) + # 3xx (default branch) + self.assertEqual( + middleware._log_level_for_status_code(301), + app_settings.STATUS_DEFAULT_LOG_LEVEL, + ) + # 1xx (default branch) + self.assertEqual( + middleware._log_level_for_status_code(100), + app_settings.STATUS_DEFAULT_LOG_LEVEL, + )