##// END OF EJS Templates
api: added store_exception_api for remote exception storage....
marcink -
r3317:a029d28f default
parent child Browse files
Show More
@@ -0,0 +1,59 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok, assert_error
25
26
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestStoreException(object):
29
30 def test_store_exception_invalid_json(self):
31 id_, params = build_data(self.apikey, 'store_exception',
32 exc_data_json='XXX,{')
33 response = api_call(self.app, params)
34
35 expected = 'Failed to parse JSON data from exc_data_json field. ' \
36 'Please make sure it contains a valid JSON.'
37 assert_error(id_, expected, given=response.body)
38
39 def test_store_exception_missing_json_params_json(self):
40 id_, params = build_data(self.apikey, 'store_exception',
41 exc_data_json='{"foo":"bar"}')
42 response = api_call(self.app, params)
43
44 expected = "Missing exc_traceback, or exc_type_name in " \
45 "exc_data_json field. Missing: 'exc_traceback'"
46 assert_error(id_, expected, given=response.body)
47
48 def test_store_exception(self):
49 id_, params = build_data(
50 self.apikey, 'store_exception',
51 exc_data_json='{"exc_traceback": "invalid", "exc_type_name":"ValueError"}')
52 response = api_call(self.app, params)
53 exc_id = response.json['result']['exc_id']
54
55 expected = {
56 'exc_id': exc_id,
57 'exc_url': 'http://example.com/_admin/settings/exceptions/{}'.format(exc_id)
58 }
59 assert_ok(id_, expected, given=response.body)
@@ -1,351 +1,414 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
21 import inspect
22 import logging
22 import logging
23 import itertools
23 import itertools
24
24
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
27
27
28 from rhodecode.api.utils import (
28 from rhodecode.api.utils import (
29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
30 from rhodecode.lib.utils import repo2db_mapper
30 from rhodecode.lib.utils import repo2db_mapper
31 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
32 from rhodecode.lib import user_sessions
32 from rhodecode.lib import user_sessions
33 from rhodecode.lib import exc_tracking
34 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.model.db import UserIpMap
36 from rhodecode.model.db import UserIpMap
35 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
36 from rhodecode.model.settings import VcsSettingsModel
38 from rhodecode.model.settings import VcsSettingsModel
37
39
38 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
39
41
40
42
41 @jsonrpc_method()
43 @jsonrpc_method()
42 def get_server_info(request, apiuser):
44 def get_server_info(request, apiuser):
43 """
45 """
44 Returns the |RCE| server information.
46 Returns the |RCE| server information.
45
47
46 This includes the running version of |RCE| and all installed
48 This includes the running version of |RCE| and all installed
47 packages. This command takes the following options:
49 packages. This command takes the following options:
48
50
49 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
52 :type apiuser: AuthUser
51
53
52 Example output:
54 Example output:
53
55
54 .. code-block:: bash
56 .. code-block:: bash
55
57
56 id : <id_given_in_input>
58 id : <id_given_in_input>
57 result : {
59 result : {
58 'modules': [<module name>,...]
60 'modules': [<module name>,...]
59 'py_version': <python version>,
61 'py_version': <python version>,
60 'platform': <platform type>,
62 'platform': <platform type>,
61 'rhodecode_version': <rhodecode version>
63 'rhodecode_version': <rhodecode version>
62 }
64 }
63 error : null
65 error : null
64 """
66 """
65
67
66 if not has_superadmin_permission(apiuser):
68 if not has_superadmin_permission(apiuser):
67 raise JSONRPCForbidden()
69 raise JSONRPCForbidden()
68
70
69 server_info = ScmModel().get_server_info(request.environ)
71 server_info = ScmModel().get_server_info(request.environ)
70 # rhodecode-index requires those
72 # rhodecode-index requires those
71
73
72 server_info['index_storage'] = server_info['search']['value']['location']
74 server_info['index_storage'] = server_info['search']['value']['location']
73 server_info['storage'] = server_info['storage']['value']['path']
75 server_info['storage'] = server_info['storage']['value']['path']
74
76
75 return server_info
77 return server_info
76
78
77
79
78 @jsonrpc_method()
80 @jsonrpc_method()
79 def get_repo_store(request, apiuser):
81 def get_repo_store(request, apiuser):
80 """
82 """
81 Returns the |RCE| repository storage information.
83 Returns the |RCE| repository storage information.
82
84
83 :param apiuser: This is filled automatically from the |authtoken|.
85 :param apiuser: This is filled automatically from the |authtoken|.
84 :type apiuser: AuthUser
86 :type apiuser: AuthUser
85
87
86 Example output:
88 Example output:
87
89
88 .. code-block:: bash
90 .. code-block:: bash
89
91
90 id : <id_given_in_input>
92 id : <id_given_in_input>
91 result : {
93 result : {
92 'modules': [<module name>,...]
94 'modules': [<module name>,...]
93 'py_version': <python version>,
95 'py_version': <python version>,
94 'platform': <platform type>,
96 'platform': <platform type>,
95 'rhodecode_version': <rhodecode version>
97 'rhodecode_version': <rhodecode version>
96 }
98 }
97 error : null
99 error : null
98 """
100 """
99
101
100 if not has_superadmin_permission(apiuser):
102 if not has_superadmin_permission(apiuser):
101 raise JSONRPCForbidden()
103 raise JSONRPCForbidden()
102
104
103 path = VcsSettingsModel().get_repos_location()
105 path = VcsSettingsModel().get_repos_location()
104 return {"path": path}
106 return {"path": path}
105
107
106
108
107 @jsonrpc_method()
109 @jsonrpc_method()
108 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
109 """
111 """
110 Displays the IP Address as seen from the |RCE| server.
112 Displays the IP Address as seen from the |RCE| server.
111
113
112 * This command displays the IP Address, as well as all the defined IP
114 * This command displays the IP Address, as well as all the defined IP
113 addresses for the specified user. If the ``userid`` is not set, the
115 addresses for the specified user. If the ``userid`` is not set, the
114 data returned is for the user calling the method.
116 data returned is for the user calling the method.
115
117
116 This command can only be run using an |authtoken| with admin rights to
118 This command can only be run using an |authtoken| with admin rights to
117 the specified repository.
119 the specified repository.
118
120
119 This command takes the following options:
121 This command takes the following options:
120
122
121 :param apiuser: This is filled automatically from |authtoken|.
123 :param apiuser: This is filled automatically from |authtoken|.
122 :type apiuser: AuthUser
124 :type apiuser: AuthUser
123 :param userid: Sets the userid for which associated IP Address data
125 :param userid: Sets the userid for which associated IP Address data
124 is returned.
126 is returned.
125 :type userid: Optional(str or int)
127 :type userid: Optional(str or int)
126
128
127 Example output:
129 Example output:
128
130
129 .. code-block:: bash
131 .. code-block:: bash
130
132
131 id : <id_given_in_input>
133 id : <id_given_in_input>
132 result : {
134 result : {
133 "server_ip_addr": "<ip_from_clien>",
135 "server_ip_addr": "<ip_from_clien>",
134 "user_ips": [
136 "user_ips": [
135 {
137 {
136 "ip_addr": "<ip_with_mask>",
138 "ip_addr": "<ip_with_mask>",
137 "ip_range": ["<start_ip>", "<end_ip>"],
139 "ip_range": ["<start_ip>", "<end_ip>"],
138 },
140 },
139 ...
141 ...
140 ]
142 ]
141 }
143 }
142
144
143 """
145 """
144 if not has_superadmin_permission(apiuser):
146 if not has_superadmin_permission(apiuser):
145 raise JSONRPCForbidden()
147 raise JSONRPCForbidden()
146
148
147 userid = Optional.extract(userid, evaluate_locals=locals())
149 userid = Optional.extract(userid, evaluate_locals=locals())
148 userid = getattr(userid, 'user_id', userid)
150 userid = getattr(userid, 'user_id', userid)
149
151
150 user = get_user_or_error(userid)
152 user = get_user_or_error(userid)
151 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
152 return {
154 return {
153 'server_ip_addr': request.rpc_ip_addr,
155 'server_ip_addr': request.rpc_ip_addr,
154 'user_ips': ips
156 'user_ips': ips
155 }
157 }
156
158
157
159
158 @jsonrpc_method()
160 @jsonrpc_method()
159 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
160 """
162 """
161 Triggers a rescan of the specified repositories.
163 Triggers a rescan of the specified repositories.
162
164
163 * If the ``remove_obsolete`` option is set, it also deletes repositories
165 * If the ``remove_obsolete`` option is set, it also deletes repositories
164 that are found in the database but not on the file system, so called
166 that are found in the database but not on the file system, so called
165 "clean zombies".
167 "clean zombies".
166
168
167 This command can only be run using an |authtoken| with admin rights to
169 This command can only be run using an |authtoken| with admin rights to
168 the specified repository.
170 the specified repository.
169
171
170 This command takes the following options:
172 This command takes the following options:
171
173
172 :param apiuser: This is filled automatically from the |authtoken|.
174 :param apiuser: This is filled automatically from the |authtoken|.
173 :type apiuser: AuthUser
175 :type apiuser: AuthUser
174 :param remove_obsolete: Deletes repositories from the database that
176 :param remove_obsolete: Deletes repositories from the database that
175 are not found on the filesystem.
177 are not found on the filesystem.
176 :type remove_obsolete: Optional(``True`` | ``False``)
178 :type remove_obsolete: Optional(``True`` | ``False``)
177
179
178 Example output:
180 Example output:
179
181
180 .. code-block:: bash
182 .. code-block:: bash
181
183
182 id : <id_given_in_input>
184 id : <id_given_in_input>
183 result : {
185 result : {
184 'added': [<added repository name>,...]
186 'added': [<added repository name>,...]
185 'removed': [<removed repository name>,...]
187 'removed': [<removed repository name>,...]
186 }
188 }
187 error : null
189 error : null
188
190
189 Example error output:
191 Example error output:
190
192
191 .. code-block:: bash
193 .. code-block:: bash
192
194
193 id : <id_given_in_input>
195 id : <id_given_in_input>
194 result : null
196 result : null
195 error : {
197 error : {
196 'Error occurred during rescan repositories action'
198 'Error occurred during rescan repositories action'
197 }
199 }
198
200
199 """
201 """
200 if not has_superadmin_permission(apiuser):
202 if not has_superadmin_permission(apiuser):
201 raise JSONRPCForbidden()
203 raise JSONRPCForbidden()
202
204
203 try:
205 try:
204 rm_obsolete = Optional.extract(remove_obsolete)
206 rm_obsolete = Optional.extract(remove_obsolete)
205 added, removed = repo2db_mapper(ScmModel().repo_scan(),
207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
206 remove_obsolete=rm_obsolete)
208 remove_obsolete=rm_obsolete)
207 return {'added': added, 'removed': removed}
209 return {'added': added, 'removed': removed}
208 except Exception:
210 except Exception:
209 log.exception('Failed to run repo rescann')
211 log.exception('Failed to run repo rescann')
210 raise JSONRPCError(
212 raise JSONRPCError(
211 'Error occurred during rescan repositories action'
213 'Error occurred during rescan repositories action'
212 )
214 )
213
215
214
216
215 @jsonrpc_method()
217 @jsonrpc_method()
216 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
217 """
219 """
218 Triggers a session cleanup action.
220 Triggers a session cleanup action.
219
221
220 If the ``older_then`` option is set, only sessions that hasn't been
222 If the ``older_then`` option is set, only sessions that hasn't been
221 accessed in the given number of days will be removed.
223 accessed in the given number of days will be removed.
222
224
223 This command can only be run using an |authtoken| with admin rights to
225 This command can only be run using an |authtoken| with admin rights to
224 the specified repository.
226 the specified repository.
225
227
226 This command takes the following options:
228 This command takes the following options:
227
229
228 :param apiuser: This is filled automatically from the |authtoken|.
230 :param apiuser: This is filled automatically from the |authtoken|.
229 :type apiuser: AuthUser
231 :type apiuser: AuthUser
230 :param older_then: Deletes session that hasn't been accessed
232 :param older_then: Deletes session that hasn't been accessed
231 in given number of days.
233 in given number of days.
232 :type older_then: Optional(int)
234 :type older_then: Optional(int)
233
235
234 Example output:
236 Example output:
235
237
236 .. code-block:: bash
238 .. code-block:: bash
237
239
238 id : <id_given_in_input>
240 id : <id_given_in_input>
239 result: {
241 result: {
240 "backend": "<type of backend>",
242 "backend": "<type of backend>",
241 "sessions_removed": <number_of_removed_sessions>
243 "sessions_removed": <number_of_removed_sessions>
242 }
244 }
243 error : null
245 error : null
244
246
245 Example error output:
247 Example error output:
246
248
247 .. code-block:: bash
249 .. code-block:: bash
248
250
249 id : <id_given_in_input>
251 id : <id_given_in_input>
250 result : null
252 result : null
251 error : {
253 error : {
252 'Error occurred during session cleanup'
254 'Error occurred during session cleanup'
253 }
255 }
254
256
255 """
257 """
256 if not has_superadmin_permission(apiuser):
258 if not has_superadmin_permission(apiuser):
257 raise JSONRPCForbidden()
259 raise JSONRPCForbidden()
258
260
259 older_then = safe_int(Optional.extract(older_then)) or 60
261 older_then = safe_int(Optional.extract(older_then)) or 60
260 older_than_seconds = 60 * 60 * 24 * older_then
262 older_than_seconds = 60 * 60 * 24 * older_then
261
263
262 config = system_info.rhodecode_config().get_value()['value']['config']
264 config = system_info.rhodecode_config().get_value()['value']['config']
263 session_model = user_sessions.get_session_handler(
265 session_model = user_sessions.get_session_handler(
264 config.get('beaker.session.type', 'memory'))(config)
266 config.get('beaker.session.type', 'memory'))(config)
265
267
266 backend = session_model.SESSION_TYPE
268 backend = session_model.SESSION_TYPE
267 try:
269 try:
268 cleaned = session_model.clean_sessions(
270 cleaned = session_model.clean_sessions(
269 older_than_seconds=older_than_seconds)
271 older_than_seconds=older_than_seconds)
270 return {'sessions_removed': cleaned, 'backend': backend}
272 return {'sessions_removed': cleaned, 'backend': backend}
271 except user_sessions.CleanupCommand as msg:
273 except user_sessions.CleanupCommand as msg:
272 return {'cleanup_command': msg.message, 'backend': backend}
274 return {'cleanup_command': msg.message, 'backend': backend}
273 except Exception as e:
275 except Exception as e:
274 log.exception('Failed session cleanup')
276 log.exception('Failed session cleanup')
275 raise JSONRPCError(
277 raise JSONRPCError(
276 'Error occurred during session cleanup'
278 'Error occurred during session cleanup'
277 )
279 )
278
280
279
281
280 @jsonrpc_method()
282 @jsonrpc_method()
281 def get_method(request, apiuser, pattern=Optional('*')):
283 def get_method(request, apiuser, pattern=Optional('*')):
282 """
284 """
283 Returns list of all available API methods. By default match pattern
285 Returns list of all available API methods. By default match pattern
284 os "*" but any other pattern can be specified. eg *comment* will return
286 os "*" but any other pattern can be specified. eg *comment* will return
285 all methods with comment inside them. If just single method is matched
287 all methods with comment inside them. If just single method is matched
286 returned data will also include method specification
288 returned data will also include method specification
287
289
288 This command can only be run using an |authtoken| with admin rights to
290 This command can only be run using an |authtoken| with admin rights to
289 the specified repository.
291 the specified repository.
290
292
291 This command takes the following options:
293 This command takes the following options:
292
294
293 :param apiuser: This is filled automatically from the |authtoken|.
295 :param apiuser: This is filled automatically from the |authtoken|.
294 :type apiuser: AuthUser
296 :type apiuser: AuthUser
295 :param pattern: pattern to match method names against
297 :param pattern: pattern to match method names against
296 :type older_then: Optional("*")
298 :type pattern: Optional("*")
297
299
298 Example output:
300 Example output:
299
301
300 .. code-block:: bash
302 .. code-block:: bash
301
303
302 id : <id_given_in_input>
304 id : <id_given_in_input>
303 "result": [
305 "result": [
304 "changeset_comment",
306 "changeset_comment",
305 "comment_pull_request",
307 "comment_pull_request",
306 "comment_commit"
308 "comment_commit"
307 ]
309 ]
308 error : null
310 error : null
309
311
310 .. code-block:: bash
312 .. code-block:: bash
311
313
312 id : <id_given_in_input>
314 id : <id_given_in_input>
313 "result": [
315 "result": [
314 "comment_commit",
316 "comment_commit",
315 {
317 {
316 "apiuser": "<RequiredType>",
318 "apiuser": "<RequiredType>",
317 "comment_type": "<Optional:u'note'>",
319 "comment_type": "<Optional:u'note'>",
318 "commit_id": "<RequiredType>",
320 "commit_id": "<RequiredType>",
319 "message": "<RequiredType>",
321 "message": "<RequiredType>",
320 "repoid": "<RequiredType>",
322 "repoid": "<RequiredType>",
321 "request": "<RequiredType>",
323 "request": "<RequiredType>",
322 "resolves_comment_id": "<Optional:None>",
324 "resolves_comment_id": "<Optional:None>",
323 "status": "<Optional:None>",
325 "status": "<Optional:None>",
324 "userid": "<Optional:<OptionalAttr:apiuser>>"
326 "userid": "<Optional:<OptionalAttr:apiuser>>"
325 }
327 }
326 ]
328 ]
327 error : null
329 error : null
328 """
330 """
329 if not has_superadmin_permission(apiuser):
331 if not has_superadmin_permission(apiuser):
330 raise JSONRPCForbidden()
332 raise JSONRPCForbidden()
331
333
332 pattern = Optional.extract(pattern)
334 pattern = Optional.extract(pattern)
333
335
334 matches = find_methods(request.registry.jsonrpc_methods, pattern)
336 matches = find_methods(request.registry.jsonrpc_methods, pattern)
335
337
336 args_desc = []
338 args_desc = []
337 if len(matches) == 1:
339 if len(matches) == 1:
338 func = matches[matches.keys()[0]]
340 func = matches[matches.keys()[0]]
339
341
340 argspec = inspect.getargspec(func)
342 argspec = inspect.getargspec(func)
341 arglist = argspec[0]
343 arglist = argspec[0]
342 defaults = map(repr, argspec[3] or [])
344 defaults = map(repr, argspec[3] or [])
343
345
344 default_empty = '<RequiredType>'
346 default_empty = '<RequiredType>'
345
347
346 # kw arguments required by this method
348 # kw arguments required by this method
347 func_kwargs = dict(itertools.izip_longest(
349 func_kwargs = dict(itertools.izip_longest(
348 reversed(arglist), reversed(defaults), fillvalue=default_empty))
350 reversed(arglist), reversed(defaults), fillvalue=default_empty))
349 args_desc.append(func_kwargs)
351 args_desc.append(func_kwargs)
350
352
351 return matches.keys() + args_desc
353 return matches.keys() + args_desc
354
355
356 @jsonrpc_method()
357 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
358 """
359 Stores sent exception inside the built-in exception tracker in |RCE| server.
360
361 This command can only be run using an |authtoken| with admin rights to
362 the specified repository.
363
364 This command takes the following options:
365
366 :param apiuser: This is filled automatically from the |authtoken|.
367 :type apiuser: AuthUser
368
369 :param exc_data_json: JSON data with exception e.g
370 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
371 :type exc_data_json: JSON data
372
373 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
374 :type prefix: Optional("rhodecode")
375
376 Example output:
377
378 .. code-block:: bash
379
380 id : <id_given_in_input>
381 "result": {
382 "exc_id": 139718459226384,
383 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
384 }
385 error : null
386 """
387 if not has_superadmin_permission(apiuser):
388 raise JSONRPCForbidden()
389
390 prefix = Optional.extract(prefix)
391 exc_id = exc_tracking.generate_id()
392
393 try:
394 exc_data = json.loads(exc_data_json)
395 except Exception:
396 log.error('Failed to parse JSON: %r', exc_data_json)
397 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
398 'Please make sure it contains a valid JSON.')
399
400 try:
401 exc_traceback = exc_data['exc_traceback']
402 exc_type_name = exc_data['exc_type_name']
403 except KeyError as err:
404 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
405 'in exc_data_json field. Missing: {}'.format(err))
406
407 exc_tracking._store_exception(
408 exc_id=exc_id, exc_traceback=exc_traceback,
409 exc_type_name=exc_type_name, prefix=prefix)
410
411 exc_url = request.route_url(
412 'admin_settings_exception_tracker_show', exception_id=exc_id)
413 return {'exc_id': exc_id, 'exc_url': exc_url}
414
@@ -1,151 +1,166 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import time
22 import time
23 import datetime
23 import datetime
24 import msgpack
24 import msgpack
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import tempfile
27 import tempfile
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 global_prefix = 'rhodecode'
33 global_prefix = 'rhodecode'
34 exc_store_dir_name = 'rc_exception_store_v1'
34 exc_store_dir_name = 'rc_exception_store_v1'
35
35
36
36
37 def exc_serialize(exc_id, tb, exc_type):
37 def exc_serialize(exc_id, tb, exc_type):
38
38
39 data = {
39 data = {
40 'version': 'v1',
40 'version': 'v1',
41 'exc_id': exc_id,
41 'exc_id': exc_id,
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 'exc_timestamp': repr(time.time()),
43 'exc_timestamp': repr(time.time()),
44 'exc_message': tb,
44 'exc_message': tb,
45 'exc_type': exc_type,
45 'exc_type': exc_type,
46 }
46 }
47 return msgpack.packb(data), data
47 return msgpack.packb(data), data
48
48
49
49
50 def exc_unserialize(tb):
50 def exc_unserialize(tb):
51 return msgpack.unpackb(tb)
51 return msgpack.unpackb(tb)
52
52
53
53
54 def get_exc_store():
54 def get_exc_store():
55 """
55 """
56 Get and create exception store if it's not existing
56 Get and create exception store if it's not existing
57 """
57 """
58 import rhodecode as app
58 import rhodecode as app
59
59
60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
62
62
63 _exc_store_path = os.path.abspath(_exc_store_path)
63 _exc_store_path = os.path.abspath(_exc_store_path)
64 if not os.path.isdir(_exc_store_path):
64 if not os.path.isdir(_exc_store_path):
65 os.makedirs(_exc_store_path)
65 os.makedirs(_exc_store_path)
66 log.debug('Initializing exceptions store at %s', _exc_store_path)
66 log.debug('Initializing exceptions store at %s', _exc_store_path)
67 return _exc_store_path
67 return _exc_store_path
68
68
69
69
70 def _store_exception(exc_id, exc_info, prefix):
70 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix):
71 exc_type, exc_value, exc_traceback = exc_info
71 """
72 tb = ''.join(traceback.format_exception(
72 Low level function to store exception in the exception tracker
73 exc_type, exc_value, exc_traceback, None))
73 """
74
74
75 exc_type_name = exc_type.__name__
76 exc_store_path = get_exc_store()
75 exc_store_path = get_exc_store()
77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
76 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name)
78 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
77 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
79 if not os.path.isdir(exc_store_path):
78 if not os.path.isdir(exc_store_path):
80 os.makedirs(exc_store_path)
79 os.makedirs(exc_store_path)
81 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
80 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
82 with open(stored_exc_path, 'wb') as f:
81 with open(stored_exc_path, 'wb') as f:
83 f.write(exc_data)
82 f.write(exc_data)
84 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
83 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
85
84
86
85
86 def _prepare_exception(exc_info):
87 exc_type, exc_value, exc_traceback = exc_info
88 exc_type_name = exc_type.__name__
89
90 tb = ''.join(traceback.format_exception(
91 exc_type, exc_value, exc_traceback, None))
92
93 return exc_type_name, tb
94
95
87 def store_exception(exc_id, exc_info, prefix=global_prefix):
96 def store_exception(exc_id, exc_info, prefix=global_prefix):
88 """
97 """
89 Example usage::
98 Example usage::
90
99
91 exc_info = sys.exc_info()
100 exc_info = sys.exc_info()
92 store_exception(id(exc_info), exc_info)
101 store_exception(id(exc_info), exc_info)
93 """
102 """
94
103
95 try:
104 try:
96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
105 exc_type_name, exc_traceback = _prepare_exception(exc_info)
106 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
107 exc_traceback=exc_traceback, prefix=prefix)
97 except Exception:
108 except Exception:
98 log.exception('Failed to store exception `%s` information', exc_id)
109 log.exception('Failed to store exception `%s` information', exc_id)
99 # there's no way this can fail, it will crash server badly if it does.
110 # there's no way this can fail, it will crash server badly if it does.
100 pass
111 pass
101
112
102
113
103 def _find_exc_file(exc_id, prefix=global_prefix):
114 def _find_exc_file(exc_id, prefix=global_prefix):
104 exc_store_path = get_exc_store()
115 exc_store_path = get_exc_store()
105 if prefix:
116 if prefix:
106 exc_id = '{}_{}'.format(exc_id, prefix)
117 exc_id = '{}_{}'.format(exc_id, prefix)
107 else:
118 else:
108 # search without a prefix
119 # search without a prefix
109 exc_id = '{}'.format(exc_id)
120 exc_id = '{}'.format(exc_id)
110
121
111 # we need to search the store for such start pattern as above
122 # we need to search the store for such start pattern as above
112 for fname in os.listdir(exc_store_path):
123 for fname in os.listdir(exc_store_path):
113 if fname.startswith(exc_id):
124 if fname.startswith(exc_id):
114 exc_id = os.path.join(exc_store_path, fname)
125 exc_id = os.path.join(exc_store_path, fname)
115 break
126 break
116 continue
127 continue
117 else:
128 else:
118 exc_id = None
129 exc_id = None
119
130
120 return exc_id
131 return exc_id
121
132
122
133
123 def _read_exception(exc_id, prefix):
134 def _read_exception(exc_id, prefix):
124 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
135 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
125 if exc_id_file_path:
136 if exc_id_file_path:
126 with open(exc_id_file_path, 'rb') as f:
137 with open(exc_id_file_path, 'rb') as f:
127 return exc_unserialize(f.read())
138 return exc_unserialize(f.read())
128 else:
139 else:
129 log.debug('Exception File `%s` not found', exc_id_file_path)
140 log.debug('Exception File `%s` not found', exc_id_file_path)
130 return None
141 return None
131
142
132
143
133 def read_exception(exc_id, prefix=global_prefix):
144 def read_exception(exc_id, prefix=global_prefix):
134 try:
145 try:
135 return _read_exception(exc_id=exc_id, prefix=prefix)
146 return _read_exception(exc_id=exc_id, prefix=prefix)
136 except Exception:
147 except Exception:
137 log.exception('Failed to read exception `%s` information', exc_id)
148 log.exception('Failed to read exception `%s` information', exc_id)
138 # there's no way this can fail, it will crash server badly if it does.
149 # there's no way this can fail, it will crash server badly if it does.
139 return None
150 return None
140
151
141
152
142 def delete_exception(exc_id, prefix=global_prefix):
153 def delete_exception(exc_id, prefix=global_prefix):
143 try:
154 try:
144 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
155 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
145 if exc_id_file_path:
156 if exc_id_file_path:
146 os.remove(exc_id_file_path)
157 os.remove(exc_id_file_path)
147
158
148 except Exception:
159 except Exception:
149 log.exception('Failed to remove exception `%s` information', exc_id)
160 log.exception('Failed to remove exception `%s` information', exc_id)
150 # there's no way this can fail, it will crash server badly if it does.
161 # there's no way this can fail, it will crash server badly if it does.
151 pass
162 pass
163
164
165 def generate_id():
166 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now