##// END OF EJS Templates
metrics: use non decimal version for timer....
super-admin -
r4806:9676846e default
parent child Browse files
Show More
@@ -1,797 +1,797 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 52 from rhodecode.lib.exc_tracking import store_exception
53 53 from rhodecode.subscribers import (
54 54 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 55 write_metadata_if_needed, write_usage_data)
56 56 from rhodecode.lib.statsd_client import StatsdClient
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def is_http_error(response):
62 62 # error which should have traceback
63 63 return response.status_code > 499
64 64
65 65
66 66 def should_load_all():
67 67 """
68 68 Returns if all application components should be loaded. In some cases it's
69 69 desired to skip apps loading for faster shell script execution
70 70 """
71 71 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
72 72 if ssh_cmd:
73 73 return False
74 74
75 75 return True
76 76
77 77
78 78 def make_pyramid_app(global_config, **settings):
79 79 """
80 80 Constructs the WSGI application based on Pyramid.
81 81
82 82 Specials:
83 83
84 84 * The application can also be integrated like a plugin via the call to
85 85 `includeme`. This is accompanied with the other utility functions which
86 86 are called. Changing this should be done with great care to not break
87 87 cases when these fragments are assembled from another place.
88 88
89 89 """
90 90
91 91 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
92 92 # will be replaced by the value of the environment variable "NAME" in this case.
93 93 start_time = time.time()
94 94 log.info('Pyramid app config starting')
95 95
96 96 # init and bootstrap StatsdClient
97 97 StatsdClient.setup(settings)
98 98
99 99 debug = asbool(global_config.get('debug'))
100 100 if debug:
101 101 enable_debug()
102 102
103 103 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
104 104
105 105 global_config = _substitute_values(global_config, environ)
106 106 settings = _substitute_values(settings, environ)
107 107
108 108 sanitize_settings_and_apply_defaults(global_config, settings)
109 109
110 110 config = Configurator(settings=settings)
111 111 # Init our statsd at very start
112 112 config.registry.statsd = StatsdClient.statsd
113 113
114 114 # Apply compatibility patches
115 115 patches.inspect_getargspec()
116 116
117 117 load_pyramid_environment(global_config, settings)
118 118
119 119 # Static file view comes first
120 120 includeme_first(config)
121 121
122 122 includeme(config)
123 123
124 124 pyramid_app = config.make_wsgi_app()
125 125 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
126 126 pyramid_app.config = config
127 127
128 128 config.configure_celery(global_config['__file__'])
129 129
130 130 # creating the app uses a connection - return it after we are done
131 131 meta.Session.remove()
132 132 statsd = StatsdClient.statsd
133 133
134 134 total_time = time.time() - start_time
135 135 log.info('Pyramid app `%s` created and configured in %.2fs',
136 136 pyramid_app.func_name, total_time)
137 137 if statsd:
138 elapsed_time_ms = 1000.0 * total_time
138 elapsed_time_ms = round(1000.0 * total_time) # use ms only rounded time
139 139 statsd.timing('rhodecode_app_bootstrap_timing', elapsed_time_ms, tags=[
140 140 "pyramid_app:{}".format(pyramid_app.func_name)
141 ])
141 ], use_decimals=False)
142 142 return pyramid_app
143 143
144 144
145 145 def not_found_view(request):
146 146 """
147 147 This creates the view which should be registered as not-found-view to
148 148 pyramid.
149 149 """
150 150
151 151 if not getattr(request, 'vcs_call', None):
152 152 # handle like regular case with our error_handler
153 153 return error_handler(HTTPNotFound(), request)
154 154
155 155 # handle not found view as a vcs call
156 156 settings = request.registry.settings
157 157 ae_client = getattr(request, 'ae_client', None)
158 158 vcs_app = VCSMiddleware(
159 159 HTTPNotFound(), request.registry, settings,
160 160 appenlight_client=ae_client)
161 161
162 162 return wsgiapp(vcs_app)(None, request)
163 163
164 164
165 165 def error_handler(exception, request):
166 166 import rhodecode
167 167 from rhodecode.lib import helpers
168 168 from rhodecode.lib.utils2 import str2bool
169 169
170 170 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
171 171
172 172 base_response = HTTPInternalServerError()
173 173 # prefer original exception for the response since it may have headers set
174 174 if isinstance(exception, HTTPException):
175 175 base_response = exception
176 176 elif isinstance(exception, VCSCommunicationError):
177 177 base_response = VCSServerUnavailable()
178 178
179 179 if is_http_error(base_response):
180 180 log.exception(
181 181 'error occurred handling this request for path: %s', request.path)
182 182
183 183 statsd = request.registry.statsd
184 184 if statsd and base_response.status_code > 499:
185 185 statsd.incr('rhodecode_exception_total',
186 186 tags=["exc_source:web", "type:{}".format(base_response.status_code)])
187 187
188 188 error_explanation = base_response.explanation or str(base_response)
189 189 if base_response.status_code == 404:
190 190 error_explanation += " Optionally you don't have permission to access this page."
191 191 c = AttributeDict()
192 192 c.error_message = base_response.status
193 193 c.error_explanation = error_explanation
194 194 c.visual = AttributeDict()
195 195
196 196 c.visual.rhodecode_support_url = (
197 197 request.registry.settings.get('rhodecode_support_url') or
198 198 request.route_url('rhodecode_support')
199 199 )
200 200 c.redirect_time = 0
201 201 c.rhodecode_name = rhodecode_title
202 202 if not c.rhodecode_name:
203 203 c.rhodecode_name = 'Rhodecode'
204 204
205 205 c.causes = []
206 206 if is_http_error(base_response):
207 207 c.causes.append('Server is overloaded.')
208 208 c.causes.append('Server database connection is lost.')
209 209 c.causes.append('Server expected unhandled error.')
210 210
211 211 if hasattr(base_response, 'causes'):
212 212 c.causes = base_response.causes
213 213
214 214 c.messages = helpers.flash.pop_messages(request=request)
215 215
216 216 exc_info = sys.exc_info()
217 217 c.exception_id = id(exc_info)
218 218 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
219 219 or base_response.status_code > 499
220 220 c.exception_id_url = request.route_url(
221 221 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
222 222
223 223 if c.show_exception_id:
224 224 store_exception(c.exception_id, exc_info)
225 225 c.exception_debug = str2bool(rhodecode.CONFIG.get('debug'))
226 226 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
227 227
228 228 response = render_to_response(
229 229 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
230 230 response=base_response)
231 231
232 232 return response
233 233
234 234
235 235 def includeme_first(config):
236 236 # redirect automatic browser favicon.ico requests to correct place
237 237 def favicon_redirect(context, request):
238 238 return HTTPFound(
239 239 request.static_path('rhodecode:public/images/favicon.ico'))
240 240
241 241 config.add_view(favicon_redirect, route_name='favicon')
242 242 config.add_route('favicon', '/favicon.ico')
243 243
244 244 def robots_redirect(context, request):
245 245 return HTTPFound(
246 246 request.static_path('rhodecode:public/robots.txt'))
247 247
248 248 config.add_view(robots_redirect, route_name='robots')
249 249 config.add_route('robots', '/robots.txt')
250 250
251 251 config.add_static_view(
252 252 '_static/deform', 'deform:static')
253 253 config.add_static_view(
254 254 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
255 255
256 256
257 257 def includeme(config, auth_resources=None):
258 258 from rhodecode.lib.celerylib.loader import configure_celery
259 259 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
260 260 settings = config.registry.settings
261 261 config.set_request_factory(Request)
262 262
263 263 # plugin information
264 264 config.registry.rhodecode_plugins = collections.OrderedDict()
265 265
266 266 config.add_directive(
267 267 'register_rhodecode_plugin', register_rhodecode_plugin)
268 268
269 269 config.add_directive('configure_celery', configure_celery)
270 270
271 271 if asbool(settings.get('appenlight', 'false')):
272 272 config.include('appenlight_client.ext.pyramid_tween')
273 273
274 274 load_all = should_load_all()
275 275
276 276 # Includes which are required. The application would fail without them.
277 277 config.include('pyramid_mako')
278 278 config.include('rhodecode.lib.rc_beaker')
279 279 config.include('rhodecode.lib.rc_cache')
280 280 config.include('rhodecode.apps._base.navigation')
281 281 config.include('rhodecode.apps._base.subscribers')
282 282 config.include('rhodecode.tweens')
283 283 config.include('rhodecode.authentication')
284 284
285 285 if load_all:
286 286 ce_auth_resources = [
287 287 'rhodecode.authentication.plugins.auth_crowd',
288 288 'rhodecode.authentication.plugins.auth_headers',
289 289 'rhodecode.authentication.plugins.auth_jasig_cas',
290 290 'rhodecode.authentication.plugins.auth_ldap',
291 291 'rhodecode.authentication.plugins.auth_pam',
292 292 'rhodecode.authentication.plugins.auth_rhodecode',
293 293 'rhodecode.authentication.plugins.auth_token',
294 294 ]
295 295
296 296 # load CE authentication plugins
297 297
298 298 if auth_resources:
299 299 ce_auth_resources.extend(auth_resources)
300 300
301 301 for resource in ce_auth_resources:
302 302 config.include(resource)
303 303
304 304 # Auto discover authentication plugins and include their configuration.
305 305 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
306 306 from rhodecode.authentication import discover_legacy_plugins
307 307 discover_legacy_plugins(config)
308 308
309 309 # apps
310 310 if load_all:
311 311 config.include('rhodecode.api')
312 312 config.include('rhodecode.apps._base')
313 313 config.include('rhodecode.apps.hovercards')
314 314 config.include('rhodecode.apps.ops')
315 315 config.include('rhodecode.apps.channelstream')
316 316 config.include('rhodecode.apps.file_store')
317 317 config.include('rhodecode.apps.admin')
318 318 config.include('rhodecode.apps.login')
319 319 config.include('rhodecode.apps.home')
320 320 config.include('rhodecode.apps.journal')
321 321
322 322 config.include('rhodecode.apps.repository')
323 323 config.include('rhodecode.apps.repo_group')
324 324 config.include('rhodecode.apps.user_group')
325 325 config.include('rhodecode.apps.search')
326 326 config.include('rhodecode.apps.user_profile')
327 327 config.include('rhodecode.apps.user_group_profile')
328 328 config.include('rhodecode.apps.my_account')
329 329 config.include('rhodecode.apps.gist')
330 330
331 331 config.include('rhodecode.apps.svn_support')
332 332 config.include('rhodecode.apps.ssh_support')
333 333 config.include('rhodecode.apps.debug_style')
334 334
335 335 if load_all:
336 336 config.include('rhodecode.integrations')
337 337
338 338 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
339 339 config.add_translation_dirs('rhodecode:i18n/')
340 340 settings['default_locale_name'] = settings.get('lang', 'en')
341 341
342 342 # Add subscribers.
343 343 if load_all:
344 344 config.add_subscriber(scan_repositories_if_enabled,
345 345 pyramid.events.ApplicationCreated)
346 346 config.add_subscriber(write_metadata_if_needed,
347 347 pyramid.events.ApplicationCreated)
348 348 config.add_subscriber(write_usage_data,
349 349 pyramid.events.ApplicationCreated)
350 350 config.add_subscriber(write_js_routes_if_enabled,
351 351 pyramid.events.ApplicationCreated)
352 352
353 353 # request custom methods
354 354 config.add_request_method(
355 355 'rhodecode.lib.partial_renderer.get_partial_renderer',
356 356 'get_partial_renderer')
357 357
358 358 config.add_request_method(
359 359 'rhodecode.lib.request_counter.get_request_counter',
360 360 'request_count')
361 361
362 362 # Set the authorization policy.
363 363 authz_policy = ACLAuthorizationPolicy()
364 364 config.set_authorization_policy(authz_policy)
365 365
366 366 # Set the default renderer for HTML templates to mako.
367 367 config.add_mako_renderer('.html')
368 368
369 369 config.add_renderer(
370 370 name='json_ext',
371 371 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
372 372
373 373 config.add_renderer(
374 374 name='string_html',
375 375 factory='rhodecode.lib.string_renderer.html')
376 376
377 377 # include RhodeCode plugins
378 378 includes = aslist(settings.get('rhodecode.includes', []))
379 379 for inc in includes:
380 380 config.include(inc)
381 381
382 382 # custom not found view, if our pyramid app doesn't know how to handle
383 383 # the request pass it to potential VCS handling ap
384 384 config.add_notfound_view(not_found_view)
385 385 if not settings.get('debugtoolbar.enabled', False):
386 386 # disabled debugtoolbar handle all exceptions via the error_handlers
387 387 config.add_view(error_handler, context=Exception)
388 388
389 389 # all errors including 403/404/50X
390 390 config.add_view(error_handler, context=HTTPError)
391 391
392 392
393 393 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
394 394 """
395 395 Apply outer WSGI middlewares around the application.
396 396 """
397 397 registry = config.registry
398 398 settings = registry.settings
399 399
400 400 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
401 401 pyramid_app = HttpsFixup(pyramid_app, settings)
402 402
403 403 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
404 404 pyramid_app, settings)
405 405 registry.ae_client = _ae_client
406 406
407 407 if settings['gzip_responses']:
408 408 pyramid_app = make_gzip_middleware(
409 409 pyramid_app, settings, compress_level=1)
410 410
411 411 # this should be the outer most middleware in the wsgi stack since
412 412 # middleware like Routes make database calls
413 413 def pyramid_app_with_cleanup(environ, start_response):
414 414 try:
415 415 return pyramid_app(environ, start_response)
416 416 finally:
417 417 # Dispose current database session and rollback uncommitted
418 418 # transactions.
419 419 meta.Session.remove()
420 420
421 421 # In a single threaded mode server, on non sqlite db we should have
422 422 # '0 Current Checked out connections' at the end of a request,
423 423 # if not, then something, somewhere is leaving a connection open
424 424 pool = meta.Base.metadata.bind.engine.pool
425 425 log.debug('sa pool status: %s', pool.status())
426 426 log.debug('Request processing finalized')
427 427
428 428 return pyramid_app_with_cleanup
429 429
430 430
431 431 def sanitize_settings_and_apply_defaults(global_config, settings):
432 432 """
433 433 Applies settings defaults and does all type conversion.
434 434
435 435 We would move all settings parsing and preparation into this place, so that
436 436 we have only one place left which deals with this part. The remaining parts
437 437 of the application would start to rely fully on well prepared settings.
438 438
439 439 This piece would later be split up per topic to avoid a big fat monster
440 440 function.
441 441 """
442 442
443 443 settings.setdefault('rhodecode.edition', 'Community Edition')
444 444 settings.setdefault('rhodecode.edition_id', 'CE')
445 445
446 446 if 'mako.default_filters' not in settings:
447 447 # set custom default filters if we don't have it defined
448 448 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
449 449 settings['mako.default_filters'] = 'h_filter'
450 450
451 451 if 'mako.directories' not in settings:
452 452 mako_directories = settings.setdefault('mako.directories', [
453 453 # Base templates of the original application
454 454 'rhodecode:templates',
455 455 ])
456 456 log.debug(
457 457 "Using the following Mako template directories: %s",
458 458 mako_directories)
459 459
460 460 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
461 461 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
462 462 raw_url = settings['beaker.session.url']
463 463 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
464 464 settings['beaker.session.url'] = 'redis://' + raw_url
465 465
466 466 # Default includes, possible to change as a user
467 467 pyramid_includes = settings.setdefault('pyramid.includes', [])
468 468 log.debug(
469 469 "Using the following pyramid.includes: %s",
470 470 pyramid_includes)
471 471
472 472 # TODO: johbo: Re-think this, usually the call to config.include
473 473 # should allow to pass in a prefix.
474 474 settings.setdefault('rhodecode.api.url', '/_admin/api')
475 475 settings.setdefault('__file__', global_config.get('__file__'))
476 476
477 477 # Sanitize generic settings.
478 478 _list_setting(settings, 'default_encoding', 'UTF-8')
479 479 _bool_setting(settings, 'is_test', 'false')
480 480 _bool_setting(settings, 'gzip_responses', 'false')
481 481
482 482 # Call split out functions that sanitize settings for each topic.
483 483 _sanitize_appenlight_settings(settings)
484 484 _sanitize_vcs_settings(settings)
485 485 _sanitize_cache_settings(settings)
486 486
487 487 # configure instance id
488 488 config_utils.set_instance_id(settings)
489 489
490 490 return settings
491 491
492 492
493 493 def enable_debug():
494 494 """
495 495 Helper to enable debug on running instance
496 496 :return:
497 497 """
498 498 import tempfile
499 499 import textwrap
500 500 import logging.config
501 501
502 502 ini_template = textwrap.dedent("""
503 503 #####################################
504 504 ### DEBUG LOGGING CONFIGURATION ####
505 505 #####################################
506 506 [loggers]
507 507 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
508 508
509 509 [handlers]
510 510 keys = console, console_sql
511 511
512 512 [formatters]
513 513 keys = generic, color_formatter, color_formatter_sql
514 514
515 515 #############
516 516 ## LOGGERS ##
517 517 #############
518 518 [logger_root]
519 519 level = NOTSET
520 520 handlers = console
521 521
522 522 [logger_sqlalchemy]
523 523 level = INFO
524 524 handlers = console_sql
525 525 qualname = sqlalchemy.engine
526 526 propagate = 0
527 527
528 528 [logger_beaker]
529 529 level = DEBUG
530 530 handlers =
531 531 qualname = beaker.container
532 532 propagate = 1
533 533
534 534 [logger_rhodecode]
535 535 level = DEBUG
536 536 handlers =
537 537 qualname = rhodecode
538 538 propagate = 1
539 539
540 540 [logger_ssh_wrapper]
541 541 level = DEBUG
542 542 handlers =
543 543 qualname = ssh_wrapper
544 544 propagate = 1
545 545
546 546 [logger_celery]
547 547 level = DEBUG
548 548 handlers =
549 549 qualname = celery
550 550
551 551
552 552 ##############
553 553 ## HANDLERS ##
554 554 ##############
555 555
556 556 [handler_console]
557 557 class = StreamHandler
558 558 args = (sys.stderr, )
559 559 level = DEBUG
560 560 formatter = color_formatter
561 561
562 562 [handler_console_sql]
563 563 # "level = DEBUG" logs SQL queries and results.
564 564 # "level = INFO" logs SQL queries.
565 565 # "level = WARN" logs neither. (Recommended for production systems.)
566 566 class = StreamHandler
567 567 args = (sys.stderr, )
568 568 level = WARN
569 569 formatter = color_formatter_sql
570 570
571 571 ################
572 572 ## FORMATTERS ##
573 573 ################
574 574
575 575 [formatter_generic]
576 576 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
577 577 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
578 578 datefmt = %Y-%m-%d %H:%M:%S
579 579
580 580 [formatter_color_formatter]
581 581 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
582 582 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
583 583 datefmt = %Y-%m-%d %H:%M:%S
584 584
585 585 [formatter_color_formatter_sql]
586 586 class = rhodecode.lib.logging_formatter.ColorFormatterSql
587 587 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
588 588 datefmt = %Y-%m-%d %H:%M:%S
589 589 """)
590 590
591 591 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
592 592 delete=False) as f:
593 593 log.info('Saved Temporary DEBUG config at %s', f.name)
594 594 f.write(ini_template)
595 595
596 596 logging.config.fileConfig(f.name)
597 597 log.debug('DEBUG MODE ON')
598 598 os.remove(f.name)
599 599
600 600
601 601 def _sanitize_appenlight_settings(settings):
602 602 _bool_setting(settings, 'appenlight', 'false')
603 603
604 604
605 605 def _sanitize_vcs_settings(settings):
606 606 """
607 607 Applies settings defaults and does type conversion for all VCS related
608 608 settings.
609 609 """
610 610 _string_setting(settings, 'vcs.svn.compatible_version', '')
611 611 _string_setting(settings, 'vcs.hooks.protocol', 'http')
612 612 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
613 613 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
614 614 _string_setting(settings, 'vcs.server', '')
615 615 _string_setting(settings, 'vcs.server.protocol', 'http')
616 616 _bool_setting(settings, 'startup.import_repos', 'false')
617 617 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
618 618 _bool_setting(settings, 'vcs.server.enable', 'true')
619 619 _bool_setting(settings, 'vcs.start_server', 'false')
620 620 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
621 621 _int_setting(settings, 'vcs.connection_timeout', 3600)
622 622
623 623 # Support legacy values of vcs.scm_app_implementation. Legacy
624 624 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
625 625 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
626 626 scm_app_impl = settings['vcs.scm_app_implementation']
627 627 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
628 628 settings['vcs.scm_app_implementation'] = 'http'
629 629
630 630
631 631 def _sanitize_cache_settings(settings):
632 632 temp_store = tempfile.gettempdir()
633 633 default_cache_dir = os.path.join(temp_store, 'rc_cache')
634 634
635 635 # save default, cache dir, and use it for all backends later.
636 636 default_cache_dir = _string_setting(
637 637 settings,
638 638 'cache_dir',
639 639 default_cache_dir, lower=False, default_when_empty=True)
640 640
641 641 # ensure we have our dir created
642 642 if not os.path.isdir(default_cache_dir):
643 643 os.makedirs(default_cache_dir, mode=0o755)
644 644
645 645 # exception store cache
646 646 _string_setting(
647 647 settings,
648 648 'exception_tracker.store_path',
649 649 temp_store, lower=False, default_when_empty=True)
650 650 _bool_setting(
651 651 settings,
652 652 'exception_tracker.send_email',
653 653 'false')
654 654 _string_setting(
655 655 settings,
656 656 'exception_tracker.email_prefix',
657 657 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
658 658
659 659 # cache_perms
660 660 _string_setting(
661 661 settings,
662 662 'rc_cache.cache_perms.backend',
663 663 'dogpile.cache.rc.file_namespace', lower=False)
664 664 _int_setting(
665 665 settings,
666 666 'rc_cache.cache_perms.expiration_time',
667 667 60)
668 668 _string_setting(
669 669 settings,
670 670 'rc_cache.cache_perms.arguments.filename',
671 671 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
672 672
673 673 # cache_repo
674 674 _string_setting(
675 675 settings,
676 676 'rc_cache.cache_repo.backend',
677 677 'dogpile.cache.rc.file_namespace', lower=False)
678 678 _int_setting(
679 679 settings,
680 680 'rc_cache.cache_repo.expiration_time',
681 681 60)
682 682 _string_setting(
683 683 settings,
684 684 'rc_cache.cache_repo.arguments.filename',
685 685 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
686 686
687 687 # cache_license
688 688 _string_setting(
689 689 settings,
690 690 'rc_cache.cache_license.backend',
691 691 'dogpile.cache.rc.file_namespace', lower=False)
692 692 _int_setting(
693 693 settings,
694 694 'rc_cache.cache_license.expiration_time',
695 695 5*60)
696 696 _string_setting(
697 697 settings,
698 698 'rc_cache.cache_license.arguments.filename',
699 699 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
700 700
701 701 # cache_repo_longterm memory, 96H
702 702 _string_setting(
703 703 settings,
704 704 'rc_cache.cache_repo_longterm.backend',
705 705 'dogpile.cache.rc.memory_lru', lower=False)
706 706 _int_setting(
707 707 settings,
708 708 'rc_cache.cache_repo_longterm.expiration_time',
709 709 345600)
710 710 _int_setting(
711 711 settings,
712 712 'rc_cache.cache_repo_longterm.max_size',
713 713 10000)
714 714
715 715 # sql_cache_short
716 716 _string_setting(
717 717 settings,
718 718 'rc_cache.sql_cache_short.backend',
719 719 'dogpile.cache.rc.memory_lru', lower=False)
720 720 _int_setting(
721 721 settings,
722 722 'rc_cache.sql_cache_short.expiration_time',
723 723 30)
724 724 _int_setting(
725 725 settings,
726 726 'rc_cache.sql_cache_short.max_size',
727 727 10000)
728 728
729 729
730 730 def _int_setting(settings, name, default):
731 731 settings[name] = int(settings.get(name, default))
732 732 return settings[name]
733 733
734 734
735 735 def _bool_setting(settings, name, default):
736 736 input_val = settings.get(name, default)
737 737 if isinstance(input_val, unicode):
738 738 input_val = input_val.encode('utf8')
739 739 settings[name] = asbool(input_val)
740 740 return settings[name]
741 741
742 742
743 743 def _list_setting(settings, name, default):
744 744 raw_value = settings.get(name, default)
745 745
746 746 old_separator = ','
747 747 if old_separator in raw_value:
748 748 # If we get a comma separated list, pass it to our own function.
749 749 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
750 750 else:
751 751 # Otherwise we assume it uses pyramids space/newline separation.
752 752 settings[name] = aslist(raw_value)
753 753 return settings[name]
754 754
755 755
756 756 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
757 757 value = settings.get(name, default)
758 758
759 759 if default_when_empty and not value:
760 760 # use default value when value is empty
761 761 value = default
762 762
763 763 if lower:
764 764 value = value.lower()
765 765 settings[name] = value
766 766 return settings[name]
767 767
768 768
769 769 def _substitute_values(mapping, substitutions):
770 770 result = {}
771 771
772 772 try:
773 773 for key, value in mapping.items():
774 774 # initialize without substitution first
775 775 result[key] = value
776 776
777 777 # Note: Cannot use regular replacements, since they would clash
778 778 # with the implementation of ConfigParser. Using "format" instead.
779 779 try:
780 780 result[key] = value.format(**substitutions)
781 781 except KeyError as e:
782 782 env_var = '{}'.format(e.args[0])
783 783
784 784 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
785 785 'Make sure your environment has {var} set, or remove this ' \
786 786 'variable from config file'.format(key=key, var=env_var)
787 787
788 788 if env_var.startswith('ENV_'):
789 789 raise ValueError(msg)
790 790 else:
791 791 log.warning(msg)
792 792
793 793 except ValueError as e:
794 794 log.warning('Failed to substitute ENV variable: %s', e)
795 795 result = mapping
796 796
797 797 return result
@@ -1,131 +1,148 b''
1 1 from __future__ import absolute_import, division, unicode_literals
2 2
3 3 import re
4 4 import random
5 5 from collections import deque
6 6 from datetime import timedelta
7 7 from repoze.lru import lru_cache
8 8
9 9 from .timer import Timer
10 10
11 11 TAG_INVALID_CHARS_RE = re.compile(
12 12 r"[^\w\d_\-:/\.]",
13 13 #re.UNICODE
14 14 )
15 15 TAG_INVALID_CHARS_SUBS = "_"
16 16
17 # we save and expose methods called by statsd for discovery
18 stat_dict = {
19
20 }
21
22
17 23
18 24 @lru_cache(maxsize=500)
19 25 def _normalize_tags_with_cache(tag_list):
20 26 return [TAG_INVALID_CHARS_RE.sub(TAG_INVALID_CHARS_SUBS, tag) for tag in tag_list]
21 27
22 28
23 29 def normalize_tags(tag_list):
24 30 # We have to turn our input tag list into a non-mutable tuple for it to
25 31 # be hashable (and thus usable) by the @lru_cache decorator.
26 32 return _normalize_tags_with_cache(tuple(tag_list))
27 33
28 34
29 35 class StatsClientBase(object):
30 36 """A Base class for various statsd clients."""
31 37
32 38 def close(self):
33 39 """Used to close and clean up any underlying resources."""
34 40 raise NotImplementedError()
35 41
36 42 def _send(self):
37 43 raise NotImplementedError()
38 44
39 45 def pipeline(self):
40 46 raise NotImplementedError()
41 47
42 48 def timer(self, stat, rate=1, tags=None):
43 49 return Timer(self, stat, rate, tags)
44 50
45 def timing(self, stat, delta, rate=1, tags=None):
51 def timing(self, stat, delta, rate=1, tags=None, use_decimals=True):
46 52 """
47 53 Send new timing information.
48 54
49 55 `delta` can be either a number of milliseconds or a timedelta.
50 56 """
51 57 if isinstance(delta, timedelta):
52 58 # Convert timedelta to number of milliseconds.
53 59 delta = delta.total_seconds() * 1000.
54 self._send_stat(stat, '%0.6f|ms' % delta, rate, tags)
60 if use_decimals:
61 fmt = '%0.6f|ms'
62 else:
63 fmt = '%s|ms'
64 self._send_stat(stat, fmt % delta, rate, tags)
55 65
56 66 def incr(self, stat, count=1, rate=1, tags=None):
57 67 """Increment a stat by `count`."""
58 68 self._send_stat(stat, '%s|c' % count, rate, tags)
59 69
60 70 def decr(self, stat, count=1, rate=1, tags=None):
61 71 """Decrement a stat by `count`."""
62 72 self.incr(stat, -count, rate, tags)
63 73
64 74 def gauge(self, stat, value, rate=1, delta=False, tags=None):
65 75 """Set a gauge value."""
66 76 if value < 0 and not delta:
67 77 if rate < 1:
68 78 if random.random() > rate:
69 79 return
70 80 with self.pipeline() as pipe:
71 81 pipe._send_stat(stat, '0|g', 1)
72 82 pipe._send_stat(stat, '%s|g' % value, 1)
73 83 else:
74 84 prefix = '+' if delta and value >= 0 else ''
75 85 self._send_stat(stat, '%s%s|g' % (prefix, value), rate, tags)
76 86
77 87 def set(self, stat, value, rate=1):
78 88 """Set a set value."""
79 89 self._send_stat(stat, '%s|s' % value, rate)
80 90
91 def histogram(self, stat, value, rate=1, tags=None):
92 """Set a histogram"""
93 self._send_stat(stat, '%s|h' % value, rate, tags)
94
81 95 def _send_stat(self, stat, value, rate, tags=None):
82 96 self._after(self._prepare(stat, value, rate, tags))
83 97
84 98 def _prepare(self, stat, value, rate, tags=None):
99 global stat_dict
100 stat_dict[stat] = 1
101
85 102 if rate < 1:
86 103 if random.random() > rate:
87 104 return
88 105 value = '%s|@%s' % (value, rate)
89 106
90 107 if self._prefix:
91 108 stat = '%s.%s' % (self._prefix, stat)
92 109
93 110 res = '%s:%s%s' % (
94 111 stat,
95 112 value,
96 113 ("|#" + ",".join(normalize_tags(tags))) if tags else "",
97 114 )
98 115 return res
99 116
100 117 def _after(self, data):
101 118 if data:
102 119 self._send(data)
103 120
104 121
105 122 class PipelineBase(StatsClientBase):
106 123
107 124 def __init__(self, client):
108 125 self._client = client
109 126 self._prefix = client._prefix
110 127 self._stats = deque()
111 128
112 129 def _send(self):
113 130 raise NotImplementedError()
114 131
115 132 def _after(self, data):
116 133 if data is not None:
117 134 self._stats.append(data)
118 135
119 136 def __enter__(self):
120 137 return self
121 138
122 139 def __exit__(self, typ, value, tb):
123 140 self.send()
124 141
125 142 def send(self):
126 143 if not self._stats:
127 144 return
128 145 self._send()
129 146
130 147 def pipeline(self):
131 148 return self.__class__(self)
@@ -1,72 +1,73 b''
1 1 from __future__ import absolute_import, division, unicode_literals
2 2
3 3 import functools
4 4
5 5 # Use timer that's not susceptible to time of day adjustments.
6 6 try:
7 7 # perf_counter is only present on Py3.3+
8 8 from time import perf_counter as time_now
9 9 except ImportError:
10 10 # fall back to using time
11 11 from time import time as time_now
12 12
13 13
14 14 def safe_wraps(wrapper, *args, **kwargs):
15 15 """Safely wraps partial functions."""
16 16 while isinstance(wrapper, functools.partial):
17 17 wrapper = wrapper.func
18 18 return functools.wraps(wrapper, *args, **kwargs)
19 19
20 20
21 21 class Timer(object):
22 22 """A context manager/decorator for statsd.timing()."""
23 23
24 def __init__(self, client, stat, rate=1, tags=None):
24 def __init__(self, client, stat, rate=1, tags=None, use_decimals=True):
25 25 self.client = client
26 26 self.stat = stat
27 27 self.rate = rate
28 28 self.tags = tags
29 29 self.ms = None
30 30 self._sent = False
31 31 self._start_time = None
32 self.use_decimals = use_decimals
32 33
33 34 def __call__(self, f):
34 35 """Thread-safe timing function decorator."""
35 36 @safe_wraps(f)
36 37 def _wrapped(*args, **kwargs):
37 38 start_time = time_now()
38 39 try:
39 40 return f(*args, **kwargs)
40 41 finally:
41 42 elapsed_time_ms = 1000.0 * (time_now() - start_time)
42 self.client.timing(self.stat, elapsed_time_ms, self.rate, self.tags)
43 self.client.timing(self.stat, elapsed_time_ms, self.rate, self.tags, self.use_decimals)
43 44 return _wrapped
44 45
45 46 def __enter__(self):
46 47 return self.start()
47 48
48 49 def __exit__(self, typ, value, tb):
49 50 self.stop()
50 51
51 52 def start(self):
52 53 self.ms = None
53 54 self._sent = False
54 55 self._start_time = time_now()
55 56 return self
56 57
57 58 def stop(self, send=True):
58 59 if self._start_time is None:
59 60 raise RuntimeError('Timer has not started.')
60 61 dt = time_now() - self._start_time
61 62 self.ms = 1000.0 * dt # Convert to milliseconds.
62 63 if send:
63 64 self.send()
64 65 return self
65 66
66 67 def send(self):
67 68 if self.ms is None:
68 69 raise RuntimeError('No data recorded.')
69 70 if self._sent:
70 71 raise RuntimeError('Already sent data.')
71 72 self._sent = True
72 self.client.timing(self.stat, self.ms, self.rate)
73 self.client.timing(self.stat, self.ms, self.rate, self.tags, self.use_decimals)
@@ -1,90 +1,91 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.auth import AuthUser
26 26 from rhodecode.lib.base import get_ip_addr, get_access_path, get_user_agent
27 27 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RequestWrapperTween(object):
34 34 def __init__(self, handler, registry):
35 35 self.handler = handler
36 36 self.registry = registry
37 37
38 38 # one-time configuration code goes here
39 39
40 40 def _get_user_info(self, request):
41 41 user = get_current_rhodecode_user(request)
42 42 if not user:
43 43 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
44 44 return user
45 45
46 46 def __call__(self, request):
47 47 start = time.time()
48 48 log.debug('Starting request time measurement')
49 49 try:
50 50 response = self.handler(request)
51 51 finally:
52 52 count = request.request_count()
53 53 _ver_ = rhodecode.__version__
54 54 _path = safe_str(get_access_path(request.environ))
55 55 _auth_user = self._get_user_info(request)
56 56 user_id = getattr(_auth_user, 'user_id', _auth_user)
57 57 total = time.time() - start
58 58 log.info(
59 59 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
60 60 count, _auth_user, request.environ.get('REQUEST_METHOD'),
61 61 _path, total, get_user_agent(request. environ), _ver_
62 62 )
63 63
64 64 statsd = request.registry.statsd
65 65 if statsd:
66 66 match_route = request.matched_route.name if request.matched_route else _path
67 67 resp_code = response.status_code
68 elapsed_time_ms = 1000.0 * total
68 elapsed_time_ms = round(1000.0 * total) # use ms only
69 69 statsd.timing(
70 70 'rhodecode_req_timing', elapsed_time_ms,
71 71 tags=[
72 72 "view_name:{}".format(match_route),
73 73 #"user:{}".format(user_id),
74 74 "code:{}".format(resp_code)
75 ]
75 ],
76 use_decimals=False
76 77 )
77 78 statsd.incr(
78 79 'rhodecode_req_total', tags=[
79 80 "view_name:{}".format(match_route),
80 81 #"user:{}".format(user_id),
81 82 "code:{}".format(resp_code)
82 83 ])
83 84
84 85 return response
85 86
86 87
87 88 def includeme(config):
88 89 config.add_tween(
89 90 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
90 91 )
General Comments 0
You need to be logged in to leave comments. Login now