diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -60,6 +60,19 @@ def admin_routes(config):
pattern='/settings/system/updates')
config.add_route(
+ name='admin_settings_exception_tracker',
+ pattern='/settings/exceptions')
+ config.add_route(
+ name='admin_settings_exception_tracker_delete_all',
+ pattern='/settings/exceptions/delete')
+ config.add_route(
+ name='admin_settings_exception_tracker_show',
+ pattern='/settings/exceptions/{exception_id}')
+ config.add_route(
+ name='admin_settings_exception_tracker_delete',
+ pattern='/settings/exceptions/{exception_id}/delete')
+
+ config.add_route(
name='admin_settings_sessions',
pattern='/settings/sessions')
config.add_route(
diff --git a/rhodecode/apps/admin/navigation.py b/rhodecode/apps/admin/navigation.py
--- a/rhodecode/apps/admin/navigation.py
+++ b/rhodecode/apps/admin/navigation.py
@@ -89,6 +89,9 @@ class NavigationRegistry(object):
'global_integrations_home'),
NavEntry('system', _('System Info'),
'admin_settings_system'),
+ NavEntry('exceptions', _('Exceptions Tracker'),
+ 'admin_settings_exception_tracker',
+ active_list=['exceptions', 'exceptions_browse']),
NavEntry('process_management', _('Processes'),
'admin_settings_process_management'),
NavEntry('sessions', _('User Sessions'),
diff --git a/rhodecode/apps/admin/views/exception_tracker.py b/rhodecode/apps/admin/views/exception_tracker.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/exception_tracker.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2018-2018 RhodeCode GmbH
+#
+# 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 .
+#
+# 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/
+import os
+import logging
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.apps.admin.navigation import navigation_list
+from rhodecode.lib import helpers as h
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAllDecorator, CSRFRequired)
+from rhodecode.lib.utils2 import time_to_utcdatetime
+from rhodecode.lib import exc_tracking
+
+log = logging.getLogger(__name__)
+
+
+class ExceptionsTrackerView(BaseAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ c.navlist = navigation_list(self.request)
+ return c
+
+ def count_all_exceptions(self):
+ exc_store_path = exc_tracking.get_exc_store()
+ count = 0
+ for fname in os.listdir(exc_store_path):
+ parts = fname.split('_', 2)
+ if not len(parts) == 3:
+ continue
+ count +=1
+ return count
+
+ def get_all_exceptions(self, read_metadata=False, limit=None):
+ exc_store_path = exc_tracking.get_exc_store()
+ exception_list = []
+
+ def key_sorter(val):
+ try:
+ return val.split('_')[-1]
+ except Exception:
+ return 0
+ count = 0
+ for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
+
+ parts = fname.split('_', 2)
+ if not len(parts) == 3:
+ continue
+
+ exc_id, app_type, exc_timestamp = parts
+
+ exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
+ 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
+
+ if read_metadata:
+ full_path = os.path.join(exc_store_path, fname)
+ # we can read our metadata
+ with open(full_path, 'rb') as f:
+ exc_metadata = exc_tracking.exc_unserialize(f.read())
+ exc.update(exc_metadata)
+
+ # convert our timestamp to a date obj, for nicer representation
+ exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
+ exception_list.append(exc)
+
+ count += 1
+ if limit and count >= limit:
+ break
+ return exception_list
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_settings_exception_tracker', request_method='GET',
+ renderer='rhodecode:templates/admin/settings/settings.mako')
+ def browse_exceptions(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'exceptions_browse'
+ c.limit = self.request.GET.get('limit', 50)
+ c.exception_list = self.get_all_exceptions(read_metadata=True, limit=c.limit)
+ c.exception_list_count = self.count_all_exceptions()
+ c.exception_store_dir = exc_tracking.get_exc_store()
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_settings_exception_tracker_show', request_method='GET',
+ renderer='rhodecode:templates/admin/settings/settings.mako')
+ def exception_show(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.active = 'exceptions'
+ c.exception_id = self.request.matchdict['exception_id']
+ c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
+ renderer='rhodecode:templates/admin/settings/settings.mako')
+ def exception_delete_all(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.active = 'exceptions'
+ all_exc = self.get_all_exceptions()
+ exc_count = len(all_exc)
+ for exc in all_exc:
+ exc_tracking.delete_exception(exc['exc_id'], prefix=None)
+
+ h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
+ raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_settings_exception_tracker_delete', request_method='POST',
+ renderer='rhodecode:templates/admin/settings/settings.mako')
+ def exception_delete(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.active = 'exceptions'
+ c.exception_id = self.request.matchdict['exception_id']
+ exc_tracking.delete_exception(c.exception_id, prefix=None)
+
+ h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
+ raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py
--- a/rhodecode/config/middleware.py
+++ b/rhodecode/config/middleware.py
@@ -19,6 +19,7 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
+import sys
import logging
import traceback
import collections
@@ -48,6 +49,7 @@ from rhodecode.lib.middleware.https_fixu
from rhodecode.lib.celerylib.loader import configure_celery
from rhodecode.lib.plugins.utils import register_rhodecode_plugin
from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
+from rhodecode.lib.exc_tracking import store_exception
from rhodecode.subscribers import (
scan_repositories_if_enabled, write_js_routes_if_enabled,
write_metadata_if_needed, inject_app_settings)
@@ -172,7 +174,17 @@ def error_handler(exception, request):
c.causes = base_response.causes
c.messages = helpers.flash.pop_messages(request=request)
- c.traceback = traceback.format_exc()
+
+ exc_info = sys.exc_info()
+ c.exception_id = id(exc_info)
+ c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
+ or base_response.status_code > 499
+ c.exception_id_url = request.route_url(
+ 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
+
+ if c.show_exception_id:
+ store_exception(c.exception_id, exc_info)
+
response = render_to_response(
'/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
response=base_response)
diff --git a/rhodecode/lib/exc_tracking.py b/rhodecode/lib/exc_tracking.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/exc_tracking.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2010-2018 RhodeCode GmbH
+#
+# 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 .
+#
+# 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/
+
+import os
+import time
+import datetime
+import msgpack
+import logging
+import traceback
+import tempfile
+
+
+log = logging.getLogger(__name__)
+
+# NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
+global_prefix = 'rhodecode'
+
+
+def exc_serialize(exc_id, tb, exc_type):
+
+ data = {
+ '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,
+ }
+ return msgpack.packb(data), data
+
+
+def exc_unserialize(tb):
+ return msgpack.unpackb(tb)
+
+
+def get_exc_store():
+ """
+ Get and create exception store if it's not existing
+ """
+ exc_store_dir = 'rc_exception_store_v1'
+ # fallback
+ _exc_store_path = os.path.join(tempfile.gettempdir(), exc_store_dir)
+
+ exc_store_dir = '' # TODO: need a persistent cross instance store here
+ if exc_store_dir:
+ _exc_store_path = os.path.join(exc_store_dir, exc_store_dir)
+
+ _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)
+ return _exc_store_path
+
+
+def _store_exception(exc_id, exc_info, prefix):
+ exc_type, exc_value, exc_traceback = exc_info
+ tb = ''.join(traceback.format_exception(
+ exc_type, exc_value, exc_traceback, None))
+
+ exc_type_name = exc_type.__name__
+ exc_store_path = get_exc_store()
+ exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
+ exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
+ 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)
+
+
+def store_exception(exc_id, exc_info, prefix=global_prefix):
+ try:
+ _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
+ except Exception:
+ log.exception('Failed to store exception `%s` information', exc_id)
+ # 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:
+ exc_id = '{}_{}'.format(exc_id, prefix)
+ else:
+ # search without a prefix
+ exc_id = '{}'.format(exc_id)
+
+ # we need to search the store for such start pattern as above
+ for fname in os.listdir(exc_store_path):
+ if fname.startswith(exc_id):
+ exc_id = os.path.join(exc_store_path, fname)
+ break
+ continue
+ else:
+ exc_id = None
+
+ return exc_id
+
+
+def _read_exception(exc_id, prefix):
+ exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
+ if exc_id_file_path:
+ with open(exc_id_file_path, 'rb') as f:
+ return exc_unserialize(f.read())
+ else:
+ log.debug('Exception File `%s` not found', exc_id_file_path)
+ return None
+
+
+def read_exception(exc_id, prefix=global_prefix):
+ try:
+ return _read_exception(exc_id=exc_id, prefix=prefix)
+ except Exception:
+ log.exception('Failed to read exception `%s` information', exc_id)
+ # 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:
+ log.exception('Failed to remove exception `%s` information', exc_id)
+ # there's no way this can fail, it will crash server badly if it does.
+ pass
diff --git a/rhodecode/public/css/main-content.less b/rhodecode/public/css/main-content.less
--- a/rhodecode/public/css/main-content.less
+++ b/rhodecode/public/css/main-content.less
@@ -77,6 +77,13 @@
min-width: 100%;
}
+.main-content-auto-width {
+ .main-content;
+ width: auto;
+ min-width: 100%;
+ max-width: inherit;
+}
+
.field {
clear: left;
margin-bottom: @padding;
diff --git a/rhodecode/templates/admin/settings/settings.mako b/rhodecode/templates/admin/settings/settings.mako
--- a/rhodecode/templates/admin/settings/settings.mako
+++ b/rhodecode/templates/admin/settings/settings.mako
@@ -44,7 +44,7 @@
-
diff --git a/rhodecode/templates/admin/settings/settings_exceptions.mako b/rhodecode/templates/admin/settings/settings_exceptions.mako
new file mode 100644
--- /dev/null
+++ b/rhodecode/templates/admin/settings/settings_exceptions.mako
@@ -0,0 +1,38 @@
+
+
+
${_('Exceptions Tracker - Exception ID')}: ${c.exception_id}
+
+
+ % if c.traceback:
+
+
${_('Exception `{}` generated on UTC date: {}').format(c.traceback.get('exc_type', 'NO_TYPE'), c.traceback.get('exc_utc_date', 'NO_DATE'))}
+
${c.traceback.get('exc_message', 'NO_MESSAGE')}
+
+ % else:
+ ${_('Unable to Read Exception. It might be removed or non-existing.')}
+ % endif
+
+
+
+
+% if c.traceback:
+
+
+
${_('Delete this Exception')}
+
+
+ ${h.secure_form(h.route_path('admin_settings_exception_tracker_delete', exception_id=c.exception_id), request=request)}
+
+
+
+
+
+ ${_('Delete This Exception')}
+
+
+
+ ${h.end_form()}
+
+
+% endif
diff --git a/rhodecode/templates/admin/settings/settings_exceptions_browse.mako b/rhodecode/templates/admin/settings/settings_exceptions_browse.mako
new file mode 100644
--- /dev/null
+++ b/rhodecode/templates/admin/settings/settings_exceptions_browse.mako
@@ -0,0 +1,52 @@
+
+
+
${_('Exceptions Tracker ')}
+
+
+ % if c.exception_list_count == 1:
+ ${_('There is {} stored exception.').format(c.exception_list_count)}
+ % else:
+ ${_('There are {} stored exceptions.').format(c.exception_list_count)}
+ % endif
+ ${_('Store directory')}: ${c.exception_store_dir}
+
+ ${h.secure_form(h.route_path('admin_settings_exception_tracker_delete_all'), request=request)}
+
+
+
+
+
+ ${_('Delete All')}
+
+
+
+ ${h.end_form()}
+
+
+
+
+
+
+
+
${_('Exceptions Tracker - Showing the last {} Exceptions').format(c.limit)}
+
+
+
+
+ Exception ID
+ Date
+ App Type
+ Exc Type
+
+ % for tb in c.exception_list:
+
+ ${tb['exc_id']}
+ ${h.format_date(tb['exc_utc_date'])}
+ ${tb['app_type']}
+ ${tb['exc_type']}
+
+ % endfor
+
+
+
diff --git a/rhodecode/templates/errors/error_document.mako b/rhodecode/templates/errors/error_document.mako
--- a/rhodecode/templates/errors/error_document.mako
+++ b/rhodecode/templates/errors/error_document.mako
@@ -60,9 +60,17 @@
Support
-
For support, go to ${_('Support')} .
+
For help and support, go to the ${_('Support Page')} .
It may be useful to include your log file; see the log file locations here .
+ % if c.show_exception_id:
+
+ Super Admin can see detailed traceback information from this exception by checking the below Exception ID.
+ Please include the below link for further details of this exception.
+
+ Exception ID: ${c.exception_id}
+
+ % endif
Documentation