Вывод имени пользователя на stdout с помощью Django + Gunicorn Application

Прямо сейчас мое приложение Django + Gunicorn печатает только эту информацию:

[03.10.2022 19:43:14] INFO [django.request:middleware] GET /analyse/v2/ping - 200

Если запрос авторизован, я хотел бы показать также пользователя (имя пользователя/email) за кодом статуса, что-то вроде:

[03.10.2022 19:43:14] INFO [django.request:middleware] GET /analyse/v2/ping - 200 - useremail@outlook.com

Если запрос не авторизован, напишите UNAUTHORIZED:

[03.10.2022 19:43:14] INFO [django.request:middleware] GET /analyse/v2/ping - 200 - UNAUTHORIZED

Как я могу достичь этого с помощью комбинации Django и Gunicorn?

Спасибо

Настраиваемое промежуточное ПО - вот как вы можете легко добиться этого. Вы можете сделать что-то вроде следующего.

import logging

from loguru import logger  # optional if you are not using it already.

from django.utils import timezone

logger = logging.getLogger('django.request')


class LogRequest:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        user = (
            request.user.email
            if request.user.is_authenticated
            else "UNAUTHORIZED"
        )

        logger.info(
            f"[{timezone.now().strftime('%d.%m.%Y %H:%M:%S')}] INFO [myapp.custom_logger] {request.method} {request.path} - {response.status_code} - {user}"
        )

        return response

Затем вы можете настроить и активировать ваше новое промежуточное ПО, зарегистрировав его.

MIDDLEWARE = [
    "django.middleware.gzip.GZipMiddleware",
    "django.middleware.security.SecurityMiddleware",
    ...
    "myapp.middleware.LogRequest",
]

В результате получается вывод, как показано ниже.

[25.11.2022 15:57:37] INFO [myapp.custom_logger] GET /analyse/v2/ping - 200 - oppen@heimer.xyz

Непонятно, откуда взялась эта строка журнала. Насколько я могу судить, Django регистрирует только запросы 4xx и 5xx в логгер django.request. Это также не похоже на строку журнала доступа к gunicorn. И если вы инициировали эту строку журнала в своем собственном коде, вы должны быть в состоянии легко добавить пользователя.

Итак, вот несколько общих решений.


(Вариант 1) Для строки журнала доступа gunicorn

У вас нет доступа к объекту запроса Django, и поэтому вы не сможете получить пользователя из gunicorn. Однако вы можете обойти эту проблему, добавив пользователя в заголовки ответа.

yourapp/middleware.py

class UserHeaderMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        user = request.user
        response['X-User'] = user.email if user.is_authenticated() else 'UNAUTHORIZED'
        return response

yourproject/settings.py

MIDDLEWARE = [
    ...,
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...,  # Your custom middleware must be called after authentication
    'yourapp.middleware.UserHeaderMiddleware',
    ...,
]

Затем измените настройку gunicorn access_log_format, чтобы включить этот заголовок. Например: '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%({x-user}o)s"'


(Вариант 2) Для django.request регистратора

Если ваша строка журнала отправляется в django.request логгер, есть вероятность, что он предоставил запрос в дополнительном контексте .

В этом случае вы можете написать пользовательский Formatter для включения пользователя:

yourapp/logging.py

from logging import Formatter

class RequestFormatter(Formatter):

    def format(self, record):
        request = getattr(record, 'request', None)
        if user := getattr(request, 'user', None):
            record.user = user.email if user.is_authenticated() else 'UNAUTHORIZED'
        else:
            record.user = '-'
        return super().format(record)

yourapp/logging.py

LOGGING = {
    ...,
    'formatters': {
        ...,
        "django.request": {
            "()": "yourapp.logging.RequestFormatter",
            "format": "[{asctime}] {levelname} [{name}] {message} - {status_code} - {user}",
            "style": "{",
        },
    },
    'loggers': {
        ...,
        "django.request": {
            "handlers": ...,
            "level": "INFO",
            "formatter": 'django.request',
       }
       ...,
    },
}

(Вариант 3) Скажите Django регистрировать все запросы в django.request

Django регистрирует только запросы 4xx и 5xx в django.request. Смотрите исходный код

Но мы можем изменить это поведение, используя пользовательский обработчик WSGI.

В вашем проекте/wsgi.py у вас должно быть что-то вроде этого:

import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourproject.settings')
application = get_wsgi_application()

Вы можете изменить это, чтобы использовать пользовательский обработчик WSGI:

import os
import django
from django.core.wsgi import WSGIHandler
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourproject.settings')


class CustomWSGIHandler(WSGIHandler):
    def get_response(self, request):
        # Note that this is only a copy of BaseHandler.get_response()
        # without the condition on log_response()
        set_urlconf(settings.ROOT_URLCONF)
        response = self._middleware_chain(request)
        response._resource_closers.append(request.close)
        log_response(
            "%s: %s",
            response.reason_phrase,
            request.path,
            response=response,
            request=request,
        )
        return response

django.setup(set_prefix=False)
application = CustomWSGIHandler()

Затем обратитесь к варианту 2, чтобы включить пользователя в форматер.


(Вариант 4) Создайте промежуточное ПО для добавления новой строки журнала

Если у вас нет доступа к этой строке журнала для ее обновления и нет доступа к запросу в форматере журнала, вам придется добавить новую строку журнала вручную (и, возможно, замолчать первую, чтобы избежать дублирования).

yourapp/middleware.py

import logging

logger = logging.getLogger('django.request')

class LoggingMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        user_email = request.user.email if request.user.is_authenticated() else 'UNAUTHORIZED'
        logger.info(f"{request.method} {request.path} - {request.status_code} - {user_email}")
        return response

yourproject/settings.py

MIDDLEWARE = [
    ...,
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...,  # Your custom middleware must be called after authentication
    'yourapp.middleware.LoggingMiddleware',
    ...,
]
Вернуться на верх