# HG changeset patch # User Marcin Kuzminski # Date 2018-12-18 11:05:03 # Node ID a029d28fe6dafaf72aa7d07d8809bd8c4daa8228 # Parent 899b726f59eb68bb7af3cb7f00b41dc962ec014c api: added store_exception_api for remote exception storage. - this will be used in the new indexer that could now report indexing error right back into RhodeCode. 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())