zulip/zerver/logging_handlers.py

160 lines
6.5 KiB
Python

# System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html
import logging
import platform
import os
import subprocess
import traceback
from typing import Any, Dict, Optional
from django.conf import settings
from django.http import HttpRequest
from django.views.debug import get_exception_reporter_filter
from zerver.lib.logging_util import find_log_caller_module
from zerver.lib.queue import queue_json_publish
from version import ZULIP_VERSION
def try_git_describe() -> Optional[str]:
try: # nocoverage
return subprocess.check_output(
['git', 'describe', '--tags', '--always', '--dirty', '--long'],
stderr=subprocess.PIPE,
cwd=os.path.join(os.path.dirname(__file__), '..'),
).strip().decode('utf-8')
except Exception: # nocoverage
return None
def add_deployment_metadata(report: Dict[str, Any]) -> None:
report['git_described'] = try_git_describe()
report['zulip_version_const'] = ZULIP_VERSION
version_path = os.path.join(os.path.dirname(__file__), '../version')
if os.path.exists(version_path):
with open(version_path, 'r') as f: # nocoverage
report['zulip_version_file'] = f.read().strip()
def add_request_metadata(report: Dict[str, Any], request: HttpRequest) -> None:
report['has_request'] = True
report['path'] = request.path
report['method'] = request.method
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),
try:
from django.contrib.auth.models import AnonymousUser
user_profile = request.user
if isinstance(user_profile, AnonymousUser):
user_full_name = None
user_email = None
else:
user_full_name = user_profile.full_name
user_email = user_profile.email
except Exception:
# Unexpected exceptions here should be handled gracefully
traceback.print_exc()
user_full_name = None
user_email = None
report['user_email'] = user_email
report['user_full_name'] = user_full_name
exception_filter = get_exception_reporter_filter(request)
try:
report['data'] = request.GET if request.method == 'GET' else \
exception_filter.get_post_parameters(request)
except Exception:
# exception_filter.get_post_parameters will throw
# RequestDataTooBig if there's a really big file uploaded
report['data'] = {}
try:
report['host'] = request.get_host().split(':')[0]
except Exception:
# request.get_host() will throw a DisallowedHost
# exception if the host is invalid
report['host'] = platform.node()
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.
"""
# adapted in part from django/utils/log.py
def __init__(self) -> None:
logging.Handler.__init__(self)
def emit(self, record: logging.LogRecord) -> None:
report = {} # type: Dict[str, Any]
# This parameter determines whether Zulip should attempt to
# send Zulip messages containing the error report. If there's
# syntax that makes the markdown processor throw an exception,
# 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).
#
# We initialize is_bugdown_rendering_exception to `True` to
# prevent the infinite loop of zulip messages by ERROR_BOT if
# the outer try block here throws an exception before we have
# a chance to check the exception for whether it comes from
# bugdown.
is_bugdown_rendering_exception = True
try:
report['node'] = platform.node()
report['host'] = platform.node()
add_deployment_metadata(report)
if record.exc_info:
stack_trace = ''.join(traceback.format_exception(*record.exc_info))
message = str(record.exc_info[1])
is_bugdown_rendering_exception = record.msg.startswith('Exception in Markdown parser')
else:
stack_trace = 'No stack trace available'
message = record.getMessage()
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]
is_bugdown_rendering_exception = False
report['stack_trace'] = stack_trace
report['message'] = message
report['logger_name'] = record.name
report['log_module'] = find_log_caller_module(record)
report['log_lineno'] = record.lineno
if hasattr(record, "request"):
add_request_metadata(report, record.request) # type: ignore # record.request is added dynamically
except Exception:
report['message'] = "Exception in preparing exception report!"
logging.warning(report['message'], exc_info=True)
report['stack_trace'] = "See /var/log/zulip/errors.log"
if settings.DEBUG_ERROR_REPORTING: # nocoverage
logging.warning("Reporting an error to admins...")
logging.warning("Reporting an error to admins: {} {} {} {} {}" .format(
record.levelname, report['logger_name'], report['log_module'],
report['message'], report['stack_trace']))
try:
if settings.STAGING_ERROR_NOTIFICATIONS:
# On staging, process the report directly so it can happen inside this
# try/except to prevent looping
from zerver.lib.error_notify import notify_server_error
notify_server_error(report, is_bugdown_rendering_exception)
else:
queue_json_publish('error_reports', dict(
type = "server",
report = report,
))
except Exception:
# 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)