exc_tracking.py
315 lines
| 9.6 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2010-2023 RhodeCode GmbH | |||
r2907 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
r5127 | import io | |||
r2907 | import os | |||
import time | ||||
r4811 | import sys | |||
r2907 | import datetime | |||
import msgpack | ||||
import logging | ||||
import traceback | ||||
import tempfile | ||||
r3975 | import glob | |||
r2907 | ||||
log = logging.getLogger(__name__) | ||||
# NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking | ||||
r5127 | global_prefix = "rhodecode" | |||
exc_store_dir_name = "rc_exception_store_v1" | ||||
r2907 | ||||
r4301 | def exc_serialize(exc_id, tb, exc_type, extra_data=None): | |||
r2907 | data = { | |||
r5127 | "version": "v1", | |||
"exc_id": exc_id, | ||||
"exc_utc_date": datetime.datetime.utcnow().isoformat(), | ||||
"exc_timestamp": repr(time.time()), | ||||
"exc_message": tb, | ||||
"exc_type": exc_type, | ||||
r2907 | } | |||
r4301 | if extra_data: | |||
data.update(extra_data) | ||||
r2907 | return msgpack.packb(data), data | |||
def exc_unserialize(tb): | ||||
return msgpack.unpackb(tb) | ||||
r5127 | ||||
r3975 | _exc_store = None | |||
r2907 | ||||
r5127 | def maybe_send_exc_email(exc_id, exc_type_name, send_email): | |||
from pyramid.threadlocal import get_current_request | ||||
r3019 | import rhodecode as app | |||
r2907 | ||||
r4301 | request = get_current_request() | |||
r2907 | ||||
r4276 | if send_email is None: | |||
# NOTE(marcink): read app config unless we specify explicitly | ||||
r5127 | send_email = app.CONFIG.get("exception_tracker.send_email", False) | |||
r4276 | ||||
r5127 | mail_server = app.CONFIG.get("smtp_server") or None | |||
r4297 | send_email = send_email and mail_server | |||
r4541 | if send_email and request: | |||
r4276 | try: | |||
r4301 | send_exc_email(request, exc_id, exc_type_name) | |||
r4276 | except Exception: | |||
r5127 | log.exception("Failed to send exception email") | |||
r4811 | exc_info = sys.exc_info() | |||
store_exception(id(exc_info), exc_info, send_email=False) | ||||
r4276 | ||||
r4301 | def send_exc_email(request, exc_id, exc_type_name): | |||
r4276 | import rhodecode as app | |||
from rhodecode.apps._base import TemplateArgs | ||||
from rhodecode.lib.utils2 import aslist | ||||
from rhodecode.lib.celerylib import run_task, tasks | ||||
from rhodecode.lib.base import attach_context_attributes | ||||
from rhodecode.model.notification import EmailNotificationModel | ||||
r5127 | recipients = aslist(app.CONFIG.get("exception_tracker.send_email_recipients", "")) | |||
log.debug("Sending Email exception to: `%s`", recipients or "all super admins") | ||||
r4276 | ||||
# NOTE(marcink): needed for email template rendering | ||||
r4277 | user_id = None | |||
r5127 | if hasattr(request, "user"): | |||
r4277 | user_id = request.user.user_id | |||
attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True) | ||||
r4276 | ||||
email_kwargs = { | ||||
r5127 | "email_prefix": app.CONFIG.get("exception_tracker.email_prefix", "") | |||
or "[RHODECODE ERROR]", | ||||
"exc_url": request.route_url( | ||||
"admin_settings_exception_tracker_show", exception_id=exc_id | ||||
), | ||||
"exc_id": exc_id, | ||||
"exc_type_name": exc_type_name, | ||||
"exc_traceback": read_exception(exc_id, prefix=None), | ||||
r4276 | } | |||
r4447 | (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email( | |||
r5127 | EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs | |||
) | ||||
run_task(tasks.send_email, recipients, subject, email_body_plaintext, email_body) | ||||
def get_exc_store(): | ||||
""" | ||||
Get and create exception store if it's not existing | ||||
""" | ||||
global _exc_store | ||||
r4276 | ||||
r5127 | if _exc_store is not None: | |||
# quick global cache | ||||
return _exc_store | ||||
import rhodecode as app | ||||
exc_store_dir = ( | ||||
app.CONFIG.get("exception_tracker.store_path", "") or tempfile.gettempdir() | ||||
) | ||||
_exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name) | ||||
_exc_store_path = os.path.abspath(_exc_store_path) | ||||
if not os.path.isdir(_exc_store_path): | ||||
os.makedirs(_exc_store_path) | ||||
log.debug("Initializing exceptions store at %s", _exc_store_path) | ||||
_exc_store = _exc_store_path | ||||
return _exc_store_path | ||||
r4276 | ||||
r2907 | ||||
r5127 | def get_detailed_tb(exc_info): | |||
try: | ||||
from pip._vendor.rich import ( | ||||
traceback as rich_tb, | ||||
scope as rich_scope, | ||||
console as rich_console, | ||||
) | ||||
except ImportError: | ||||
try: | ||||
from rich import ( | ||||
traceback as rich_tb, | ||||
scope as rich_scope, | ||||
console as rich_console, | ||||
) | ||||
except ImportError: | ||||
return None | ||||
console = rich_console.Console(width=160, file=io.StringIO()) | ||||
exc = rich_tb.Traceback.extract(*exc_info, show_locals=True) | ||||
r3317 | ||||
r5127 | tb_rich = rich_tb.Traceback( | |||
trace=exc, | ||||
width=160, | ||||
extra_lines=3, | ||||
theme=None, | ||||
word_wrap=False, | ||||
show_locals=False, | ||||
max_frames=100, | ||||
) | ||||
r3317 | ||||
r5127 | # last_stack = exc.stacks[-1] | |||
# last_frame = last_stack.frames[-1] | ||||
# if last_frame and last_frame.locals: | ||||
# console.print( | ||||
# rich_scope.render_scope( | ||||
# last_frame.locals, | ||||
# title=f'{last_frame.filename}:{last_frame.lineno}')) | ||||
console.print(tb_rich) | ||||
formatted_locals = console.file.getvalue() | ||||
return formatted_locals | ||||
r3317 | ||||
r5127 | def get_request_metadata(request=None) -> dict: | |||
request_metadata = {} | ||||
if not request: | ||||
from pyramid.threadlocal import get_current_request | ||||
request = get_current_request() | ||||
# NOTE(marcink): store request information into exc_data | ||||
if request: | ||||
request_metadata["client_address"] = getattr(request, "client_addr", "") | ||||
request_metadata["user_agent"] = getattr(request, "user_agent", "") | ||||
request_metadata["method"] = getattr(request, "method", "") | ||||
request_metadata["url"] = getattr(request, "url", "") | ||||
return request_metadata | ||||
def format_exc(exc_info): | ||||
exc_type, exc_value, exc_traceback = exc_info | ||||
tb = "++ TRACEBACK ++\n\n" | ||||
tb += "".join(traceback.format_exception(exc_type, exc_value, exc_traceback, None)) | ||||
locals_tb = get_detailed_tb(exc_info) | ||||
if locals_tb: | ||||
tb += f"\n+++ DETAILS +++\n\n{locals_tb}\n" "" | ||||
return tb | ||||
def _store_exception(exc_id, exc_info, prefix, request_path='', send_email=None): | ||||
""" | ||||
Low level function to store exception in the exception tracker | ||||
""" | ||||
extra_data = {} | ||||
extra_data.update(get_request_metadata()) | ||||
exc_type, exc_value, exc_traceback = exc_info | ||||
tb = format_exc(exc_info) | ||||
exc_type_name = exc_type.__name__ | ||||
exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name, extra_data=extra_data) | ||||
exc_pref_id = f"{exc_id}_{prefix}_{org_data['exc_timestamp']}" | ||||
exc_store_path = get_exc_store() | ||||
if not os.path.isdir(exc_store_path): | ||||
os.makedirs(exc_store_path) | ||||
stored_exc_path = os.path.join(exc_store_path, exc_pref_id) | ||||
with open(stored_exc_path, "wb") as f: | ||||
f.write(exc_data) | ||||
log.debug("Stored generated exception %s as: %s", exc_id, stored_exc_path) | ||||
if request_path: | ||||
log.error( | ||||
'error occurred handling this request.\n' | ||||
'Path: `%s`, %s', | ||||
request_path, tb) | ||||
maybe_send_exc_email(exc_id, exc_type_name, send_email) | ||||
def store_exception(exc_id, exc_info, prefix=global_prefix, request_path='', send_email=None): | ||||
r3007 | """ | |||
Example usage:: | ||||
exc_info = sys.exc_info() | ||||
store_exception(id(exc_info), exc_info) | ||||
""" | ||||
r2907 | try: | |||
r5127 | exc_type = exc_info[0] | |||
exc_type_name = exc_type.__name__ | ||||
_store_exception( | ||||
exc_id=exc_id, exc_info=exc_info, prefix=prefix, request_path=request_path, | ||||
send_email=send_email | ||||
) | ||||
r4112 | return exc_id, exc_type_name | |||
r2907 | except Exception: | |||
r5127 | log.exception("Failed to store exception `%s` information", exc_id) | |||
r2907 | # there's no way this can fail, it will crash server badly if it does. | |||
pass | ||||
def _find_exc_file(exc_id, prefix=global_prefix): | ||||
exc_store_path = get_exc_store() | ||||
if prefix: | ||||
r5127 | exc_id = f"{exc_id}_{prefix}" | |||
r2907 | else: | |||
# search without a prefix | ||||
r5127 | exc_id = f"{exc_id}" | |||
r2907 | ||||
r3975 | found_exc_id = None | |||
r5127 | matches = glob.glob(os.path.join(exc_store_path, exc_id) + "*") | |||
r3975 | if matches: | |||
found_exc_id = matches[0] | ||||
r2907 | ||||
r3975 | return found_exc_id | |||
r2907 | ||||
def _read_exception(exc_id, prefix): | ||||
exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix) | ||||
if exc_id_file_path: | ||||
r5127 | with open(exc_id_file_path, "rb") as f: | |||
r2907 | return exc_unserialize(f.read()) | |||
else: | ||||
r5127 | log.debug("Exception File `%s` not found", exc_id_file_path) | |||
r2907 | return None | |||
def read_exception(exc_id, prefix=global_prefix): | ||||
try: | ||||
return _read_exception(exc_id=exc_id, prefix=prefix) | ||||
except Exception: | ||||
r5127 | log.exception("Failed to read exception `%s` information", exc_id) | |||
r2907 | # there's no way this can fail, it will crash server badly if it does. | |||
return None | ||||
def delete_exception(exc_id, prefix=global_prefix): | ||||
try: | ||||
exc_id_file_path = _find_exc_file(exc_id, prefix=prefix) | ||||
if exc_id_file_path: | ||||
os.remove(exc_id_file_path) | ||||
except Exception: | ||||
r5127 | log.exception("Failed to remove exception `%s` information", exc_id) | |||
r2907 | # there's no way this can fail, it will crash server badly if it does. | |||
pass | ||||
r3317 | ||||
def generate_id(): | ||||
return id(object()) | ||||