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

Thanks andreif for the contributing to the patch.
This commit is contained in:
Flavio Curella 2015-11-06 10:19:41 -06:00 committed by Tim Graham
parent 3792e291e6
commit 0bc5cd6280
6 changed files with 180 additions and 48 deletions

View File

@ -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):
extra = {
msg = "[%s] " % self.log_date_time_string() 'request': self.request,
try: 'server_time': self.log_date_time_string(),
msg += "%s\n" % (format % args) }
except UnicodeDecodeError: if args[1][0] == '4':
# 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':
# 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, " extra['status_code'] = 500
"but it only supports HTTP.\n") logger.error(
msg = self.style.HTTP_BAD_REQUEST(msg) "You're accessing the development server over HTTPS, but "
else: "it only supports HTTP.\n", extra=extra,
# Any 5XX, or any other response )
msg = self.style.HTTP_SERVER_ERROR(msg) return
sys.stderr.write(msg) 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:
level = logger.info
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

View File

@ -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)

View File

@ -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

View File

@ -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
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -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

View File

@ -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):
# 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 = WSGIRequest(RequestFactory().get('/').environ)
request.makefile = lambda *args, **kwargs: BytesIO() request.makefile = lambda *args, **kwargs: BytesIO()
handler = WSGIRequestHandler(request, '192.168.0.2', None) handler = WSGIRequestHandler(request, '192.168.0.2', None)
level_status_codes = {
'info': [200, 301, 304],
'warning': [400, 403, 404],
'error': [500, 503],
}
with captured_stderr() as stderr: def _log_level_code(level, status_code):
handler.log_message('GET %s %s', 'A', 'B') with patch_logger('django.server', level) as messages:
self.assertIn('] GET A B', stderr.getvalue()) 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,12 +56,12 @@ 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( self.assertIn(
"You're accessing the development server over HTTPS, " "You're accessing the development server over HTTPS, "
"but it only supports HTTP.", "but it only supports HTTP.",
stderr.getvalue() messages[0]
) )
def test_strips_underscore_headers(self): def test_strips_underscore_headers(self):