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