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