diff --git a/rhodecode/api/tests/test_store_exception.py b/rhodecode/api/tests/test_store_exception.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/api/tests/test_store_exception.py
@@ -0,0 +1,59 @@
+# -*- 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 pytest
+
+from rhodecode.api.tests.utils import build_data, api_call, assert_ok, assert_error
+
+
+@pytest.mark.usefixtures("testuser_api", "app")
+class TestStoreException(object):
+
+ def test_store_exception_invalid_json(self):
+ id_, params = build_data(self.apikey, 'store_exception',
+ exc_data_json='XXX,{')
+ response = api_call(self.app, params)
+
+ expected = 'Failed to parse JSON data from exc_data_json field. ' \
+ 'Please make sure it contains a valid JSON.'
+ assert_error(id_, expected, given=response.body)
+
+ def test_store_exception_missing_json_params_json(self):
+ id_, params = build_data(self.apikey, 'store_exception',
+ exc_data_json='{"foo":"bar"}')
+ response = api_call(self.app, params)
+
+ expected = "Missing exc_traceback, or exc_type_name in " \
+ "exc_data_json field. Missing: 'exc_traceback'"
+ assert_error(id_, expected, given=response.body)
+
+ def test_store_exception(self):
+ id_, params = build_data(
+ self.apikey, 'store_exception',
+ exc_data_json='{"exc_traceback": "invalid", "exc_type_name":"ValueError"}')
+ response = api_call(self.app, params)
+ exc_id = response.json['result']['exc_id']
+
+ expected = {
+ 'exc_id': exc_id,
+ 'exc_url': 'http://example.com/_admin/settings/exceptions/{}'.format(exc_id)
+ }
+ assert_ok(id_, expected, given=response.body)
diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py
--- a/rhodecode/api/views/server_api.py
+++ b/rhodecode/api/views/server_api.py
@@ -30,6 +30,8 @@ from rhodecode.api.utils import (
from rhodecode.lib.utils import repo2db_mapper
from rhodecode.lib import system_info
from rhodecode.lib import user_sessions
+from rhodecode.lib import exc_tracking
+from rhodecode.lib.ext_json import json
from rhodecode.lib.utils2 import safe_int
from rhodecode.model.db import UserIpMap
from rhodecode.model.scm import ScmModel
@@ -293,7 +295,7 @@ def get_method(request, apiuser, pattern
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param pattern: pattern to match method names against
- :type older_then: Optional("*")
+ :type pattern: Optional("*")
Example output:
@@ -349,3 +351,64 @@ def get_method(request, apiuser, pattern
args_desc.append(func_kwargs)
return matches.keys() + args_desc
+
+
+@jsonrpc_method()
+def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
+ """
+ Stores sent exception inside the built-in exception tracker in |RCE| server.
+
+ This command can only be run using an |authtoken| with admin rights to
+ the specified repository.
+
+ This command takes the following options:
+
+ :param apiuser: This is filled automatically from the |authtoken|.
+ :type apiuser: AuthUser
+
+ :param exc_data_json: JSON data with exception e.g
+ {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
+ :type exc_data_json: JSON data
+
+ :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
+ :type prefix: Optional("rhodecode")
+
+ Example output:
+
+ .. code-block:: bash
+
+ id :
+ "result": {
+ "exc_id": 139718459226384,
+ "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
+ }
+ error : null
+ """
+ if not has_superadmin_permission(apiuser):
+ raise JSONRPCForbidden()
+
+ prefix = Optional.extract(prefix)
+ exc_id = exc_tracking.generate_id()
+
+ try:
+ exc_data = json.loads(exc_data_json)
+ except Exception:
+ log.error('Failed to parse JSON: %r', exc_data_json)
+ raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
+ 'Please make sure it contains a valid JSON.')
+
+ try:
+ exc_traceback = exc_data['exc_traceback']
+ exc_type_name = exc_data['exc_type_name']
+ except KeyError as err:
+ raise JSONRPCError('Missing exc_traceback, or exc_type_name '
+ 'in exc_data_json field. Missing: {}'.format(err))
+
+ exc_tracking._store_exception(
+ exc_id=exc_id, exc_traceback=exc_traceback,
+ exc_type_name=exc_type_name, prefix=prefix)
+
+ exc_url = request.route_url(
+ 'admin_settings_exception_tracker_show', exception_id=exc_id)
+ return {'exc_id': exc_id, 'exc_url': exc_url}
+
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
@@ -67,14 +67,13 @@ def get_exc_store():
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))
+def _store_exception(exc_id, exc_type_name, exc_traceback, prefix):
+ """
+ Low level function to store exception in the exception tracker
+ """
- 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_data, org_data = exc_serialize(exc_id, exc_traceback, 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)
@@ -84,6 +83,16 @@ def _store_exception(exc_id, exc_info, p
log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
+def _prepare_exception(exc_info):
+ exc_type, exc_value, exc_traceback = exc_info
+ exc_type_name = exc_type.__name__
+
+ tb = ''.join(traceback.format_exception(
+ exc_type, exc_value, exc_traceback, None))
+
+ return exc_type_name, tb
+
+
def store_exception(exc_id, exc_info, prefix=global_prefix):
"""
Example usage::
@@ -93,7 +102,9 @@ def store_exception(exc_id, exc_info, pr
"""
try:
- _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
+ exc_type_name, exc_traceback = _prepare_exception(exc_info)
+ _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
+ exc_traceback=exc_traceback, 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.
@@ -149,3 +160,7 @@ def delete_exception(exc_id, prefix=glob
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
+
+
+def generate_id():
+ return id(object())