diff --git a/.idea/misc.xml b/.idea/misc.xml index e259dad37..b485cd2d3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 5bcc80581..390b75bc8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ In general, configuration can be done both with environment variables (taken fir `production.ini` file. You can configure the base URL for accessing the views provided by c2cwsgiutils with an environment variable -named `C2C_BASE_PATH` or in the `production.ini` file with a property named `c2c.base_path`. +named `C2C_BASE_PATH` or in the `production.ini` file with a property named `c2c.base_path`. Pyramid @@ -208,6 +208,14 @@ version of a app. This file is generated by calling the `c2cwsgiutils_genversion command line. Usually done in the [Dockerfile](acceptance_tests/app/Dockerfile) of the WSGI application. +Debugging +--------- + +To enable the debugging interface, you must set the `DEBUG_VIEW_SECRET` environment variable or the +`c2c.debug_view_secret` variable. Then you can get a dump of every threads with this URL: +`{C2C_BASE_PATH}/debug/stacks?secret={DEBUG_VIEW_SECRET}` + + CORS ---- diff --git a/acceptance_tests/tests/acceptance.iml b/acceptance_tests/tests/acceptance.iml index bb4ef979e..b233d535d 100644 --- a/acceptance_tests/tests/acceptance.iml +++ b/acceptance_tests/tests/acceptance.iml @@ -6,7 +6,7 @@ - + diff --git a/acceptance_tests/tests/docker-compose.yml b/acceptance_tests/tests/docker-compose.yml index 135ace006..091b1b6a6 100644 --- a/acceptance_tests/tests/docker-compose.yml +++ b/acceptance_tests/tests/docker-compose.yml @@ -10,6 +10,7 @@ services: STATSD_PREFIX: acceptance LOG_VIEW_SECRET: changeme SQL_PROFILER_SECRET: changeme + DEBUG_VIEW_SECRET: changeme LOG_HOST: 172.17.0.1 LOG_TYPE: 'console,logstash' SQL_LOG_LEVEL: DEBUG diff --git a/acceptance_tests/tests/tests/test_debug.py b/acceptance_tests/tests/tests/test_debug.py new file mode 100644 index 000000000..10c26db6f --- /dev/null +++ b/acceptance_tests/tests/tests/test_debug.py @@ -0,0 +1,7 @@ +def test_ok(app_connection): + stacks = app_connection.get('c2c/debug/stacks', params={'secret': 'changeme'}) + assert 'c2cwsgiutils/debug' in stacks + + +def test_no_auth(app_connection): + app_connection.get_json('c2c/debug/stacks', expected_status=403) diff --git a/c2cwsgiutils/_utils.py b/c2cwsgiutils/_utils.py index 3f6b11f18..063abf8cd 100644 --- a/c2cwsgiutils/_utils.py +++ b/c2cwsgiutils/_utils.py @@ -3,6 +3,7 @@ """ import os +from pyramid.httpexceptions import HTTPForbidden def get_base_path(config): @@ -13,3 +14,8 @@ def env_or_config(config, env_name, config_name, default): if env_name in os.environ: return os.environ[env_name] return config.get_settings().get(config_name, default) + + +def auth_view(request, env_name, config_name): + if request.params.get('secret') != env_or_config(request.registry.settings, env_name, config_name, False): + raise HTTPForbidden('Missing or invalid secret parameter') diff --git a/c2cwsgiutils/debug.py b/c2cwsgiutils/debug.py new file mode 100644 index 000000000..9126c1e83 --- /dev/null +++ b/c2cwsgiutils/debug.py @@ -0,0 +1,32 @@ +import logging +import threading +import traceback +import sys + +from c2cwsgiutils import _utils + +CONFIG_KEY = 'c2c.debug_view_secret' +ENV_KEY = 'DEBUG_VIEW_SECRET' + +LOG = logging.getLogger(__name__) + + +def _dump_stacks(request): + _utils.auth_view(request, ENV_KEY, CONFIG_KEY) + id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) + code = [] + for threadId, stack in sys._current_frames().items(): + code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId)) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + return "\n".join(code) + + +def init(config): + if _utils.env_or_config(config, ENV_KEY, CONFIG_KEY, False): + config.add_route("c2c_debug_stacks", _utils.get_base_path(config) + r"/debug/stacks", + request_method="GET") + config.add_view(_dump_stacks, route_name="c2c_debug_stacks", renderer="string", http_cache=0) + LOG.info("Enabled the /debug/stacks API") diff --git a/c2cwsgiutils/pyramid.py b/c2cwsgiutils/pyramid.py index d1073de22..7163b4fdd 100644 --- a/c2cwsgiutils/pyramid.py +++ b/c2cwsgiutils/pyramid.py @@ -1,7 +1,7 @@ import cornice import pyramid_tm -from c2cwsgiutils import stats, pyramid_logging, sql_profiler, version +from c2cwsgiutils import stats, pyramid_logging, sql_profiler, version, debug def includeme(config): @@ -16,4 +16,5 @@ def includeme(config): pyramid_logging.install_subscriber(config) sql_profiler.init(config) version.init(config) + debug.init(config) config.scan("c2cwsgiutils.errors") diff --git a/c2cwsgiutils/pyramid_logging.py b/c2cwsgiutils/pyramid_logging.py index 1c08420e4..7d2b9c611 100644 --- a/c2cwsgiutils/pyramid_logging.py +++ b/c2cwsgiutils/pyramid_logging.py @@ -8,15 +8,16 @@ A pyramid event handler is installed to setup this filter for the current request. """ import logging -import os import uuid from cee_syslog_handler import CeeSysLogHandler -from pyramid.httpexceptions import HTTPForbidden from pyramid.threadlocal import get_current_request from c2cwsgiutils import _utils +CONFIG_KEY = 'c2c.log_view_secret' +ENV_KEY = 'LOG_VIEW_SECRET' + LOG = logging.getLogger(__name__) @@ -53,7 +54,7 @@ def install_subscriber(config): Install the view to configure the loggers, if configured to do so. """ config.add_request_method(lambda request: str(uuid.uuid4()), 'c2c_request_id', reify=True) - if 'LOG_VIEW_SECRET' in os.environ: + if _utils.env_or_config(config, ENV_KEY, CONFIG_KEY, False): config.add_route("c2c_logging_level", _utils.get_base_path(config) + r"/logging/level", request_method="GET") config.add_view(_logging_change_level, route_name="c2c_logging_level", renderer="json", http_cache=0) @@ -61,8 +62,7 @@ def install_subscriber(config): def _logging_change_level(request): - if request.params.get('secret') != os.environ['LOG_VIEW_SECRET']: - raise HTTPForbidden('Missing or invalid secret parameter') + _utils.auth_view(request, ENV_KEY, CONFIG_KEY) name = request.params['name'] level = request.params.get('level') logger = logging.getLogger(name) diff --git a/c2cwsgiutils/sql_profiler.py b/c2cwsgiutils/sql_profiler.py index f099536e4..1328b9113 100644 --- a/c2cwsgiutils/sql_profiler.py +++ b/c2cwsgiutils/sql_profiler.py @@ -3,8 +3,6 @@ every SELECT query going through SQLAlchemy. """ import logging -import os -from pyramid.httpexceptions import HTTPForbidden import re import sqlalchemy.event import sqlalchemy.engine @@ -12,14 +10,14 @@ from c2cwsgiutils import _utils ENV_KEY = 'SQL_PROFILER_SECRET' +CONFIG_KEY = 'c2c.sql_profiler_secret' LOG = logging.getLogger(__name__) enabled = False def _sql_profiler_view(request): global enabled - if request.params.get('secret') != os.environ[ENV_KEY]: - raise HTTPForbidden('Missing or invalid secret parameter') + _utils.auth_view(request, ENV_KEY, CONFIG_KEY) if 'enable' in request.params: if request.params['enable'] == '1': if not enabled: @@ -68,7 +66,7 @@ def init(config): """ Install a pyramid event handler that adds the request information """ - if 'SQL_PROFILER_SECRET' in os.environ: + if _utils.env_or_config(config, ENV_KEY, CONFIG_KEY, False): config.add_route("c2c_sql_profiler", _utils.get_base_path(config) + r"/sql_profiler", request_method="GET") config.add_view(_sql_profiler_view, route_name="c2c_sql_profiler", renderer="json", http_cache=0) diff --git a/setup.py b/setup.py index 0ee12b343..63b3e43c8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.10.1' +VERSION = '0.11.0' HERE = os.path.abspath(os.path.dirname(__file__)) INSTALL_REQUIRES = open(os.path.join(HERE, 'rel_requirements.txt')).read().splitlines()