Fixed #25684 -- Made runserver use logging for request/response output.
Thanks andreif for the contributing to the patch.
This commit is contained in:
parent
3792e291e6
commit
0bc5cd6280
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue