2017-11-16 19:51:44 +01:00
|
|
|
# System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html
|
2012-12-06 22:00:34 +01:00
|
|
|
import logging
|
2017-12-01 01:54:24 +01:00
|
|
|
import os
|
2020-06-11 00:54:34 +02:00
|
|
|
import platform
|
2017-12-01 01:54:24 +01:00
|
|
|
import subprocess
|
2017-11-16 00:50:28 +01:00
|
|
|
import traceback
|
|
|
|
from typing import Any, Dict, Optional
|
2020-06-27 03:05:47 +02:00
|
|
|
from urllib.parse import SplitResult
|
2012-12-06 22:00:34 +01:00
|
|
|
|
2017-11-16 00:50:28 +01:00
|
|
|
from django.conf import settings
|
2016-06-05 07:51:18 +02:00
|
|
|
from django.http import HttpRequest
|
models: Force the translated role into a translated string.
The return type of `ugettext_lazy('...')` (aliased as `_`) is a
promise, which is only forced into a string when it is dealt with in
string context. This `django.utils.functional.lazy.__proxy__` object
is not entirely transparent, however -- it cannot be serialized by
`orjson`, and `isinstance(x, str) == False`, which can lead to
surprising action-at-a-distance.
In the two places which will serialize the role value (either into
Zulip's own error reporting queue, or Sentry's), force the return
value. Failure to do this results in errors being dropped
mostly-silently, as they cannot be serialized and enqueued by the
error reporter logger, which has no recourse but to just log a
warning; see previous commit.
When we do this forcing, explicitly override the language to be the
realm default. Failure to provide this override would translate the
role into the role in the language of the _request_, yielding varying
results.
2020-08-14 02:59:07 +02:00
|
|
|
from django.utils.translation import override as override_language
|
2019-02-02 23:53:55 +01:00
|
|
|
from django.views.debug import get_exception_reporter_filter
|
2020-08-14 07:52:01 +02:00
|
|
|
from sentry_sdk import capture_exception
|
2020-06-23 22:53:08 +02:00
|
|
|
from typing_extensions import Protocol, runtime_checkable
|
2012-12-06 22:00:34 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from version import ZULIP_VERSION
|
2017-12-01 01:26:37 +01:00
|
|
|
from zerver.lib.logging_util import find_log_caller_module
|
2013-11-13 19:12:22 +01:00
|
|
|
from zerver.lib.queue import queue_json_publish
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-12-01 01:54:24 +01:00
|
|
|
|
|
|
|
def try_git_describe() -> Optional[str]:
|
2018-03-11 17:29:38 +01:00
|
|
|
try: # nocoverage
|
2017-12-01 01:54:24 +01:00
|
|
|
return subprocess.check_output(
|
2019-10-24 23:02:59 +02:00
|
|
|
['git', 'describe', '--tags', '--match=[0-9]*', '--always', '--dirty', '--long'],
|
2017-12-01 01:54:24 +01:00
|
|
|
stderr=subprocess.PIPE,
|
2019-02-28 22:14:54 +01:00
|
|
|
cwd=os.path.join(os.path.dirname(__file__), '..'),
|
2017-12-01 01:54:24 +01:00
|
|
|
).strip().decode('utf-8')
|
2020-10-09 03:32:00 +02:00
|
|
|
except (FileNotFoundError, subprocess.CalledProcessError): # nocoverage
|
2017-12-01 01:54:24 +01:00
|
|
|
return None
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def add_request_metadata(report: Dict[str, Any], request: HttpRequest) -> None:
|
2017-11-30 23:45:45 +01:00
|
|
|
report['has_request'] = True
|
|
|
|
|
2017-03-26 06:53:10 +02:00
|
|
|
report['path'] = request.path
|
|
|
|
report['method'] = request.method
|
2020-04-10 05:27:24 +02:00
|
|
|
report['remote_addr'] = request.META.get('REMOTE_ADDR', None)
|
|
|
|
report['query_string'] = request.META.get('QUERY_STRING', None)
|
|
|
|
report['server_name'] = request.META.get('SERVER_NAME', None)
|
2017-03-26 06:53:10 +02:00
|
|
|
try:
|
|
|
|
from django.contrib.auth.models import AnonymousUser
|
|
|
|
user_profile = request.user
|
|
|
|
if isinstance(user_profile, AnonymousUser):
|
|
|
|
user_full_name = None
|
|
|
|
user_email = None
|
2020-07-21 02:44:56 +02:00
|
|
|
user_role = None
|
2017-03-26 06:53:10 +02:00
|
|
|
else:
|
|
|
|
user_full_name = user_profile.full_name
|
|
|
|
user_email = user_profile.email
|
models: Force the translated role into a translated string.
The return type of `ugettext_lazy('...')` (aliased as `_`) is a
promise, which is only forced into a string when it is dealt with in
string context. This `django.utils.functional.lazy.__proxy__` object
is not entirely transparent, however -- it cannot be serialized by
`orjson`, and `isinstance(x, str) == False`, which can lead to
surprising action-at-a-distance.
In the two places which will serialize the role value (either into
Zulip's own error reporting queue, or Sentry's), force the return
value. Failure to do this results in errors being dropped
mostly-silently, as they cannot be serialized and enqueued by the
error reporter logger, which has no recourse but to just log a
warning; see previous commit.
When we do this forcing, explicitly override the language to be the
realm default. Failure to provide this override would translate the
role into the role in the language of the _request_, yielding varying
results.
2020-08-14 02:59:07 +02:00
|
|
|
with override_language(settings.LANGUAGE_CODE):
|
|
|
|
# str() to force the lazy-translation to apply now,
|
|
|
|
# since it won't serialize into the worker queue.
|
|
|
|
user_role = str(user_profile.get_role_name())
|
2017-03-26 06:53:10 +02:00
|
|
|
except Exception:
|
|
|
|
# Unexpected exceptions here should be handled gracefully
|
|
|
|
traceback.print_exc()
|
|
|
|
user_full_name = None
|
|
|
|
user_email = None
|
2020-07-21 02:44:56 +02:00
|
|
|
user_role = None
|
2020-07-21 02:08:10 +02:00
|
|
|
|
|
|
|
report['user'] = {
|
|
|
|
'user_email': user_email,
|
|
|
|
'user_full_name': user_full_name,
|
2020-07-21 02:44:56 +02:00
|
|
|
'user_role': user_role,
|
2020-07-21 02:08:10 +02:00
|
|
|
}
|
2017-03-26 06:53:10 +02:00
|
|
|
|
|
|
|
exception_filter = get_exception_reporter_filter(request)
|
|
|
|
try:
|
2019-08-12 05:44:35 +02:00
|
|
|
report['data'] = exception_filter.get_post_parameters(request) \
|
|
|
|
if request.method == 'POST' else request.GET
|
2017-03-26 06:53:10 +02:00
|
|
|
except Exception:
|
|
|
|
# exception_filter.get_post_parameters will throw
|
|
|
|
# RequestDataTooBig if there's a really big file uploaded
|
|
|
|
report['data'] = {}
|
|
|
|
|
|
|
|
try:
|
2020-06-27 03:05:47 +02:00
|
|
|
report['host'] = SplitResult("", request.get_host(), "", "", "").hostname
|
2017-03-26 06:53:10 +02:00
|
|
|
except Exception:
|
|
|
|
# request.get_host() will throw a DisallowedHost
|
|
|
|
# exception if the host is invalid
|
|
|
|
report['host'] = platform.node()
|
|
|
|
|
2020-06-23 22:53:08 +02:00
|
|
|
@runtime_checkable
|
|
|
|
class HasRequest(Protocol):
|
|
|
|
request: HttpRequest
|
|
|
|
|
2017-11-30 23:06:21 +01:00
|
|
|
class AdminNotifyHandler(logging.Handler):
|
|
|
|
"""An logging handler that sends the log/exception to the queue to be
|
|
|
|
turned into an email and/or a Zulip message for the server admins.
|
2012-12-06 22:00:34 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
# adapted in part from django/utils/log.py
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def __init__(self) -> None:
|
2012-12-06 22:00:34 +01:00
|
|
|
logging.Handler.__init__(self)
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
report: Dict[str, Any] = {}
|
2017-11-30 22:55:48 +01:00
|
|
|
|
2018-07-02 09:55:42 +02:00
|
|
|
# This parameter determines whether Zulip should attempt to
|
|
|
|
# send Zulip messages containing the error report. If there's
|
2020-08-11 01:47:49 +02:00
|
|
|
# syntax that makes the Markdown processor throw an exception,
|
2018-07-02 09:55:42 +02:00
|
|
|
# we really don't want to send that syntax into a new Zulip
|
|
|
|
# message in exception handler (that's the stuff of which
|
|
|
|
# recursive exception loops are made).
|
|
|
|
#
|
2020-06-28 14:21:55 +02:00
|
|
|
# We initialize is_markdown_rendering_exception to `True` to
|
2020-10-23 02:43:28 +02:00
|
|
|
# prevent the infinite loop of Zulip messages by ERROR_BOT if
|
2018-07-02 09:55:42 +02:00
|
|
|
# the outer try block here throws an exception before we have
|
|
|
|
# a chance to check the exception for whether it comes from
|
2020-06-28 14:21:55 +02:00
|
|
|
# markdown.
|
|
|
|
is_markdown_rendering_exception = True
|
2018-07-02 09:55:42 +02:00
|
|
|
|
2012-12-06 22:00:34 +01:00
|
|
|
try:
|
2017-11-30 22:55:48 +01:00
|
|
|
report['node'] = platform.node()
|
|
|
|
report['host'] = platform.node()
|
|
|
|
|
2020-05-07 00:30:36 +02:00
|
|
|
report['deployment_data'] = dict(
|
|
|
|
git=try_git_describe(),
|
|
|
|
ZULIP_VERSION=ZULIP_VERSION,
|
|
|
|
)
|
2017-12-01 01:54:24 +01:00
|
|
|
|
2013-11-13 19:12:22 +01:00
|
|
|
if record.exc_info:
|
2017-11-30 22:55:48 +01:00
|
|
|
stack_trace = ''.join(traceback.format_exception(*record.exc_info))
|
2017-03-26 07:45:10 +02:00
|
|
|
message = str(record.exc_info[1])
|
2020-06-28 14:21:55 +02:00
|
|
|
is_markdown_rendering_exception = record.msg.startswith('Exception in Markdown parser')
|
2013-11-13 19:12:22 +01:00
|
|
|
else:
|
2017-11-30 23:18:16 +01:00
|
|
|
stack_trace = 'No stack trace available'
|
2017-03-26 07:45:10 +02:00
|
|
|
message = record.getMessage()
|
2017-04-26 03:52:12 +02:00
|
|
|
if '\n' in message:
|
|
|
|
# Some exception code paths in queue processors
|
|
|
|
# seem to result in super-long messages
|
|
|
|
stack_trace = message
|
|
|
|
message = message.split('\n')[0]
|
2020-06-28 14:21:55 +02:00
|
|
|
is_markdown_rendering_exception = False
|
2017-11-30 22:55:48 +01:00
|
|
|
report['stack_trace'] = stack_trace
|
|
|
|
report['message'] = message
|
2013-11-13 19:12:22 +01:00
|
|
|
|
2017-12-01 01:26:37 +01:00
|
|
|
report['logger_name'] = record.name
|
|
|
|
report['log_module'] = find_log_caller_module(record)
|
|
|
|
report['log_lineno'] = record.lineno
|
|
|
|
|
2020-06-23 22:53:08 +02:00
|
|
|
if isinstance(record, HasRequest):
|
|
|
|
add_request_metadata(report, record.request)
|
2017-11-30 22:55:48 +01:00
|
|
|
|
2017-01-08 16:36:19 +01:00
|
|
|
except Exception:
|
2017-11-30 22:55:48 +01:00
|
|
|
report['message'] = "Exception in preparing exception report!"
|
|
|
|
logging.warning(report['message'], exc_info=True)
|
|
|
|
report['stack_trace'] = "See /var/log/zulip/errors.log"
|
2020-08-14 07:52:01 +02:00
|
|
|
capture_exception()
|
2012-12-06 22:00:34 +01:00
|
|
|
|
2018-03-21 01:03:12 +01:00
|
|
|
if settings.DEBUG_ERROR_REPORTING: # nocoverage
|
|
|
|
logging.warning("Reporting an error to admins...")
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.warning(
|
|
|
|
"Reporting an error to admins: %s %s %s %s %s",
|
2018-03-21 01:03:12 +01:00
|
|
|
record.levelname, report['logger_name'], report['log_module'],
|
2020-05-02 08:44:14 +02:00
|
|
|
report['message'], report['stack_trace'],
|
|
|
|
)
|
2018-03-21 01:03:12 +01:00
|
|
|
|
2013-01-25 20:15:26 +01:00
|
|
|
try:
|
2016-07-19 06:41:55 +02:00
|
|
|
if settings.STAGING_ERROR_NOTIFICATIONS:
|
2013-11-13 19:12:22 +01:00
|
|
|
# On staging, process the report directly so it can happen inside this
|
|
|
|
# try/except to prevent looping
|
2017-03-26 07:00:08 +02:00
|
|
|
from zerver.lib.error_notify import notify_server_error
|
2020-06-28 14:21:55 +02:00
|
|
|
notify_server_error(report, is_markdown_rendering_exception)
|
2015-08-21 09:02:03 +02:00
|
|
|
else:
|
|
|
|
queue_json_publish('error_reports', dict(
|
|
|
|
type = "server",
|
|
|
|
report = report,
|
2017-11-24 13:18:46 +01:00
|
|
|
))
|
2017-01-08 16:36:19 +01:00
|
|
|
except Exception:
|
2013-01-25 20:15:26 +01:00
|
|
|
# If this breaks, complain loudly but don't pass the traceback up the stream
|
|
|
|
# However, we *don't* want to use logging.exception since that could trigger a loop.
|
|
|
|
logging.warning("Reporting an exception triggered an exception!", exc_info=True)
|
2020-08-14 07:52:01 +02:00
|
|
|
capture_exception()
|