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