# HG changeset patch # User Marcin Kuzminski # Date 2020-03-26 13:03:07 # Node ID cf6613424d89ff7a70e78d744118184dba99afc1 # Parent 9be1d088f314c707976ac7dd1c544badf9bb963e exception-tracker: enable send email on exception diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -350,6 +350,17 @@ labs_settings_active = true ; This is used to store exception from RhodeCode in shared directory #exception_tracker.store_path = +; Send email with exception details when it happens +#exception_tracker.send_email = false + +; Comma separated list of recipients for exception emails, +; e.g admin@rhodecode.com,devops@rhodecode.com +; Can be left empty, then emails will be sent to ALL super-admins +#exception_tracker.send_email_recipients = + +; optional prefix to Add to email Subject +#exception_tracker.email_prefix = [RHODECODE ERROR] + ; File store configuration. This is used to store and serve uploaded files file_store.enabled = true diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -301,6 +301,17 @@ labs_settings_active = true ; This is used to store exception from RhodeCode in shared directory #exception_tracker.store_path = +; Send email with exception details when it happens +#exception_tracker.send_email = false + +; Comma separated list of recipients for exception emails, +; e.g admin@rhodecode.com,devops@rhodecode.com +; Can be left empty, then emails will be sent to ALL super-admins +#exception_tracker.send_email_recipients = + +; optional prefix to Add to email Subject +#exception_tracker.email_prefix = [RHODECODE ERROR] + ; File store configuration. This is used to store and serve uploaded files file_store.enabled = true diff --git a/rhodecode/apps/debug_style/views.py b/rhodecode/apps/debug_style/views.py --- a/rhodecode/apps/debug_style/views.py +++ b/rhodecode/apps/debug_style/views.py @@ -88,6 +88,15 @@ Check if we should use full-topic or min 'modified': ['b/modified_file.rst'], 'removed': ['.idea'], }) + + exc_traceback = { + 'exc_utc_date': '2020-03-26T12:54:50.683281', + 'exc_id': 139638856342656, + 'exc_timestamp': '1585227290.683288', + 'version': 'v1', + 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n', + 'exc_type': 'AttributeError' + } email_kwargs = { 'test': {}, 'message': { @@ -97,6 +106,13 @@ Check if we should use full-topic or min 'user': user, 'date': datetime.datetime.now(), }, + 'exception': { + 'email_prefix': '[RHODECODE ERROR]', + 'exc_id': exc_traceback['exc_id'], + 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']), + 'exc_type_name': 'NameError', + 'exc_traceback': exc_traceback, + }, 'password_reset': { 'password_reset_url': 'http://example.com/reset-rhodecode-password/token', diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -611,6 +611,14 @@ def _sanitize_cache_settings(settings): settings, 'exception_tracker.store_path', temp_store, lower=False, default_when_empty=True) + _bool_setting( + settings, + 'exception_tracker.send_email', + 'false') + _string_setting( + settings, + 'exception_tracker.email_prefix', + '[RHODECODE ERROR]', lower=False, default_when_empty=True) # cache_perms _string_setting( diff --git a/rhodecode/lib/exc_tracking.py b/rhodecode/lib/exc_tracking.py --- a/rhodecode/lib/exc_tracking.py +++ b/rhodecode/lib/exc_tracking.py @@ -27,7 +27,6 @@ import traceback import tempfile import glob - log = logging.getLogger(__name__) # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking @@ -77,10 +76,11 @@ def get_exc_store(): return _exc_store_path -def _store_exception(exc_id, exc_type_name, exc_traceback, prefix): +def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None): """ Low level function to store exception in the exception tracker """ + import rhodecode as app exc_store_path = get_exc_store() exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name) @@ -92,6 +92,50 @@ def _store_exception(exc_id, exc_type_na f.write(exc_data) log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path) + if send_email is None: + # NOTE(marcink): read app config unless we specify explicitly + send_email = app.CONFIG.get('exception_tracker.send_email', False) + + if send_email: + try: + send_exc_email(exc_id, exc_type_name) + except Exception: + log.exception('Failed to send exception email') + pass + + +def send_exc_email(exc_id, exc_type_name): + import rhodecode as app + from pyramid.threadlocal import get_current_request + 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 + + request = get_current_request() + + recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', '')) + log.debug('Sending Email exception to: `%s`', recipients or 'all super admins') + + # NOTE(marcink): needed for email template rendering + attach_context_attributes(TemplateArgs(), request, request.user.user_id) + + email_kwargs = { + '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), + } + + (subject, headers, email_body, + email_body_plaintext) = EmailNotificationModel().render_email( + EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs) + + run_task(tasks.send_email, recipients, subject, + email_body_plaintext, email_body) + def _prepare_exception(exc_info): exc_type, exc_value, exc_traceback = exc_info diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -304,6 +304,7 @@ class EmailNotificationModel(BaseModel): TYPE_PASSWORD_RESET = 'password_reset' TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation' TYPE_EMAIL_TEST = 'email_test' + TYPE_EMAIL_EXCEPTION = 'exception' TYPE_TEST = 'test' email_types = { @@ -311,6 +312,8 @@ class EmailNotificationModel(BaseModel): 'rhodecode:templates/email_templates/main.mako', TYPE_TEST: 'rhodecode:templates/email_templates/test.mako', + TYPE_EMAIL_EXCEPTION: + 'rhodecode:templates/email_templates/exception_tracker.mako', TYPE_EMAIL_TEST: 'rhodecode:templates/email_templates/email_test.mako', TYPE_REGISTRATION: diff --git a/rhodecode/templates/email_templates/exception_tracker.mako b/rhodecode/templates/email_templates/exception_tracker.mako new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/exception_tracker.mako @@ -0,0 +1,18 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.mako"/> +<%namespace name="base" file="base.mako"/> + +<%def name="subject()" filter="n,trim,whitespace_filter"> +${email_prefix} ${exc_type_name} (${exc_id}) + + +## plain text version of the email. Empty by default +<%def name="body_plaintext()" filter="n,trim"> + NO PLAINTEXT VERSION + + +

${_('Exception `{}` generated on UTC date: {}').format(exc_traceback.get('exc_type', 'NO_TYPE'), exc_traceback.get('exc_utc_date', 'NO_DATE'))}

+

+ View exception ${exc_id} +

+
${exc_traceback.get('exc_message', 'NO_MESSAGE')}