1import logging
2import logging.config # needed when logging_config doesn't start with logging.config
3from copy import copy
4
5from django.conf import settings
6from django.core import mail
7from django.core.mail import get_connection
8from django.core.management.color import color_style
9from django.utils.module_loading import import_string
10
11request_logger = logging.getLogger("django.request")
12
13# Default logging for Django. This sends an email to the site admins on every
14# HTTP 500 error. Depending on DEBUG, all other log records are either sent to
15# the console (DEBUG=True) or discarded (DEBUG=False) by means of the
16# require_debug_true filter. This configuration is quoted in
17# docs/ref/logging.txt; please amend it there if edited here.
18DEFAULT_LOGGING = {
19 "version": 1,
20 "disable_existing_loggers": False,
21 "filters": {
22 "require_debug_false": {
23 "()": "django.utils.log.RequireDebugFalse",
24 },
25 "require_debug_true": {
26 "()": "django.utils.log.RequireDebugTrue",
27 },
28 },
29 "formatters": {
30 "django.server": {
31 "()": "django.utils.log.ServerFormatter",
32 "format": "[{server_time}] {message}",
33 "style": "{",
34 }
35 },
36 "handlers": {
37 "console": {
38 "level": "INFO",
39 "filters": ["require_debug_true"],
40 "class": "logging.StreamHandler",
41 },
42 "django.server": {
43 "level": "INFO",
44 "class": "logging.StreamHandler",
45 "formatter": "django.server",
46 },
47 "mail_admins": {
48 "level": "ERROR",
49 "filters": ["require_debug_false"],
50 "class": "django.utils.log.AdminEmailHandler",
51 },
52 },
53 "loggers": {
54 "django": {
55 "handlers": ["console", "mail_admins"],
56 "level": "INFO",
57 },
58 "django.server": {
59 "handlers": ["django.server"],
60 "level": "INFO",
61 "propagate": False,
62 },
63 },
64}
65
66
67def configure_logging(logging_config, logging_settings):
68 if logging_config:
69 # First find the logging configuration function ...
70 logging_config_func = import_string(logging_config)
71
72 logging.config.dictConfig(DEFAULT_LOGGING)
73
74 # ... then invoke it with the logging settings
75 if logging_settings:
76 logging_config_func(logging_settings)
77
78
79class AdminEmailHandler(logging.Handler):
80 """An exception log handler that emails log entries to site admins.
81
82 If the request is passed as the first argument to the log record,
83 request data will be provided in the email report.
84 """
85
86 def __init__(self, include_html=False, email_backend=None, reporter_class=None):
87 super().__init__()
88 self.include_html = include_html
89 self.email_backend = email_backend
90 self.reporter_class = import_string(
91 reporter_class or settings.DEFAULT_EXCEPTION_REPORTER
92 )
93
94 def emit(self, record):
95 # Early return when no email will be sent.
96 if (
97 not settings.ADMINS
98 # Method not overridden.
99 and self.send_mail.__func__ is AdminEmailHandler.send_mail
100 ):
101 return
102 try:
103 request = record.request
104 subject = "%s (%s IP): %s" % (
105 record.levelname,
106 (
107 "internal"
108 if request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS
109 else "EXTERNAL"
110 ),
111 record.getMessage(),
112 )
113 except Exception:
114 subject = "%s: %s" % (record.levelname, record.getMessage())
115 request = None
116 subject = self.format_subject(subject)
117
118 # Since we add a nicely formatted traceback on our own, create a copy
119 # of the log record without the exception data.
120 no_exc_record = copy(record)
121 no_exc_record.exc_info = None
122 no_exc_record.exc_text = None
123
124 if record.exc_info:
125 exc_info = record.exc_info
126 else:
127 exc_info = (None, record.getMessage(), None)
128
129 reporter = self.reporter_class(request, is_email=True, *exc_info)
130 message = "%s\n\n%s" % (
131 self.format(no_exc_record),
132 reporter.get_traceback_text(),
133 )
134 html_message = reporter.get_traceback_html() if self.include_html else None
135 self.send_mail(subject, message, fail_silently=True, html_message=html_message)
136
137 def send_mail(self, subject, message, *args, **kwargs):
138 mail.mail_admins(
139 subject, message, *args, connection=self.connection(), **kwargs
140 )
141
142 def connection(self):
143 return get_connection(backend=self.email_backend, fail_silently=True)
144
145 def format_subject(self, subject):
146 """
147 Escape CR and LF characters.
148 """
149 return subject.replace("\n", "\\n").replace("\r", "\\r")
150
151
152class CallbackFilter(logging.Filter):
153 """
154 A logging filter that checks the return value of a given callable (which
155 takes the record-to-be-logged as its only parameter) to decide whether to
156 log a record.
157 """
158
159 def __init__(self, callback):
160 self.callback = callback
161
162 def filter(self, record):
163 if self.callback(record):
164 return 1
165 return 0
166
167
168class RequireDebugFalse(logging.Filter):
169 def filter(self, record):
170 return not settings.DEBUG
171
172
173class RequireDebugTrue(logging.Filter):
174 def filter(self, record):
175 return settings.DEBUG
176
177
178class ServerFormatter(logging.Formatter):
179 default_time_format = "%d/%b/%Y %H:%M:%S"
180
181 def __init__(self, *args, **kwargs):
182 self.style = color_style()
183 super().__init__(*args, **kwargs)
184
185 def format(self, record):
186 msg = record.msg
187 status_code = getattr(record, "status_code", None)
188
189 if status_code:
190 if 200 <= status_code < 300:
191 # Put 2XX first, since it should be the common case
192 msg = self.style.HTTP_SUCCESS(msg)
193 elif 100 <= status_code < 200:
194 msg = self.style.HTTP_INFO(msg)
195 elif status_code == 304:
196 msg = self.style.HTTP_NOT_MODIFIED(msg)
197 elif 300 <= status_code < 400:
198 msg = self.style.HTTP_REDIRECT(msg)
199 elif status_code == 404:
200 msg = self.style.HTTP_NOT_FOUND(msg)
201 elif 400 <= status_code < 500:
202 msg = self.style.HTTP_BAD_REQUEST(msg)
203 else:
204 # Any 5XX, or any other status code
205 msg = self.style.HTTP_SERVER_ERROR(msg)
206
207 if self.uses_server_time() and not hasattr(record, "server_time"):
208 record.server_time = self.formatTime(record, self.datefmt)
209
210 record.msg = msg
211 return super().format(record)
212
213 def uses_server_time(self):
214 return self._fmt.find("{server_time}") >= 0
215
216
217def log_response(
218 message,
219 *args,
220 response=None,
221 request=None,
222 logger=request_logger,
223 level=None,
224 exception=None,
225):
226 """
227 Log errors based on HttpResponse status.
228
229 Log 5xx responses as errors and 4xx responses as warnings (unless a level
230 is given as a keyword argument). The HttpResponse status_code and the
231 request are passed to the logger's extra parameter.
232 """
233 # Check if the response has already been logged. Multiple requests to log
234 # the same response can be received in some cases, e.g., when the
235 # response is the result of an exception and is logged when the exception
236 # is caught, to record the exception.
237 if getattr(response, "_has_been_logged", False):
238 return
239
240 if level is None:
241 if response.status_code >= 500:
242 level = "error"
243 elif response.status_code >= 400:
244 level = "warning"
245 else:
246 level = "info"
247
248 getattr(logger, level)(
249 message,
250 *args,
251 extra={
252 "status_code": response.status_code,
253 "request": request,
254 },
255 exc_info=exception,
256 )
257 response._has_been_logged = True