##// END OF EJS Templates
exception-tracker: send exc id headers on failed API calls for tracking errors that server generated.
marcink -
r4112:1f5dcb79 default
parent child Browse files
Show More
@@ -1,549 +1,554 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import inspect
22 22 import itertools
23 23 import logging
24 24 import sys
25 25 import types
26 26 import fnmatch
27 27
28 28 import decorator
29 29 import venusian
30 30 from collections import OrderedDict
31 31
32 32 from pyramid.exceptions import ConfigurationError
33 33 from pyramid.renderers import render
34 34 from pyramid.response import Response
35 35 from pyramid.httpexceptions import HTTPNotFound
36 36
37 37 from rhodecode.api.exc import (
38 38 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
39 39 from rhodecode.apps._base import TemplateArgs
40 40 from rhodecode.lib.auth import AuthUser
41 41 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
42 42 from rhodecode.lib.exc_tracking import store_exception
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.utils2 import safe_str
45 45 from rhodecode.lib.plugins.utils import get_plugin_settings
46 46 from rhodecode.model.db import User, UserApiKeys
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50 DEFAULT_RENDERER = 'jsonrpc_renderer'
51 51 DEFAULT_URL = '/_admin/apiv2'
52 52
53 53
54 54 def find_methods(jsonrpc_methods, pattern):
55 55 matches = OrderedDict()
56 56 if not isinstance(pattern, (list, tuple)):
57 57 pattern = [pattern]
58 58
59 59 for single_pattern in pattern:
60 60 for method_name, method in jsonrpc_methods.items():
61 61 if fnmatch.fnmatch(method_name, single_pattern):
62 62 matches[method_name] = method
63 63 return matches
64 64
65 65
66 66 class ExtJsonRenderer(object):
67 67 """
68 68 Custom renderer that mkaes use of our ext_json lib
69 69
70 70 """
71 71
72 72 def __init__(self, serializer=json.dumps, **kw):
73 73 """ Any keyword arguments will be passed to the ``serializer``
74 74 function."""
75 75 self.serializer = serializer
76 76 self.kw = kw
77 77
78 78 def __call__(self, info):
79 79 """ Returns a plain JSON-encoded string with content-type
80 80 ``application/json``. The content-type may be overridden by
81 81 setting ``request.response.content_type``."""
82 82
83 83 def _render(value, system):
84 84 request = system.get('request')
85 85 if request is not None:
86 86 response = request.response
87 87 ct = response.content_type
88 88 if ct == response.default_content_type:
89 89 response.content_type = 'application/json'
90 90
91 91 return self.serializer(value, **self.kw)
92 92
93 93 return _render
94 94
95 95
96 96 def jsonrpc_response(request, result):
97 97 rpc_id = getattr(request, 'rpc_id', None)
98 98 response = request.response
99 99
100 100 # store content_type before render is called
101 101 ct = response.content_type
102 102
103 103 ret_value = ''
104 104 if rpc_id:
105 105 ret_value = {
106 106 'id': rpc_id,
107 107 'result': result,
108 108 'error': None,
109 109 }
110 110
111 111 # fetch deprecation warnings, and store it inside results
112 112 deprecation = getattr(request, 'rpc_deprecation', None)
113 113 if deprecation:
114 114 ret_value['DEPRECATION_WARNING'] = deprecation
115 115
116 116 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
117 117 response.body = safe_str(raw_body, response.charset)
118 118
119 119 if ct == response.default_content_type:
120 120 response.content_type = 'application/json'
121 121
122 122 return response
123 123
124 124
125 def jsonrpc_error(request, message, retid=None, code=None):
125 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
126 126 """
127 127 Generate a Response object with a JSON-RPC error body
128 128
129 129 :param code:
130 130 :param retid:
131 131 :param message:
132 132 """
133 133 err_dict = {'id': retid, 'result': None, 'error': message}
134 134 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
135
135 136 return Response(
136 137 body=body,
137 138 status=code,
138 content_type='application/json'
139 content_type='application/json',
140 headerlist=headers
139 141 )
140 142
141 143
142 144 def exception_view(exc, request):
143 145 rpc_id = getattr(request, 'rpc_id', None)
144 146
145 147 if isinstance(exc, JSONRPCError):
146 148 fault_message = safe_str(exc.message)
147 149 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
148 150 elif isinstance(exc, JSONRPCValidationError):
149 151 colander_exc = exc.colander_exception
150 152 # TODO(marcink): think maybe of nicer way to serialize errors ?
151 153 fault_message = colander_exc.asdict()
152 154 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
153 155 elif isinstance(exc, JSONRPCForbidden):
154 156 fault_message = 'Access was denied to this resource.'
155 157 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
156 158 elif isinstance(exc, HTTPNotFound):
157 159 method = request.rpc_method
158 160 log.debug('json-rpc method `%s` not found in list of '
159 161 'api calls: %s, rpc_id:%s',
160 162 method, request.registry.jsonrpc_methods.keys(), rpc_id)
161 163
162 164 similar = 'none'
163 165 try:
164 166 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
165 167 similar_found = find_methods(
166 168 request.registry.jsonrpc_methods, similar_paterns)
167 169 similar = ', '.join(similar_found.keys()) or similar
168 170 except Exception:
169 171 # make the whole above block safe
170 172 pass
171 173
172 174 fault_message = "No such method: {}. Similar methods: {}".format(
173 175 method, similar)
174 176 else:
175 177 fault_message = 'undefined error'
176 178 exc_info = exc.exc_info()
177 179 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
178 180
179 181 return jsonrpc_error(request, fault_message, rpc_id)
180 182
181 183
182 184 def request_view(request):
183 185 """
184 186 Main request handling method. It handles all logic to call a specific
185 187 exposed method
186 188 """
187 189
188 190 # check if we can find this session using api_key, get_by_auth_token
189 191 # search not expired tokens only
190 192
191 193 try:
192 194 api_user = User.get_by_auth_token(request.rpc_api_key)
193 195
194 196 if api_user is None:
195 197 return jsonrpc_error(
196 198 request, retid=request.rpc_id, message='Invalid API KEY')
197 199
198 200 if not api_user.active:
199 201 return jsonrpc_error(
200 202 request, retid=request.rpc_id,
201 203 message='Request from this user not allowed')
202 204
203 205 # check if we are allowed to use this IP
204 206 auth_u = AuthUser(
205 207 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
206 208 if not auth_u.ip_allowed:
207 209 return jsonrpc_error(
208 210 request, retid=request.rpc_id,
209 211 message='Request from IP:%s not allowed' % (
210 212 request.rpc_ip_addr,))
211 213 else:
212 214 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
213 215
214 216 # register our auth-user
215 217 request.rpc_user = auth_u
216 218 request.environ['rc_auth_user_id'] = auth_u.user_id
217 219
218 220 # now check if token is valid for API
219 221 auth_token = request.rpc_api_key
220 222 token_match = api_user.authenticate_by_token(
221 223 auth_token, roles=[UserApiKeys.ROLE_API])
222 224 invalid_token = not token_match
223 225
224 226 log.debug('Checking if API KEY is valid with proper role')
225 227 if invalid_token:
226 228 return jsonrpc_error(
227 229 request, retid=request.rpc_id,
228 230 message='API KEY invalid or, has bad role for an API call')
229 231
230 232 except Exception:
231 233 log.exception('Error on API AUTH')
232 234 return jsonrpc_error(
233 235 request, retid=request.rpc_id, message='Invalid API KEY')
234 236
235 237 method = request.rpc_method
236 238 func = request.registry.jsonrpc_methods[method]
237 239
238 240 # now that we have a method, add request._req_params to
239 241 # self.kargs and dispatch control to WGIController
240 242 argspec = inspect.getargspec(func)
241 243 arglist = argspec[0]
242 244 defaults = map(type, argspec[3] or [])
243 245 default_empty = types.NotImplementedType
244 246
245 247 # kw arguments required by this method
246 248 func_kwargs = dict(itertools.izip_longest(
247 249 reversed(arglist), reversed(defaults), fillvalue=default_empty))
248 250
249 251 # This attribute will need to be first param of a method that uses
250 252 # api_key, which is translated to instance of user at that name
251 253 user_var = 'apiuser'
252 254 request_var = 'request'
253 255
254 256 for arg in [user_var, request_var]:
255 257 if arg not in arglist:
256 258 return jsonrpc_error(
257 259 request,
258 260 retid=request.rpc_id,
259 261 message='This method [%s] does not support '
260 262 'required parameter `%s`' % (func.__name__, arg))
261 263
262 264 # get our arglist and check if we provided them as args
263 265 for arg, default in func_kwargs.items():
264 266 if arg in [user_var, request_var]:
265 267 # user_var and request_var are pre-hardcoded parameters and we
266 268 # don't need to do any translation
267 269 continue
268 270
269 271 # skip the required param check if it's default value is
270 272 # NotImplementedType (default_empty)
271 273 if default == default_empty and arg not in request.rpc_params:
272 274 return jsonrpc_error(
273 275 request,
274 276 retid=request.rpc_id,
275 277 message=('Missing non optional `%s` arg in JSON DATA' % arg)
276 278 )
277 279
278 280 # sanitize extra passed arguments
279 281 for k in request.rpc_params.keys()[:]:
280 282 if k not in func_kwargs:
281 283 del request.rpc_params[k]
282 284
283 285 call_params = request.rpc_params
284 286 call_params.update({
285 287 'request': request,
286 288 'apiuser': auth_u
287 289 })
288 290
289 291 # register some common functions for usage
290 attach_context_attributes(
291 TemplateArgs(), request, request.rpc_user.user_id)
292 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
292 293
293 294 try:
294 295 ret_value = func(**call_params)
295 296 return jsonrpc_response(request, ret_value)
296 297 except JSONRPCBaseError:
297 298 raise
298 299 except Exception:
299 300 log.exception('Unhandled exception occurred on api call: %s', func)
300 301 exc_info = sys.exc_info()
301 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
302 exc_id, exc_type_name = store_exception(
303 id(exc_info), exc_info, prefix='rhodecode-api')
304 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
305 ('RhodeCode-Exception-Type', str(exc_type_name))]
302 306 return jsonrpc_error(
303 request, retid=request.rpc_id, message='Internal server error')
307 request, retid=request.rpc_id, message='Internal server error',
308 headers=error_headers)
304 309
305 310
306 311 def setup_request(request):
307 312 """
308 313 Parse a JSON-RPC request body. It's used inside the predicates method
309 314 to validate and bootstrap requests for usage in rpc calls.
310 315
311 316 We need to raise JSONRPCError here if we want to return some errors back to
312 317 user.
313 318 """
314 319
315 320 log.debug('Executing setup request: %r', request)
316 321 request.rpc_ip_addr = get_ip_addr(request.environ)
317 322 # TODO(marcink): deprecate GET at some point
318 323 if request.method not in ['POST', 'GET']:
319 324 log.debug('unsupported request method "%s"', request.method)
320 325 raise JSONRPCError(
321 326 'unsupported request method "%s". Please use POST' % request.method)
322 327
323 328 if 'CONTENT_LENGTH' not in request.environ:
324 329 log.debug("No Content-Length")
325 330 raise JSONRPCError("Empty body, No Content-Length in request")
326 331
327 332 else:
328 333 length = request.environ['CONTENT_LENGTH']
329 334 log.debug('Content-Length: %s', length)
330 335
331 336 if length == 0:
332 337 log.debug("Content-Length is 0")
333 338 raise JSONRPCError("Content-Length is 0")
334 339
335 340 raw_body = request.body
336 341 log.debug("Loading JSON body now")
337 342 try:
338 343 json_body = json.loads(raw_body)
339 344 except ValueError as e:
340 345 # catch JSON errors Here
341 346 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
342 347
343 348 request.rpc_id = json_body.get('id')
344 349 request.rpc_method = json_body.get('method')
345 350
346 351 # check required base parameters
347 352 try:
348 353 api_key = json_body.get('api_key')
349 354 if not api_key:
350 355 api_key = json_body.get('auth_token')
351 356
352 357 if not api_key:
353 358 raise KeyError('api_key or auth_token')
354 359
355 360 # TODO(marcink): support passing in token in request header
356 361
357 362 request.rpc_api_key = api_key
358 363 request.rpc_id = json_body['id']
359 364 request.rpc_method = json_body['method']
360 365 request.rpc_params = json_body['args'] \
361 366 if isinstance(json_body['args'], dict) else {}
362 367
363 368 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
364 369 except KeyError as e:
365 370 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
366 371
367 372 log.debug('setup complete, now handling method:%s rpcid:%s',
368 373 request.rpc_method, request.rpc_id, )
369 374
370 375
371 376 class RoutePredicate(object):
372 377 def __init__(self, val, config):
373 378 self.val = val
374 379
375 380 def text(self):
376 381 return 'jsonrpc route = %s' % self.val
377 382
378 383 phash = text
379 384
380 385 def __call__(self, info, request):
381 386 if self.val:
382 387 # potentially setup and bootstrap our call
383 388 setup_request(request)
384 389
385 390 # Always return True so that even if it isn't a valid RPC it
386 391 # will fall through to the underlaying handlers like notfound_view
387 392 return True
388 393
389 394
390 395 class NotFoundPredicate(object):
391 396 def __init__(self, val, config):
392 397 self.val = val
393 398 self.methods = config.registry.jsonrpc_methods
394 399
395 400 def text(self):
396 401 return 'jsonrpc method not found = {}.'.format(self.val)
397 402
398 403 phash = text
399 404
400 405 def __call__(self, info, request):
401 406 return hasattr(request, 'rpc_method')
402 407
403 408
404 409 class MethodPredicate(object):
405 410 def __init__(self, val, config):
406 411 self.method = val
407 412
408 413 def text(self):
409 414 return 'jsonrpc method = %s' % self.method
410 415
411 416 phash = text
412 417
413 418 def __call__(self, context, request):
414 419 # we need to explicitly return False here, so pyramid doesn't try to
415 420 # execute our view directly. We need our main handler to execute things
416 421 return getattr(request, 'rpc_method') == self.method
417 422
418 423
419 424 def add_jsonrpc_method(config, view, **kwargs):
420 425 # pop the method name
421 426 method = kwargs.pop('method', None)
422 427
423 428 if method is None:
424 429 raise ConfigurationError(
425 430 'Cannot register a JSON-RPC method without specifying the "method"')
426 431
427 432 # we define custom predicate, to enable to detect conflicting methods,
428 433 # those predicates are kind of "translation" from the decorator variables
429 434 # to internal predicates names
430 435
431 436 kwargs['jsonrpc_method'] = method
432 437
433 438 # register our view into global view store for validation
434 439 config.registry.jsonrpc_methods[method] = view
435 440
436 441 # we're using our main request_view handler, here, so each method
437 442 # has a unified handler for itself
438 443 config.add_view(request_view, route_name='apiv2', **kwargs)
439 444
440 445
441 446 class jsonrpc_method(object):
442 447 """
443 448 decorator that works similar to @add_view_config decorator,
444 449 but tailored for our JSON RPC
445 450 """
446 451
447 452 venusian = venusian # for testing injection
448 453
449 454 def __init__(self, method=None, **kwargs):
450 455 self.method = method
451 456 self.kwargs = kwargs
452 457
453 458 def __call__(self, wrapped):
454 459 kwargs = self.kwargs.copy()
455 460 kwargs['method'] = self.method or wrapped.__name__
456 461 depth = kwargs.pop('_depth', 0)
457 462
458 463 def callback(context, name, ob):
459 464 config = context.config.with_package(info.module)
460 465 config.add_jsonrpc_method(view=ob, **kwargs)
461 466
462 467 info = venusian.attach(wrapped, callback, category='pyramid',
463 468 depth=depth + 1)
464 469 if info.scope == 'class':
465 470 # ensure that attr is set if decorating a class method
466 471 kwargs.setdefault('attr', wrapped.__name__)
467 472
468 473 kwargs['_info'] = info.codeinfo # fbo action_method
469 474 return wrapped
470 475
471 476
472 477 class jsonrpc_deprecated_method(object):
473 478 """
474 479 Marks method as deprecated, adds log.warning, and inject special key to
475 480 the request variable to mark method as deprecated.
476 481 Also injects special docstring that extract_docs will catch to mark
477 482 method as deprecated.
478 483
479 484 :param use_method: specify which method should be used instead of
480 485 the decorated one
481 486
482 487 Use like::
483 488
484 489 @jsonrpc_method()
485 490 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
486 491 def old_func(request, apiuser, arg1, arg2):
487 492 ...
488 493 """
489 494
490 495 def __init__(self, use_method, deprecated_at_version):
491 496 self.use_method = use_method
492 497 self.deprecated_at_version = deprecated_at_version
493 498 self.deprecated_msg = ''
494 499
495 500 def __call__(self, func):
496 501 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
497 502 method=self.use_method)
498 503
499 504 docstring = """\n
500 505 .. deprecated:: {version}
501 506
502 507 {deprecation_message}
503 508
504 509 {original_docstring}
505 510 """
506 511 func.__doc__ = docstring.format(
507 512 version=self.deprecated_at_version,
508 513 deprecation_message=self.deprecated_msg,
509 514 original_docstring=func.__doc__)
510 515 return decorator.decorator(self.__wrapper, func)
511 516
512 517 def __wrapper(self, func, *fargs, **fkwargs):
513 518 log.warning('DEPRECATED API CALL on function %s, please '
514 519 'use `%s` instead', func, self.use_method)
515 520 # alter function docstring to mark as deprecated, this is picked up
516 521 # via fabric file that generates API DOC.
517 522 result = func(*fargs, **fkwargs)
518 523
519 524 request = fargs[0]
520 525 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
521 526 return result
522 527
523 528
524 529 def includeme(config):
525 530 plugin_module = 'rhodecode.api'
526 531 plugin_settings = get_plugin_settings(
527 532 plugin_module, config.registry.settings)
528 533
529 534 if not hasattr(config.registry, 'jsonrpc_methods'):
530 535 config.registry.jsonrpc_methods = OrderedDict()
531 536
532 537 # match filter by given method only
533 538 config.add_view_predicate('jsonrpc_method', MethodPredicate)
534 539 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
535 540
536 541 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
537 542 serializer=json.dumps, indent=4))
538 543 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
539 544
540 545 config.add_route_predicate(
541 546 'jsonrpc_call', RoutePredicate)
542 547
543 548 config.add_route(
544 549 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
545 550
546 551 config.scan(plugin_module, ignore='rhodecode.api.tests')
547 552 # register some exception handling view
548 553 config.add_view(exception_view, context=JSONRPCBaseError)
549 554 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,172 +1,173 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
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
31 31 log = logging.getLogger(__name__)
32 32
33 33 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
34 34 global_prefix = 'rhodecode'
35 35 exc_store_dir_name = 'rc_exception_store_v1'
36 36
37 37
38 38 def exc_serialize(exc_id, tb, exc_type):
39 39
40 40 data = {
41 41 'version': 'v1',
42 42 'exc_id': exc_id,
43 43 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
44 44 'exc_timestamp': repr(time.time()),
45 45 'exc_message': tb,
46 46 'exc_type': exc_type,
47 47 }
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 _exc_store = None
55 55
56 56
57 57 def get_exc_store():
58 58 """
59 59 Get and create exception store if it's not existing
60 60 """
61 61 global _exc_store
62 62 import rhodecode as app
63 63
64 64 if _exc_store is not None:
65 65 # quick global cache
66 66 return _exc_store
67 67
68 68 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
69 69 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
70 70
71 71 _exc_store_path = os.path.abspath(_exc_store_path)
72 72 if not os.path.isdir(_exc_store_path):
73 73 os.makedirs(_exc_store_path)
74 74 log.debug('Initializing exceptions store at %s', _exc_store_path)
75 75 _exc_store = _exc_store_path
76 76
77 77 return _exc_store_path
78 78
79 79
80 80 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix):
81 81 """
82 82 Low level function to store exception in the exception tracker
83 83 """
84 84
85 85 exc_store_path = get_exc_store()
86 86 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name)
87 87 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
88 88 if not os.path.isdir(exc_store_path):
89 89 os.makedirs(exc_store_path)
90 90 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
91 91 with open(stored_exc_path, 'wb') as f:
92 92 f.write(exc_data)
93 93 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
94 94
95 95
96 96 def _prepare_exception(exc_info):
97 97 exc_type, exc_value, exc_traceback = exc_info
98 98 exc_type_name = exc_type.__name__
99 99
100 100 tb = ''.join(traceback.format_exception(
101 101 exc_type, exc_value, exc_traceback, None))
102 102
103 103 return exc_type_name, tb
104 104
105 105
106 106 def store_exception(exc_id, exc_info, prefix=global_prefix):
107 107 """
108 108 Example usage::
109 109
110 110 exc_info = sys.exc_info()
111 111 store_exception(id(exc_info), exc_info)
112 112 """
113 113
114 114 try:
115 115 exc_type_name, exc_traceback = _prepare_exception(exc_info)
116 116 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
117 117 exc_traceback=exc_traceback, prefix=prefix)
118 return exc_id, exc_type_name
118 119 except Exception:
119 120 log.exception('Failed to store exception `%s` information', exc_id)
120 121 # there's no way this can fail, it will crash server badly if it does.
121 122 pass
122 123
123 124
124 125 def _find_exc_file(exc_id, prefix=global_prefix):
125 126 exc_store_path = get_exc_store()
126 127 if prefix:
127 128 exc_id = '{}_{}'.format(exc_id, prefix)
128 129 else:
129 130 # search without a prefix
130 131 exc_id = '{}'.format(exc_id)
131 132
132 133 found_exc_id = None
133 134 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
134 135 if matches:
135 136 found_exc_id = matches[0]
136 137
137 138 return found_exc_id
138 139
139 140
140 141 def _read_exception(exc_id, prefix):
141 142 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
142 143 if exc_id_file_path:
143 144 with open(exc_id_file_path, 'rb') as f:
144 145 return exc_unserialize(f.read())
145 146 else:
146 147 log.debug('Exception File `%s` not found', exc_id_file_path)
147 148 return None
148 149
149 150
150 151 def read_exception(exc_id, prefix=global_prefix):
151 152 try:
152 153 return _read_exception(exc_id=exc_id, prefix=prefix)
153 154 except Exception:
154 155 log.exception('Failed to read exception `%s` information', exc_id)
155 156 # there's no way this can fail, it will crash server badly if it does.
156 157 return None
157 158
158 159
159 160 def delete_exception(exc_id, prefix=global_prefix):
160 161 try:
161 162 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
162 163 if exc_id_file_path:
163 164 os.remove(exc_id_file_path)
164 165
165 166 except Exception:
166 167 log.exception('Failed to remove exception `%s` information', exc_id)
167 168 # there's no way this can fail, it will crash server badly if it does.
168 169 pass
169 170
170 171
171 172 def generate_id():
172 173 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now