##// END OF EJS Templates
exc_tracking: always nice format tb for core exceptions
super-admin -
r5127:23926495 default
parent child Browse files
Show More
@@ -1,629 +1,630 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 os
20 20 import sys
21 21 import collections
22 22 import tempfile
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.authorization import ACLAuthorizationPolicy
30 30 from pyramid.config import Configurator
31 31 from pyramid.settings import asbool, aslist
32 32 from pyramid.httpexceptions import (
33 33 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 34 from pyramid.renderers import render_to_response
35 35
36 36 from rhodecode.model import meta
37 37 from rhodecode.config import patches
38 38 from rhodecode.config import utils as config_utils
39 39 from rhodecode.config.settings_maker import SettingsMaker
40 40 from rhodecode.config.environment import load_pyramid_environment
41 41
42 42 import rhodecode.events
43 43 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 44 from rhodecode.lib.request import Request
45 45 from rhodecode.lib.vcs import VCSCommunicationError
46 46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 50 from rhodecode.lib.utils2 import AttributeDict
51 from rhodecode.lib.exc_tracking import store_exception
51 from rhodecode.lib.exc_tracking import store_exception, format_exc
52 52 from rhodecode.subscribers import (
53 53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 54 write_metadata_if_needed, write_usage_data)
55 55 from rhodecode.lib.statsd_client import StatsdClient
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 def is_http_error(response):
61 61 # error which should have traceback
62 62 return response.status_code > 499
63 63
64 64
65 65 def should_load_all():
66 66 """
67 67 Returns if all application components should be loaded. In some cases it's
68 68 desired to skip apps loading for faster shell script execution
69 69 """
70 70 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
71 71 if ssh_cmd:
72 72 return False
73 73
74 74 return True
75 75
76 76
77 77 def make_pyramid_app(global_config, **settings):
78 78 """
79 79 Constructs the WSGI application based on Pyramid.
80 80
81 81 Specials:
82 82
83 83 * The application can also be integrated like a plugin via the call to
84 84 `includeme`. This is accompanied with the other utility functions which
85 85 are called. Changing this should be done with great care to not break
86 86 cases when these fragments are assembled from another place.
87 87
88 88 """
89 89 start_time = time.time()
90 90 log.info('Pyramid app config starting')
91 91
92 92 sanitize_settings_and_apply_defaults(global_config, settings)
93 93
94 94 # init and bootstrap StatsdClient
95 95 StatsdClient.setup(settings)
96 96
97 97 config = Configurator(settings=settings)
98 98 # Init our statsd at very start
99 99 config.registry.statsd = StatsdClient.statsd
100 100
101 101 # Apply compatibility patches
102 102 patches.inspect_getargspec()
103 103
104 104 load_pyramid_environment(global_config, settings)
105 105
106 106 # Static file view comes first
107 107 includeme_first(config)
108 108
109 109 includeme(config)
110 110
111 111 pyramid_app = config.make_wsgi_app()
112 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 113 pyramid_app.config = config
114 114
115 115 celery_settings = get_celery_config(settings)
116 116 config.configure_celery(celery_settings)
117 117
118 118 # creating the app uses a connection - return it after we are done
119 119 meta.Session.remove()
120 120
121 121 total_time = time.time() - start_time
122 122 log.info('Pyramid app created and configured in %.2fs', total_time)
123 123 return pyramid_app
124 124
125 125
126 126 def get_celery_config(settings):
127 127 """
128 128 Converts basic ini configuration into celery 4.X options
129 129 """
130 130
131 131 def key_converter(key_name):
132 132 pref = 'celery.'
133 133 if key_name.startswith(pref):
134 134 return key_name[len(pref):].replace('.', '_').lower()
135 135
136 136 def type_converter(parsed_key, value):
137 137 # cast to int
138 138 if value.isdigit():
139 139 return int(value)
140 140
141 141 # cast to bool
142 142 if value.lower() in ['true', 'false', 'True', 'False']:
143 143 return value.lower() == 'true'
144 144 return value
145 145
146 146 celery_config = {}
147 147 for k, v in settings.items():
148 148 pref = 'celery.'
149 149 if k.startswith(pref):
150 150 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
151 151
152 152 # TODO:rethink if we want to support celerybeat based file config, probably NOT
153 153 # beat_config = {}
154 154 # for section in parser.sections():
155 155 # if section.startswith('celerybeat:'):
156 156 # name = section.split(':', 1)[1]
157 157 # beat_config[name] = get_beat_config(parser, section)
158 158
159 159 # final compose of settings
160 160 celery_settings = {}
161 161
162 162 if celery_config:
163 163 celery_settings.update(celery_config)
164 164 # if beat_config:
165 165 # celery_settings.update({'beat_schedule': beat_config})
166 166
167 167 return celery_settings
168 168
169 169
170 170 def not_found_view(request):
171 171 """
172 172 This creates the view which should be registered as not-found-view to
173 173 pyramid.
174 174 """
175 175
176 176 if not getattr(request, 'vcs_call', None):
177 177 # handle like regular case with our error_handler
178 178 return error_handler(HTTPNotFound(), request)
179 179
180 180 # handle not found view as a vcs call
181 181 settings = request.registry.settings
182 182 ae_client = getattr(request, 'ae_client', None)
183 183 vcs_app = VCSMiddleware(
184 184 HTTPNotFound(), request.registry, settings,
185 185 appenlight_client=ae_client)
186 186
187 187 return wsgiapp(vcs_app)(None, request)
188 188
189 189
190 190 def error_handler(exception, request):
191 191 import rhodecode
192 192 from rhodecode.lib import helpers
193 from rhodecode.lib.utils2 import str2bool
194 193
195 194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
196 195
197 196 base_response = HTTPInternalServerError()
198 197 # prefer original exception for the response since it may have headers set
199 198 if isinstance(exception, HTTPException):
200 199 base_response = exception
201 200 elif isinstance(exception, VCSCommunicationError):
202 201 base_response = VCSServerUnavailable()
203 202
204 203 if is_http_error(base_response):
205 log.exception(
206 'error occurred handling this request for path: %s', request.path)
204 traceback_info = format_exc(request.exc_info)
205 log.error(
206 'error occurred handling this request for path: %s, \n%s',
207 request.path, traceback_info)
207 208
208 209 error_explanation = base_response.explanation or str(base_response)
209 210 if base_response.status_code == 404:
210 211 error_explanation += " Optionally you don't have permission to access this page."
211 212 c = AttributeDict()
212 213 c.error_message = base_response.status
213 214 c.error_explanation = error_explanation
214 215 c.visual = AttributeDict()
215 216
216 217 c.visual.rhodecode_support_url = (
217 218 request.registry.settings.get('rhodecode_support_url') or
218 219 request.route_url('rhodecode_support')
219 220 )
220 221 c.redirect_time = 0
221 222 c.rhodecode_name = rhodecode_title
222 223 if not c.rhodecode_name:
223 224 c.rhodecode_name = 'Rhodecode'
224 225
225 226 c.causes = []
226 227 if is_http_error(base_response):
227 228 c.causes.append('Server is overloaded.')
228 229 c.causes.append('Server database connection is lost.')
229 230 c.causes.append('Server expected unhandled error.')
230 231
231 232 if hasattr(base_response, 'causes'):
232 233 c.causes = base_response.causes
233 234
234 235 c.messages = helpers.flash.pop_messages(request=request)
235 236 exc_info = sys.exc_info()
236 237 c.exception_id = id(exc_info)
237 238 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
238 239 or base_response.status_code > 499
239 240 c.exception_id_url = request.route_url(
240 241 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
241 242
242 243 debug_mode = rhodecode.ConfigGet().get_bool('debug')
243 244 if c.show_exception_id:
244 245 store_exception(c.exception_id, exc_info)
245 246 c.exception_debug = debug_mode
246 247 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
247 248
248 249 if debug_mode:
249 250 try:
250 251 from rich.traceback import install
251 252 install(show_locals=True)
252 253 log.debug('Installing rich tracebacks...')
253 254 except ImportError:
254 255 pass
255 256
256 257 response = render_to_response(
257 258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 259 response=base_response)
259 260
260 261 statsd = request.registry.statsd
261 262 if statsd and base_response.status_code > 499:
262 263 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
263 264 statsd.incr('rhodecode_exception_total',
264 265 tags=["exc_source:web",
265 266 f"http_code:{base_response.status_code}",
266 267 f"type:{exc_type}"])
267 268
268 269 return response
269 270
270 271
271 272 def includeme_first(config):
272 273 # redirect automatic browser favicon.ico requests to correct place
273 274 def favicon_redirect(context, request):
274 275 return HTTPFound(
275 276 request.static_path('rhodecode:public/images/favicon.ico'))
276 277
277 278 config.add_view(favicon_redirect, route_name='favicon')
278 279 config.add_route('favicon', '/favicon.ico')
279 280
280 281 def robots_redirect(context, request):
281 282 return HTTPFound(
282 283 request.static_path('rhodecode:public/robots.txt'))
283 284
284 285 config.add_view(robots_redirect, route_name='robots')
285 286 config.add_route('robots', '/robots.txt')
286 287
287 288 config.add_static_view(
288 289 '_static/deform', 'deform:static')
289 290 config.add_static_view(
290 291 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
291 292
292 293
293 294 ce_auth_resources = [
294 295 'rhodecode.authentication.plugins.auth_crowd',
295 296 'rhodecode.authentication.plugins.auth_headers',
296 297 'rhodecode.authentication.plugins.auth_jasig_cas',
297 298 'rhodecode.authentication.plugins.auth_ldap',
298 299 'rhodecode.authentication.plugins.auth_pam',
299 300 'rhodecode.authentication.plugins.auth_rhodecode',
300 301 'rhodecode.authentication.plugins.auth_token',
301 302 ]
302 303
303 304
304 305 def includeme(config, auth_resources=None):
305 306 from rhodecode.lib.celerylib.loader import configure_celery
306 307 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
307 308 settings = config.registry.settings
308 309 config.set_request_factory(Request)
309 310
310 311 # plugin information
311 312 config.registry.rhodecode_plugins = collections.OrderedDict()
312 313
313 314 config.add_directive(
314 315 'register_rhodecode_plugin', register_rhodecode_plugin)
315 316
316 317 config.add_directive('configure_celery', configure_celery)
317 318
318 319 if settings.get('appenlight', False):
319 320 config.include('appenlight_client.ext.pyramid_tween')
320 321
321 322 load_all = should_load_all()
322 323
323 324 # Includes which are required. The application would fail without them.
324 325 config.include('pyramid_mako')
325 326 config.include('rhodecode.lib.rc_beaker')
326 327 config.include('rhodecode.lib.rc_cache')
327 328 config.include('rhodecode.lib.rc_cache.archive_cache')
328 329
329 330 config.include('rhodecode.apps._base.navigation')
330 331 config.include('rhodecode.apps._base.subscribers')
331 332 config.include('rhodecode.tweens')
332 333 config.include('rhodecode.authentication')
333 334
334 335 if load_all:
335 336
336 337 # load CE authentication plugins
337 338
338 339 if auth_resources:
339 340 ce_auth_resources.extend(auth_resources)
340 341
341 342 for resource in ce_auth_resources:
342 343 config.include(resource)
343 344
344 345 # Auto discover authentication plugins and include their configuration.
345 346 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
346 347 from rhodecode.authentication import discover_legacy_plugins
347 348 discover_legacy_plugins(config)
348 349
349 350 # apps
350 351 if load_all:
351 352 log.debug('Starting config.include() calls')
352 353 config.include('rhodecode.api.includeme')
353 354 config.include('rhodecode.apps._base.includeme')
354 355 config.include('rhodecode.apps._base.navigation.includeme')
355 356 config.include('rhodecode.apps._base.subscribers.includeme')
356 357 config.include('rhodecode.apps.hovercards.includeme')
357 358 config.include('rhodecode.apps.ops.includeme')
358 359 config.include('rhodecode.apps.channelstream.includeme')
359 360 config.include('rhodecode.apps.file_store.includeme')
360 361 config.include('rhodecode.apps.admin.includeme')
361 362 config.include('rhodecode.apps.login.includeme')
362 363 config.include('rhodecode.apps.home.includeme')
363 364 config.include('rhodecode.apps.journal.includeme')
364 365
365 366 config.include('rhodecode.apps.repository.includeme')
366 367 config.include('rhodecode.apps.repo_group.includeme')
367 368 config.include('rhodecode.apps.user_group.includeme')
368 369 config.include('rhodecode.apps.search.includeme')
369 370 config.include('rhodecode.apps.user_profile.includeme')
370 371 config.include('rhodecode.apps.user_group_profile.includeme')
371 372 config.include('rhodecode.apps.my_account.includeme')
372 373 config.include('rhodecode.apps.gist.includeme')
373 374
374 375 config.include('rhodecode.apps.svn_support.includeme')
375 376 config.include('rhodecode.apps.ssh_support.includeme')
376 377 config.include('rhodecode.apps.debug_style')
377 378
378 379 if load_all:
379 380 config.include('rhodecode.integrations.includeme')
380 381 config.include('rhodecode.integrations.routes.includeme')
381 382
382 383 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
383 384 settings['default_locale_name'] = settings.get('lang', 'en')
384 385 config.add_translation_dirs('rhodecode:i18n/')
385 386
386 387 # Add subscribers.
387 388 if load_all:
388 389 log.debug('Adding subscribers....')
389 390 config.add_subscriber(scan_repositories_if_enabled,
390 391 pyramid.events.ApplicationCreated)
391 392 config.add_subscriber(write_metadata_if_needed,
392 393 pyramid.events.ApplicationCreated)
393 394 config.add_subscriber(write_usage_data,
394 395 pyramid.events.ApplicationCreated)
395 396 config.add_subscriber(write_js_routes_if_enabled,
396 397 pyramid.events.ApplicationCreated)
397 398
398 399
399 400 # Set the default renderer for HTML templates to mako.
400 401 config.add_mako_renderer('.html')
401 402
402 403 config.add_renderer(
403 404 name='json_ext',
404 405 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
405 406
406 407 config.add_renderer(
407 408 name='string_html',
408 409 factory='rhodecode.lib.string_renderer.html')
409 410
410 411 # include RhodeCode plugins
411 412 includes = aslist(settings.get('rhodecode.includes', []))
412 413 log.debug('processing rhodecode.includes data...')
413 414 for inc in includes:
414 415 config.include(inc)
415 416
416 417 # custom not found view, if our pyramid app doesn't know how to handle
417 418 # the request pass it to potential VCS handling ap
418 419 config.add_notfound_view(not_found_view)
419 420 if not settings.get('debugtoolbar.enabled', False):
420 421 # disabled debugtoolbar handle all exceptions via the error_handlers
421 422 config.add_view(error_handler, context=Exception)
422 423
423 424 # all errors including 403/404/50X
424 425 config.add_view(error_handler, context=HTTPError)
425 426
426 427
427 428 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
428 429 """
429 430 Apply outer WSGI middlewares around the application.
430 431 """
431 432 registry = config.registry
432 433 settings = registry.settings
433 434
434 435 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
435 436 pyramid_app = HttpsFixup(pyramid_app, settings)
436 437
437 438 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
438 439 pyramid_app, settings)
439 440 registry.ae_client = _ae_client
440 441
441 442 if settings['gzip_responses']:
442 443 pyramid_app = make_gzip_middleware(
443 444 pyramid_app, settings, compress_level=1)
444 445
445 446 # this should be the outer most middleware in the wsgi stack since
446 447 # middleware like Routes make database calls
447 448 def pyramid_app_with_cleanup(environ, start_response):
448 449 start = time.time()
449 450 try:
450 451 return pyramid_app(environ, start_response)
451 452 finally:
452 453 # Dispose current database session and rollback uncommitted
453 454 # transactions.
454 455 meta.Session.remove()
455 456
456 457 # In a single threaded mode server, on non sqlite db we should have
457 458 # '0 Current Checked out connections' at the end of a request,
458 459 # if not, then something, somewhere is leaving a connection open
459 460 pool = meta.get_engine().pool
460 461 log.debug('sa pool status: %s', pool.status())
461 462 total = time.time() - start
462 463 log.debug('Request processing finalized: %.4fs', total)
463 464
464 465 return pyramid_app_with_cleanup
465 466
466 467
467 468 def sanitize_settings_and_apply_defaults(global_config, settings):
468 469 """
469 470 Applies settings defaults and does all type conversion.
470 471
471 472 We would move all settings parsing and preparation into this place, so that
472 473 we have only one place left which deals with this part. The remaining parts
473 474 of the application would start to rely fully on well prepared settings.
474 475
475 476 This piece would later be split up per topic to avoid a big fat monster
476 477 function.
477 478 """
478 479
479 480 global_settings_maker = SettingsMaker(global_config)
480 481 global_settings_maker.make_setting('debug', default=False, parser='bool')
481 482 debug_enabled = asbool(global_config.get('debug'))
482 483
483 484 settings_maker = SettingsMaker(settings)
484 485
485 486 settings_maker.make_setting(
486 487 'logging.autoconfigure',
487 488 default=False,
488 489 parser='bool')
489 490
490 491 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
491 492 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
492 493
493 494 # Default includes, possible to change as a user
494 495 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
495 496 log.debug(
496 497 "Using the following pyramid.includes: %s",
497 498 pyramid_includes)
498 499
499 500 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
500 501 settings_maker.make_setting('rhodecode.edition_id', 'CE')
501 502
502 503 if 'mako.default_filters' not in settings:
503 504 # set custom default filters if we don't have it defined
504 505 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
505 506 settings['mako.default_filters'] = 'h_filter'
506 507
507 508 if 'mako.directories' not in settings:
508 509 mako_directories = settings.setdefault('mako.directories', [
509 510 # Base templates of the original application
510 511 'rhodecode:templates',
511 512 ])
512 513 log.debug(
513 514 "Using the following Mako template directories: %s",
514 515 mako_directories)
515 516
516 517 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
517 518 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
518 519 raw_url = settings['beaker.session.url']
519 520 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
520 521 settings['beaker.session.url'] = 'redis://' + raw_url
521 522
522 523 settings_maker.make_setting('__file__', global_config.get('__file__'))
523 524
524 525 # TODO: johbo: Re-think this, usually the call to config.include
525 526 # should allow to pass in a prefix.
526 527 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
527 528
528 529 # Sanitize generic settings.
529 530 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
530 531 settings_maker.make_setting('is_test', False, parser='bool')
531 532 settings_maker.make_setting('gzip_responses', False, parser='bool')
532 533
533 534 # statsd
534 535 settings_maker.make_setting('statsd.enabled', False, parser='bool')
535 536 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
536 537 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
537 538 settings_maker.make_setting('statsd.statsd_prefix', '')
538 539 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
539 540
540 541 settings_maker.make_setting('vcs.svn.compatible_version', '')
541 542 settings_maker.make_setting('vcs.hooks.protocol', 'http')
542 543 settings_maker.make_setting('vcs.hooks.host', '*')
543 544 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
544 545 settings_maker.make_setting('vcs.server', '')
545 546 settings_maker.make_setting('vcs.server.protocol', 'http')
546 547 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
547 548 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
548 549 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
549 550 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
550 551 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
551 552 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
552 553
553 554 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
554 555
555 556 # Support legacy values of vcs.scm_app_implementation. Legacy
556 557 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
557 558 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
558 559 scm_app_impl = settings['vcs.scm_app_implementation']
559 560 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
560 561 settings['vcs.scm_app_implementation'] = 'http'
561 562
562 563 settings_maker.make_setting('appenlight', False, parser='bool')
563 564
564 565 temp_store = tempfile.gettempdir()
565 566 tmp_cache_dir = os.path.join(temp_store, 'rc_cache')
566 567
567 568 # save default, cache dir, and use it for all backends later.
568 569 default_cache_dir = settings_maker.make_setting(
569 570 'cache_dir',
570 571 default=tmp_cache_dir, default_when_empty=True,
571 572 parser='dir:ensured')
572 573
573 574 # exception store cache
574 575 settings_maker.make_setting(
575 576 'exception_tracker.store_path',
576 577 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
577 578 parser='dir:ensured'
578 579 )
579 580
580 581 settings_maker.make_setting(
581 582 'celerybeat-schedule.path',
582 583 default=os.path.join(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
583 584 parser='file:ensured'
584 585 )
585 586
586 587 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
587 588 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
588 589
589 590 # cache_general
590 591 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
591 592 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
592 593 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_general.db'))
593 594
594 595 # cache_perms
595 596 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
596 597 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
597 598 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_perms_db'))
598 599
599 600 # cache_repo
600 601 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
601 602 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
602 603 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_repo_db'))
603 604
604 605 # cache_license
605 606 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
606 607 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
607 608 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_license_db'))
608 609
609 610 # cache_repo_longterm memory, 96H
610 611 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
611 612 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
612 613 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
613 614
614 615 # sql_cache_short
615 616 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
616 617 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
617 618 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
618 619
619 620 # archive_cache
620 621 settings_maker.make_setting('archive_cache.store_dir', os.path.join(default_cache_dir, 'archive_cache'), default_when_empty=True,)
621 622 settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float')
622 623 settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int')
623 624
624 625 settings_maker.env_expand()
625 626
626 627 # configure instance id
627 628 config_utils.set_instance_id(settings)
628 629
629 630 return settings
@@ -1,230 +1,315 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 import io
19 20 import os
20 21 import time
21 22 import sys
22 23 import datetime
23 24 import msgpack
24 25 import logging
25 26 import traceback
26 27 import tempfile
27 28 import glob
28 29
29 30 log = logging.getLogger(__name__)
30 31
31 32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
32 global_prefix = 'rhodecode'
33 exc_store_dir_name = 'rc_exception_store_v1'
33 global_prefix = "rhodecode"
34 exc_store_dir_name = "rc_exception_store_v1"
34 35
35 36
36 37 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
37
38 38 data = {
39 'version': 'v1',
40 'exc_id': exc_id,
41 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
42 'exc_timestamp': repr(time.time()),
43 'exc_message': tb,
44 'exc_type': exc_type,
39 "version": "v1",
40 "exc_id": exc_id,
41 "exc_utc_date": datetime.datetime.utcnow().isoformat(),
42 "exc_timestamp": repr(time.time()),
43 "exc_message": tb,
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 _exc_store = None
55 56
56 57
57 def get_exc_store():
58 """
59 Get and create exception store if it's not existing
60 """
61 global _exc_store
58 def maybe_send_exc_email(exc_id, exc_type_name, send_email):
59 from pyramid.threadlocal import get_current_request
62 60 import rhodecode as app
63 61
64 if _exc_store is not None:
65 # quick global cache
66 return _exc_store
67
68 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
69 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
70
71 _exc_store_path = os.path.abspath(_exc_store_path)
72 if not os.path.isdir(_exc_store_path):
73 os.makedirs(_exc_store_path)
74 log.debug('Initializing exceptions store at %s', _exc_store_path)
75 _exc_store = _exc_store_path
76
77 return _exc_store_path
78
79
80 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
81 """
82 Low level function to store exception in the exception tracker
83 """
84 from pyramid.threadlocal import get_current_request
85 import rhodecode as app
86 62 request = get_current_request()
87 extra_data = {}
88 # NOTE(marcink): store request information into exc_data
89 if request:
90 extra_data['client_address'] = getattr(request, 'client_addr', '')
91 extra_data['user_agent'] = getattr(request, 'user_agent', '')
92 extra_data['method'] = getattr(request, 'method', '')
93 extra_data['url'] = getattr(request, 'url', '')
94
95 exc_store_path = get_exc_store()
96 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
97
98 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
99 if not os.path.isdir(exc_store_path):
100 os.makedirs(exc_store_path)
101 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
102 with open(stored_exc_path, 'wb') as f:
103 f.write(exc_data)
104 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
105 63
106 64 if send_email is None:
107 65 # NOTE(marcink): read app config unless we specify explicitly
108 send_email = app.CONFIG.get('exception_tracker.send_email', False)
66 send_email = app.CONFIG.get("exception_tracker.send_email", False)
109 67
110 mail_server = app.CONFIG.get('smtp_server') or None
68 mail_server = app.CONFIG.get("smtp_server") or None
111 69 send_email = send_email and mail_server
112 70 if send_email and request:
113 71 try:
114 72 send_exc_email(request, exc_id, exc_type_name)
115 73 except Exception:
116 log.exception('Failed to send exception email')
74 log.exception("Failed to send exception email")
117 75 exc_info = sys.exc_info()
118 76 store_exception(id(exc_info), exc_info, send_email=False)
119 77
120 78
121 79 def send_exc_email(request, exc_id, exc_type_name):
122 80 import rhodecode as app
123 81 from rhodecode.apps._base import TemplateArgs
124 82 from rhodecode.lib.utils2 import aslist
125 83 from rhodecode.lib.celerylib import run_task, tasks
126 84 from rhodecode.lib.base import attach_context_attributes
127 85 from rhodecode.model.notification import EmailNotificationModel
128 86
129 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
130 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
87 recipients = aslist(app.CONFIG.get("exception_tracker.send_email_recipients", ""))
88 log.debug("Sending Email exception to: `%s`", recipients or "all super admins")
131 89
132 90 # NOTE(marcink): needed for email template rendering
133 91 user_id = None
134 if hasattr(request, 'user'):
92 if hasattr(request, "user"):
135 93 user_id = request.user.user_id
136 94 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137 95
138 96 email_kwargs = {
139 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
140 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
141 'exc_id': exc_id,
142 'exc_type_name': exc_type_name,
143 'exc_traceback': read_exception(exc_id, prefix=None),
97 "email_prefix": app.CONFIG.get("exception_tracker.email_prefix", "")
98 or "[RHODECODE ERROR]",
99 "exc_url": request.route_url(
100 "admin_settings_exception_tracker_show", exception_id=exc_id
101 ),
102 "exc_id": exc_id,
103 "exc_type_name": exc_type_name,
104 "exc_traceback": read_exception(exc_id, prefix=None),
144 105 }
145 106
146 107 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
108 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs
109 )
110
111 run_task(tasks.send_email, recipients, subject, email_body_plaintext, email_body)
112
113
114 def get_exc_store():
115 """
116 Get and create exception store if it's not existing
117 """
118 global _exc_store
148 119
149 run_task(tasks.send_email, recipients, subject,
150 email_body_plaintext, email_body)
120 if _exc_store is not None:
121 # quick global cache
122 return _exc_store
123
124 import rhodecode as app
125
126 exc_store_dir = (
127 app.CONFIG.get("exception_tracker.store_path", "") or tempfile.gettempdir()
128 )
129 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
130
131 _exc_store_path = os.path.abspath(_exc_store_path)
132 if not os.path.isdir(_exc_store_path):
133 os.makedirs(_exc_store_path)
134 log.debug("Initializing exceptions store at %s", _exc_store_path)
135 _exc_store = _exc_store_path
136
137 return _exc_store_path
151 138
152 139
153 def _prepare_exception(exc_info):
154 exc_type, exc_value, exc_traceback = exc_info
155 exc_type_name = exc_type.__name__
140 def get_detailed_tb(exc_info):
141 try:
142 from pip._vendor.rich import (
143 traceback as rich_tb,
144 scope as rich_scope,
145 console as rich_console,
146 )
147 except ImportError:
148 try:
149 from rich import (
150 traceback as rich_tb,
151 scope as rich_scope,
152 console as rich_console,
153 )
154 except ImportError:
155 return None
156
157 console = rich_console.Console(width=160, file=io.StringIO())
158
159 exc = rich_tb.Traceback.extract(*exc_info, show_locals=True)
156 160
157 tb = ''.join(traceback.format_exception(
158 exc_type, exc_value, exc_traceback, None))
161 tb_rich = rich_tb.Traceback(
162 trace=exc,
163 width=160,
164 extra_lines=3,
165 theme=None,
166 word_wrap=False,
167 show_locals=False,
168 max_frames=100,
169 )
159 170
160 return exc_type_name, tb
171 # last_stack = exc.stacks[-1]
172 # last_frame = last_stack.frames[-1]
173 # if last_frame and last_frame.locals:
174 # console.print(
175 # rich_scope.render_scope(
176 # last_frame.locals,
177 # title=f'{last_frame.filename}:{last_frame.lineno}'))
178
179 console.print(tb_rich)
180 formatted_locals = console.file.getvalue()
181
182 return formatted_locals
161 183
162 184
163 def store_exception(exc_id, exc_info, prefix=global_prefix, send_email=None):
185 def get_request_metadata(request=None) -> dict:
186 request_metadata = {}
187 if not request:
188 from pyramid.threadlocal import get_current_request
189
190 request = get_current_request()
191
192 # NOTE(marcink): store request information into exc_data
193 if request:
194 request_metadata["client_address"] = getattr(request, "client_addr", "")
195 request_metadata["user_agent"] = getattr(request, "user_agent", "")
196 request_metadata["method"] = getattr(request, "method", "")
197 request_metadata["url"] = getattr(request, "url", "")
198 return request_metadata
199
200
201 def format_exc(exc_info):
202 exc_type, exc_value, exc_traceback = exc_info
203 tb = "++ TRACEBACK ++\n\n"
204 tb += "".join(traceback.format_exception(exc_type, exc_value, exc_traceback, None))
205
206 locals_tb = get_detailed_tb(exc_info)
207 if locals_tb:
208 tb += f"\n+++ DETAILS +++\n\n{locals_tb}\n" ""
209 return tb
210
211
212 def _store_exception(exc_id, exc_info, prefix, request_path='', send_email=None):
213 """
214 Low level function to store exception in the exception tracker
215 """
216
217 extra_data = {}
218 extra_data.update(get_request_metadata())
219
220 exc_type, exc_value, exc_traceback = exc_info
221 tb = format_exc(exc_info)
222
223 exc_type_name = exc_type.__name__
224 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name, extra_data=extra_data)
225
226 exc_pref_id = f"{exc_id}_{prefix}_{org_data['exc_timestamp']}"
227 exc_store_path = get_exc_store()
228 if not os.path.isdir(exc_store_path):
229 os.makedirs(exc_store_path)
230 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
231 with open(stored_exc_path, "wb") as f:
232 f.write(exc_data)
233 log.debug("Stored generated exception %s as: %s", exc_id, stored_exc_path)
234
235 if request_path:
236 log.error(
237 'error occurred handling this request.\n'
238 'Path: `%s`, %s',
239 request_path, tb)
240
241 maybe_send_exc_email(exc_id, exc_type_name, send_email)
242
243
244 def store_exception(exc_id, exc_info, prefix=global_prefix, request_path='', send_email=None):
164 245 """
165 246 Example usage::
166 247
167 248 exc_info = sys.exc_info()
168 249 store_exception(id(exc_info), exc_info)
169 250 """
170 251
171 252 try:
172 exc_type_name, exc_traceback = _prepare_exception(exc_info)
173 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
174 exc_traceback=exc_traceback, prefix=prefix, send_email=send_email)
253 exc_type = exc_info[0]
254 exc_type_name = exc_type.__name__
255
256 _store_exception(
257 exc_id=exc_id, exc_info=exc_info, prefix=prefix, request_path=request_path,
258 send_email=send_email
259 )
175 260 return exc_id, exc_type_name
176 261 except Exception:
177 log.exception('Failed to store exception `%s` information', exc_id)
262 log.exception("Failed to store exception `%s` information", exc_id)
178 263 # there's no way this can fail, it will crash server badly if it does.
179 264 pass
180 265
181 266
182 267 def _find_exc_file(exc_id, prefix=global_prefix):
183 268 exc_store_path = get_exc_store()
184 269 if prefix:
185 exc_id = f'{exc_id}_{prefix}'
270 exc_id = f"{exc_id}_{prefix}"
186 271 else:
187 272 # search without a prefix
188 exc_id = f'{exc_id}'
273 exc_id = f"{exc_id}"
189 274
190 275 found_exc_id = None
191 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
276 matches = glob.glob(os.path.join(exc_store_path, exc_id) + "*")
192 277 if matches:
193 278 found_exc_id = matches[0]
194 279
195 280 return found_exc_id
196 281
197 282
198 283 def _read_exception(exc_id, prefix):
199 284 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
200 285 if exc_id_file_path:
201 with open(exc_id_file_path, 'rb') as f:
286 with open(exc_id_file_path, "rb") as f:
202 287 return exc_unserialize(f.read())
203 288 else:
204 log.debug('Exception File `%s` not found', exc_id_file_path)
289 log.debug("Exception File `%s` not found", exc_id_file_path)
205 290 return None
206 291
207 292
208 293 def read_exception(exc_id, prefix=global_prefix):
209 294 try:
210 295 return _read_exception(exc_id=exc_id, prefix=prefix)
211 296 except Exception:
212 log.exception('Failed to read exception `%s` information', exc_id)
297 log.exception("Failed to read exception `%s` information", exc_id)
213 298 # there's no way this can fail, it will crash server badly if it does.
214 299 return None
215 300
216 301
217 302 def delete_exception(exc_id, prefix=global_prefix):
218 303 try:
219 304 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
220 305 if exc_id_file_path:
221 306 os.remove(exc_id_file_path)
222 307
223 308 except Exception:
224 log.exception('Failed to remove exception `%s` information', exc_id)
309 log.exception("Failed to remove exception `%s` information", exc_id)
225 310 # there's no way this can fail, it will crash server badly if it does.
226 311 pass
227 312
228 313
229 314 def generate_id():
230 315 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now