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