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