浏览代码

Fixed #25684 -- Made runserver use logging for request/response output.

Thanks andreif for the contributing to the patch.
Flavio Curella 9 年之前
父节点
当前提交
0bc5cd6280
共有 6 个文件被更改,包括 180 次插入48 次删除
  1. 28 34
      django/core/servers/basehttp.py
  2. 50 0
      django/utils/log.py
  3. 8 0
      docs/ref/django-admin.txt
  4. 30 0
      docs/releases/1.10.txt
  5. 23 0
      docs/topics/logging.txt
  6. 41 14
      tests/servers/test_basehttp.py

+ 28 - 34
django/core/servers/basehttp.py

@@ -9,13 +9,13 @@ been reviewed for security issues. DON'T USE IT FOR PRODUCTION USE!
 
 
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import logging
 import socket
 import socket
 import sys
 import sys
 from wsgiref import simple_server
 from wsgiref import simple_server
 
 
 from django.core.exceptions import ImproperlyConfigured
 from django.core.exceptions import ImproperlyConfigured
 from django.core.handlers.wsgi import ISO_8859_1, UTF_8
 from django.core.handlers.wsgi import ISO_8859_1, UTF_8
-from django.core.management.color import color_style
 from django.core.wsgi import get_wsgi_application
 from django.core.wsgi import get_wsgi_application
 from django.utils import six
 from django.utils import six
 from django.utils.encoding import uri_to_iri
 from django.utils.encoding import uri_to_iri
@@ -24,6 +24,8 @@ from django.utils.six.moves import socketserver
 
 
 __all__ = ('WSGIServer', 'WSGIRequestHandler')
 __all__ = ('WSGIServer', 'WSGIRequestHandler')
 
 
+logger = logging.getLogger('django.server')
+
 
 
 def get_internal_wsgi_application():
 def get_internal_wsgi_application():
     """
     """
@@ -80,7 +82,7 @@ class WSGIServer(simple_server.WSGIServer, object):
 
 
     def handle_error(self, request, client_address):
     def handle_error(self, request, client_address):
         if is_broken_pipe_error():
         if is_broken_pipe_error():
-            sys.stderr.write("- Broken pipe from %s\n" % (client_address,))
+            logger.info("- Broken pipe from %s\n", client_address)
         else:
         else:
             super(WSGIServer, self).handle_error(request, client_address)
             super(WSGIServer, self).handle_error(request, client_address)
 
 
@@ -94,47 +96,39 @@ class ServerHandler(simple_server.ServerHandler, object):
 
 
 
 
 class WSGIRequestHandler(simple_server.WSGIRequestHandler, object):
 class WSGIRequestHandler(simple_server.WSGIRequestHandler, object):
-
-    def __init__(self, *args, **kwargs):
-        self.style = color_style()
-        super(WSGIRequestHandler, self).__init__(*args, **kwargs)
-
     def address_string(self):
     def address_string(self):
         # Short-circuit parent method to not call socket.getfqdn
         # Short-circuit parent method to not call socket.getfqdn
         return self.client_address[0]
         return self.client_address[0]
 
 
     def log_message(self, format, *args):
     def log_message(self, format, *args):
-
-        msg = "[%s] " % self.log_date_time_string()
-        try:
-            msg += "%s\n" % (format % args)
-        except UnicodeDecodeError:
-            # e.g. accessing the server via SSL on Python 2
-            msg += "\n"
-
-        # Utilize terminal colors, if available
-        if args[1][0] == '2':
-            # Put 2XX first, since it should be the common case
-            msg = self.style.HTTP_SUCCESS(msg)
-        elif args[1][0] == '1':
-            msg = self.style.HTTP_INFO(msg)
-        elif args[1] == '304':
-            msg = self.style.HTTP_NOT_MODIFIED(msg)
-        elif args[1][0] == '3':
-            msg = self.style.HTTP_REDIRECT(msg)
-        elif args[1] == '404':
-            msg = self.style.HTTP_NOT_FOUND(msg)
-        elif args[1][0] == '4':
+        extra = {
+            'request': self.request,
+            'server_time': self.log_date_time_string(),
+        }
+        if args[1][0] == '4':
             # 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x
             # 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x
             if args[0].startswith(str('\x16\x03')):
             if args[0].startswith(str('\x16\x03')):
-                msg = ("You're accessing the development server over HTTPS, "
-                    "but it only supports HTTP.\n")
-            msg = self.style.HTTP_BAD_REQUEST(msg)
+                extra['status_code'] = 500
+                logger.error(
+                    "You're accessing the development server over HTTPS, but "
+                    "it only supports HTTP.\n", extra=extra,
+                )
+                return
+
+        if args[1].isdigit() and len(args[1]) == 3:
+            status_code = int(args[1])
+            extra['status_code'] = status_code
+
+            if status_code >= 500:
+                level = logger.error
+            elif status_code >= 400:
+                level = logger.warning
+            else:
+                level = logger.info
         else:
         else:
-            # Any 5XX, or any other response
-            msg = self.style.HTTP_SERVER_ERROR(msg)
+            level = logger.info
 
 
-        sys.stderr.write(msg)
+        level(format, *args, extra=extra)
 
 
     def get_environ(self):
     def get_environ(self):
         # Strip all headers with underscores in the name before constructing
         # Strip all headers with underscores in the name before constructing

+ 50 - 0
django/utils/log.py

@@ -9,6 +9,7 @@ from copy import copy
 from django.conf import settings
 from django.conf import settings
 from django.core import mail
 from django.core import mail
 from django.core.mail import get_connection
 from django.core.mail import get_connection
+from django.core.management.color import color_style
 from django.utils.deprecation import RemovedInNextVersionWarning
 from django.utils.deprecation import RemovedInNextVersionWarning
 from django.utils.module_loading import import_string
 from django.utils.module_loading import import_string
 from django.views.debug import ExceptionReporter
 from django.views.debug import ExceptionReporter
@@ -28,12 +29,23 @@ DEFAULT_LOGGING = {
             '()': 'django.utils.log.RequireDebugTrue',
             '()': 'django.utils.log.RequireDebugTrue',
         },
         },
     },
     },
+    'formatters': {
+        'django.server': {
+            '()': 'django.utils.log.ServerFormatter',
+            'format': '[%(server_time)s] %(message)s',
+        }
+    },
     'handlers': {
     'handlers': {
         'console': {
         'console': {
             'level': 'INFO',
             'level': 'INFO',
             'filters': ['require_debug_true'],
             'filters': ['require_debug_true'],
             'class': 'logging.StreamHandler',
             'class': 'logging.StreamHandler',
         },
         },
+        'django.server': {
+            'level': 'INFO',
+            'class': 'logging.StreamHandler',
+            'formatter': 'django.server',
+        },
         'mail_admins': {
         'mail_admins': {
             'level': 'ERROR',
             'level': 'ERROR',
             'filters': ['require_debug_false'],
             'filters': ['require_debug_false'],
@@ -45,6 +57,11 @@ DEFAULT_LOGGING = {
             'handlers': ['console', 'mail_admins'],
             'handlers': ['console', 'mail_admins'],
             'level': 'INFO',
             'level': 'INFO',
         },
         },
+        'django.server': {
+            'handlers': ['django.server'],
+            'level': 'INFO',
+            'propagate': False,
+        },
         'py.warnings': {
         'py.warnings': {
             'handlers': ['console'],
             'handlers': ['console'],
         },
         },
@@ -155,3 +172,36 @@ class RequireDebugFalse(logging.Filter):
 class RequireDebugTrue(logging.Filter):
 class RequireDebugTrue(logging.Filter):
     def filter(self, record):
     def filter(self, record):
         return settings.DEBUG
         return settings.DEBUG
+
+
+class ServerFormatter(logging.Formatter):
+    def __init__(self, *args, **kwargs):
+        self.style = color_style()
+        super(ServerFormatter, self).__init__(*args, **kwargs)
+
+    def format(self, record):
+        args = record.args
+        msg = record.msg
+
+        if len(args) == 0:
+            msg = self.style.HTTP_BAD_REQUEST(msg)
+        else:
+            if args[1][0] == '2':
+                # Put 2XX first, since it should be the common case
+                msg = self.style.HTTP_SUCCESS(msg)
+            elif args[1][0] == '1':
+                msg = self.style.HTTP_INFO(msg)
+            elif args[1] == '304':
+                msg = self.style.HTTP_NOT_MODIFIED(msg)
+            elif args[1][0] == '3':
+                msg = self.style.HTTP_REDIRECT(msg)
+            elif args[1] == '404':
+                msg = self.style.HTTP_NOT_FOUND(msg)
+            elif args[1][0] == '4':
+                msg = self.style.HTTP_BAD_REQUEST(msg)
+            else:
+                # Any 5XX, or any other response
+                msg = self.style.HTTP_SERVER_ERROR(msg)
+
+        record.msg = msg
+        return super(ServerFormatter, self).format(record)

+ 8 - 0
docs/ref/django-admin.txt

@@ -827,6 +827,14 @@ with its own :ref:`runserver<staticfiles-runserver>` command.
 If :djadmin:`migrate` was not previously executed, the table that stores the
 If :djadmin:`migrate` was not previously executed, the table that stores the
 history of migrations is created at first run of ``runserver``.
 history of migrations is created at first run of ``runserver``.
 
 
+Logging of each request and response of the server is sent to the
+:ref:`django-server-logger` logger.
+
+.. versionchanged:: 1.10
+
+    In older versions, log messages were written to ``sys.stderr`` instead of
+    being handled through Python logging.
+
 .. django-admin-option:: --noreload
 .. django-admin-option:: --noreload
 
 
 Use the ``--noreload`` option to disable the use of the auto-reloader. This
 Use the ``--noreload`` option to disable the use of the auto-reloader. This

+ 30 - 0
docs/releases/1.10.txt

@@ -397,6 +397,36 @@ Dropped support for PostgreSQL 9.1
 Upstream support for PostgreSQL 9.1 ends in September 2016. As a consequence,
 Upstream support for PostgreSQL 9.1 ends in September 2016. As a consequence,
 Django 1.10 sets PostgreSQL 9.2 as the minimum version it officially supports.
 Django 1.10 sets PostgreSQL 9.2 as the minimum version it officially supports.
 
 
+``runserver`` output goes through logging
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Request and response handling of the ``runserver`` command is sent to the
+:ref:`django-server-logger` logger instead of to ``sys.stderr``. If you
+disable Django's logging configuration or override it with your own, you'll
+need to add the appropriate logging configuration if you want to see that
+output::
+
+    'formatters': {
+        'django.server': {
+            '()': 'django.utils.log.ServerFormatter',
+            'format': '[%(server_time)s] %(message)s',
+        }
+    },
+    'handlers': {
+        'django.server': {
+            'level': 'INFO',
+            'class': 'logging.StreamHandler',
+            'formatter': 'django.server',
+        },
+    },
+    'loggers': {
+        'django.server': {
+            'handlers': ['django.server'],
+            'level': 'INFO',
+            'propagate': False,
+        }
+    }
+
 Miscellaneous
 Miscellaneous
 ~~~~~~~~~~~~~
 ~~~~~~~~~~~~~
 
 

+ 23 - 0
docs/topics/logging.txt

@@ -484,6 +484,24 @@ Messages to this logger have the following extra context:
 * ``request``: The request object that generated the logging
 * ``request``: The request object that generated the logging
   message.
   message.
 
 
+.. _django-server-logger:
+
+``django.server``
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.10
+
+Log messages related to the handling of requests received by the server invoked
+by the :djadmin:`runserver` command. HTTP 5XX responses are logged as ``ERROR``
+messages, 4XX responses are logged as ``WARNING`` messages, and everything else
+is logged as ``INFO``.
+
+Messages to this logger have the following extra context:
+
+* ``status_code``: The HTTP response code associated with the request.
+
+* ``request``: The request object that generated the logging message.
+
 .. _django-template-logger:
 .. _django-template-logger:
 
 
 ``django.template``
 ``django.template``
@@ -729,6 +747,11 @@ When :setting:`DEBUG` is ``False``:
 * The ``django`` logger send messages with ``ERROR`` or ``CRITICAL`` level to
 * The ``django`` logger send messages with ``ERROR`` or ``CRITICAL`` level to
   :class:`AdminEmailHandler`.
   :class:`AdminEmailHandler`.
 
 
+Independent of the value of :setting:`DEBUG`:
+
+* The :ref:`django-server-logger` logger sends all messages at the ``INFO``
+  level or higher to the console.
+
 .. versionchanged:: 1.9
 .. versionchanged:: 1.9
 
 
     Django's default logging configuration changed. See :ref:`the release notes
     Django's default logging configuration changed. See :ref:`the release notes

+ 41 - 14
tests/servers/test_basehttp.py

@@ -1,10 +1,11 @@
+import logging
 from io import BytesIO
 from io import BytesIO
 
 
 from django.core.handlers.wsgi import WSGIRequest
 from django.core.handlers.wsgi import WSGIRequest
 from django.core.servers.basehttp import WSGIRequestHandler
 from django.core.servers.basehttp import WSGIRequestHandler
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
 from django.test.client import RequestFactory
 from django.test.client import RequestFactory
-from django.test.utils import captured_stderr
+from django.test.utils import captured_stderr, patch_logger
 
 
 
 
 class Stub(object):
 class Stub(object):
@@ -15,13 +16,39 @@ class Stub(object):
 class WSGIRequestHandlerTestCase(SimpleTestCase):
 class WSGIRequestHandlerTestCase(SimpleTestCase):
 
 
     def test_log_message(self):
     def test_log_message(self):
-        request = WSGIRequest(RequestFactory().get('/').environ)
-        request.makefile = lambda *args, **kwargs: BytesIO()
-        handler = WSGIRequestHandler(request, '192.168.0.2', None)
-
-        with captured_stderr() as stderr:
-            handler.log_message('GET %s %s', 'A', 'B')
-        self.assertIn('] GET A B', stderr.getvalue())
+        # Silence the django.server logger by replacing its StreamHandler with
+        # NullHandler.
+        logger = logging.getLogger('django.server')
+        original_handlers = logger.handlers
+        logger.handlers = [logging.NullHandler()]
+        try:
+            request = WSGIRequest(RequestFactory().get('/').environ)
+            request.makefile = lambda *args, **kwargs: BytesIO()
+            handler = WSGIRequestHandler(request, '192.168.0.2', None)
+            level_status_codes = {
+                'info': [200, 301, 304],
+                'warning': [400, 403, 404],
+                'error': [500, 503],
+            }
+
+            def _log_level_code(level, status_code):
+                with patch_logger('django.server', level) as messages:
+                    handler.log_message('GET %s %s', 'A', str(status_code))
+                return messages
+
+            for level, status_codes in level_status_codes.items():
+                for status_code in status_codes:
+                    # The correct level gets the message.
+                    messages = _log_level_code(level, status_code)
+                    self.assertIn('GET A %d' % status_code, messages[0])
+
+                    # Incorrect levels shouldn't have any messages.
+                    for wrong_level in level_status_codes.keys():
+                        if wrong_level != level:
+                            messages = _log_level_code(wrong_level, status_code)
+                            self.assertEqual(len(messages), 0)
+        finally:
+            logger.handlers = original_handlers
 
 
     def test_https(self):
     def test_https(self):
         request = WSGIRequest(RequestFactory().get('/').environ)
         request = WSGIRequest(RequestFactory().get('/').environ)
@@ -29,13 +56,13 @@ class WSGIRequestHandlerTestCase(SimpleTestCase):
 
 
         handler = WSGIRequestHandler(request, '192.168.0.2', None)
         handler = WSGIRequestHandler(request, '192.168.0.2', None)
 
 
-        with captured_stderr() as stderr:
+        with patch_logger('django.server', 'error') as messages:
             handler.log_message("GET %s %s", str('\x16\x03'), "4")
             handler.log_message("GET %s %s", str('\x16\x03'), "4")
-            self.assertIn(
-                "You're accessing the development server over HTTPS, "
-                "but it only supports HTTP.",
-                stderr.getvalue()
-            )
+        self.assertIn(
+            "You're accessing the development server over HTTPS, "
+            "but it only supports HTTP.",
+            messages[0]
+        )
 
 
     def test_strips_underscore_headers(self):
     def test_strips_underscore_headers(self):
         """WSGIRequestHandler ignores headers containing underscores.
         """WSGIRequestHandler ignores headers containing underscores.