Skip to content

Commit

Permalink
Add debug/stacks view
Browse files Browse the repository at this point in the history
  • Loading branch information
pvalsecc committed May 15, 2017
1 parent 43fe17a commit d79e207
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
----

Expand Down
2 changes: 1 addition & 1 deletion acceptance_tests/tests/acceptance.iml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/c2cwsgiutils" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="jdk" jdkName="Python 3.5.3 virtualenv at ~/src/c2c/c2cwsgiutils/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="c2cwsgiutils" />
</component>
Expand Down
1 change: 1 addition & 0 deletions acceptance_tests/tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions acceptance_tests/tests/tests/test_debug.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions c2cwsgiutils/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
from pyramid.httpexceptions import HTTPForbidden


def get_base_path(config):
Expand All @@ -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')
32 changes: 32 additions & 0 deletions c2cwsgiutils/debug.py
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 2 additions & 1 deletion c2cwsgiutils/pyramid.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")
10 changes: 5 additions & 5 deletions c2cwsgiutils/pyramid_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand Down Expand Up @@ -53,16 +54,15 @@ 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)
LOG.info("Enabled the /logging/change_level API")


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)
Expand Down
8 changes: 3 additions & 5 deletions c2cwsgiutils/sql_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
every SELECT query going through SQLAlchemy.
"""
import logging
import os
from pyramid.httpexceptions import HTTPForbidden
import re
import sqlalchemy.event
import sqlalchemy.engine

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:
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit d79e207

Please sign in to comment.