-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
da9f726
commit f8e92c8
Showing
9 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.coverage | ||
*.pyc | ||
*.egg-info | ||
.tox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
sudo: false | ||
language: python | ||
python: | ||
- '2.7' | ||
install: | ||
- pip install tox | ||
env: | ||
- TOX_ENV=py27-test | ||
- TOX_ENV=py33-test | ||
- TOX_ENV=py34-test | ||
script: | ||
- tox -e $TOX_ENV | ||
deploy: | ||
provider: pypi | ||
user: mattbennett | ||
password: | ||
secure: thV7e6kG3jA8x0/2DINX7o8U8IhOfhi6lmXenpTZXJWcbqa0bZZQvbwsPRIrlZkZlrGMEirfwsKmmK8UxBiFG9lgbOT8RMH6gyF1fW4cfINm3LQ3/U2Jjww3ZE7Km/ogVi18TQZxVSGUIQP3UUV9sEjif97N8VMON9HjsdbfquHc2XlFsP/4POK95uwOVU8W0cDWq5A4+Lh1L0IFDzUWk3AefH9HssGP386tlM9Yxuozh9bdgr+zpOYH3SDtGmA2mh3/32m4ksbI5KfBGk1g0Quf92Ql2Lykt/9BMLPEE18mEABODj3xursQvBEWTqVImdO6ykUoXEPm6MUkcBsy+v/u04OVgN9NECqvzjfbr/d2KclzNUiJJZu6l2T424zfeICQ+l0Q4hcd3QIJaqcnEEn3BkAQyfpwkMn9JOBgR1jYHXcUuHYOWfHUpG1b4j84VKvhhiLhw3HtUjZCVs5bIsVOQQAsv3la9uZyaSRQy2JnMYmcOipK+EriXOAdqNMSjGSozJ3T0/McIzn67YuqRJfkLeloHz98Toi1kB129LIRDJQQnjLC9hUZqzcvEiO2ZuP3R0VoHXJoKwjF95uTkoMGHTlbdanGeB4ZlaZSg3xctYZH2T9ajko1rkvYteK0d3tO1zI0ZOsTG8XldHa+zublI5CCjRRlwmPlxDAxk6I= | ||
on: | ||
tags: true | ||
repo: mattbennett/nameko-sentry |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
Copyright 2015 Matt Bennett | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
test: flake8 pylint pytest | ||
|
||
flake8: | ||
flake8 nameko_sentry.py test_nameko_sentry.py | ||
|
||
pylint: | ||
pylint nameko_sentry -E | ||
|
||
pytest: | ||
coverage run --concurrency=eventlet --source nameko_sentry.py --branch -m pytest test_nameko_sentry.py | ||
coverage report --show-missing --fail-under=100 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from __future__ import absolute_import | ||
|
||
# all imports are inline to make sure they happen after eventlet.monkey_patch | ||
# which is called in pytest_load_initial_conftests (calling monkey_patch at | ||
# import time breaks the pytest capturemanager) | ||
|
||
import pytest | ||
|
||
|
||
def pytest_addoption(parser): | ||
parser.addoption( | ||
'--blocking-detection', | ||
action='store_true', | ||
dest='blocking_detection', | ||
default=False, | ||
help='turn on eventlet hub blocking detection') | ||
|
||
parser.addoption( | ||
"--log-level", action="store", | ||
default='DEBUG', | ||
help=("The logging-level for the test run.")) | ||
|
||
parser.addoption( | ||
"--amqp-uri", action="store", dest='AMQP_URI', | ||
default='amqp://guest:guest@localhost:5672/nameko_test', | ||
help=("The AMQP-URI to connect to rabbit with.")) | ||
|
||
parser.addoption( | ||
"--rabbit-ctl-uri", action="store", dest='RABBIT_CTL_URI', | ||
default='http://guest:guest@localhost:15672', | ||
help=("The URI for rabbit's management API.")) | ||
|
||
|
||
def pytest_load_initial_conftests(): | ||
# make sure we monkey_patch before local conftests | ||
import eventlet | ||
eventlet.monkey_patch() | ||
|
||
|
||
def pytest_configure(config): | ||
import logging | ||
import sys | ||
|
||
if config.option.blocking_detection: # pragma: no cover | ||
from eventlet import debug | ||
debug.hub_blocking_detection(True) | ||
|
||
log_level = config.getoption('log_level') | ||
if log_level is not None: | ||
log_level = getattr(logging, log_level) | ||
logging.basicConfig(level=log_level, stream=sys.stderr) | ||
|
||
|
||
@pytest.fixture | ||
def ensure_cleanup_order(request): | ||
""" Ensure ``rabbit_config`` is invoked early if it's used by any fixture | ||
in ``request``. | ||
""" | ||
if "rabbit_config" in request.funcargnames: | ||
request.getfuncargvalue("rabbit_config") | ||
|
||
|
||
@pytest.yield_fixture | ||
def container_factory(ensure_cleanup_order): | ||
from nameko.containers import ServiceContainer | ||
|
||
all_containers = [] | ||
|
||
def make_container(service_cls, config, worker_ctx_cls=None): | ||
container = ServiceContainer(service_cls, config, worker_ctx_cls) | ||
all_containers.append(container) | ||
return container | ||
|
||
yield make_container | ||
|
||
for c in all_containers: | ||
try: | ||
c.stop() | ||
except: # pragma: no cover | ||
pass | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import logging | ||
|
||
from nameko.extensions import DependencyProvider | ||
from raven import Client | ||
from raven.transport.eventlet import EventletHTTPTransport | ||
|
||
|
||
class SentryReporter(DependencyProvider): | ||
""" Send exceptions generated by entrypoints to a sentry server. | ||
""" | ||
def setup(self): | ||
sentry_config = self.container.config.get('SENTRY') | ||
|
||
dsn = sentry_config['DSN'] | ||
kwargs = sentry_config.get('CLIENT_CONFIG', {}) | ||
|
||
self.client = Client(dsn, transport=EventletHTTPTransport, **kwargs) | ||
|
||
def worker_result(self, worker_ctx, result, exc_info): | ||
if exc_info is None: | ||
return | ||
|
||
exc = exc_info[1] | ||
call_id = worker_ctx.call_id | ||
parent_call_id = worker_ctx.immediate_parent_call_id | ||
|
||
expected_exceptions = getattr( | ||
worker_ctx.entrypoint, 'expected_exceptions', tuple()) | ||
|
||
level = logging.ERROR | ||
if expected_exceptions and isinstance(exc, expected_exceptions): | ||
level = logging.WARNING | ||
|
||
message = ( | ||
'Unhandled exception in call {}: ' | ||
'{} {!r}'.format(call_id, exc_info[0].__name__, str(exc)) | ||
) | ||
|
||
logger = '{}.{}'.format( | ||
worker_ctx.service_name, worker_ctx.entrypoint.method_name) | ||
|
||
data = { | ||
'logger': logger, | ||
'level': level, | ||
'message': message, | ||
'tags': { | ||
'call_id': call_id, | ||
'parent_call_id': parent_call_id, | ||
}, | ||
} | ||
|
||
extra = {'exc': exc} | ||
|
||
self.client.captureException( | ||
exc_info, message=message, extra=extra, data=data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#!/usr/bin/env python | ||
from setuptools import setup | ||
|
||
setup( | ||
name='nameko-sentry', | ||
version='0.0.1', | ||
description='Nameko extension sends entrypoint exceptions to sentry', | ||
author='Matt Bennett', | ||
author_email='[email protected]', | ||
url='http://github.com/mattbennett/nameko-sentry', | ||
py_modules=['nameko_sentry'], | ||
install_requires=[ | ||
"nameko>=2.0.0", | ||
"raven>=3.0.0" | ||
], | ||
extras_require={ | ||
'dev': [ | ||
"coverage==4.0a1", | ||
"flake8==2.1.0", | ||
"mccabe==0.3", | ||
"pep8==1.6.1", | ||
"pyflakes==0.8.1", | ||
"pylint==1.0.0", | ||
"pytest==2.4.2", | ||
] | ||
}, | ||
dependency_links=[], | ||
zip_safe=True, | ||
license='Apache License, Version 2.0', | ||
classifiers=[ | ||
"Programming Language :: Python", | ||
"Operating System :: MacOS :: MacOS X", | ||
"Operating System :: POSIX", | ||
"Programming Language :: Python :: 2", | ||
"Programming Language :: Python :: 2.7", | ||
"Programming Language :: Python :: 3", | ||
"Programming Language :: Python :: 3.3", | ||
"Programming Language :: Python :: 3.4", | ||
"Topic :: Internet", | ||
"Topic :: Software Development :: Libraries :: Python Modules", | ||
"Intended Audience :: Developers", | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import logging | ||
|
||
from mock import Mock, patch, call | ||
import pytest | ||
|
||
from nameko.containers import WorkerContext | ||
from nameko.extensions import Entrypoint | ||
from nameko.testing.services import dummy, entrypoint_hook, entrypoint_waiter | ||
from nameko.testing.utils import get_extension | ||
from raven.transport.eventlet import EventletHTTPTransport | ||
|
||
from nameko_sentry import SentryReporter | ||
|
||
|
||
class CustomException(Exception): | ||
pass | ||
|
||
|
||
@pytest.fixture | ||
def config(): | ||
return { | ||
'SENTRY': { | ||
'DSN': 'http://user:pass@localhost:9000/1', | ||
'CLIENT_CONFIG': { | ||
'site': 'site name' | ||
} | ||
} | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def container(config): | ||
return Mock(config=config) | ||
|
||
|
||
@pytest.fixture(params=[tuple(), CustomException]) # expected exceptions | ||
def worker_ctx(request, container): | ||
|
||
service = Mock() | ||
entrypoint = Mock(spec=Entrypoint, expected_exceptions=request.param) | ||
args = ("a", "b", "c") | ||
kwargs = {"d": "d", "e": "e"} | ||
data = { | ||
'call_id': 'service.entrypoint.1', | ||
'call_id_stack': [ | ||
'standalone_rpc_proxy.call.0' | ||
] | ||
} | ||
|
||
return WorkerContext( | ||
container, service, entrypoint, args=args, kwargs=kwargs, data=data | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def reporter(container): | ||
return SentryReporter().bind(container, "sentry") | ||
|
||
|
||
def test_setup(reporter): | ||
reporter.setup() | ||
|
||
# client config and DSN applied correctly | ||
assert reporter.client.site == "site name" | ||
assert reporter.client.get_public_dsn() == "//user@localhost:9000/1" | ||
|
||
# transport set correctly | ||
transport = reporter.client.remote.get_transport() | ||
assert isinstance(transport, EventletHTTPTransport) | ||
|
||
|
||
def test_worker_result(reporter, worker_ctx): | ||
result = "OK!" | ||
|
||
reporter.setup() | ||
with patch.object(reporter, 'client') as client: | ||
reporter.worker_result(worker_ctx, result, None) | ||
|
||
assert not client.captureException.called | ||
|
||
|
||
def test_worker_exception(reporter, worker_ctx): | ||
|
||
exc = CustomException("Error!") | ||
exc_info = (CustomException, exc, None) | ||
|
||
reporter.setup() | ||
with patch.object(reporter, 'client') as client: | ||
reporter.worker_result(worker_ctx, None, exc_info) | ||
|
||
# generate expected call args | ||
logger = "{}.{}".format( | ||
worker_ctx.service_name, worker_ctx.entrypoint.method_name) | ||
message = "Unhandled exception in call {}: {} {!r}".format( | ||
worker_ctx.call_id, CustomException.__name__, str(exc) | ||
) | ||
extra = {'exc': exc} | ||
|
||
if isinstance(exc, worker_ctx.entrypoint.expected_exceptions): | ||
loglevel = logging.WARNING | ||
else: | ||
loglevel = logging.ERROR | ||
|
||
data = { | ||
'logger': logger, | ||
'level': loglevel, | ||
'message': message, | ||
'tags': { | ||
'call_id': worker_ctx.call_id, | ||
'parent_call_id': worker_ctx.immediate_parent_call_id | ||
} | ||
} | ||
|
||
# verify call | ||
assert client.captureException.call_args_list == [ | ||
call(exc_info, message=message, extra=extra, data=data) | ||
] | ||
|
||
|
||
def test_end_to_end(container_factory, config): | ||
|
||
class Service(object): | ||
name = "service" | ||
|
||
sentry = SentryReporter() | ||
|
||
@dummy | ||
def broken(self): | ||
raise CustomException("Error!") | ||
|
||
container = container_factory(Service, config) | ||
container.start() | ||
|
||
reporter = get_extension(container, SentryReporter) | ||
|
||
with patch.object(reporter, 'client') as client: | ||
with entrypoint_hook(container, 'broken') as broken: | ||
with entrypoint_waiter(container, 'broken'): | ||
with pytest.raises(CustomException): | ||
broken() | ||
assert client.captureException.call_count == 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[tox] | ||
envlist = {py27,py33,py34}-test | ||
skipsdist = True | ||
|
||
[testenv] | ||
whitelist_externals = make | ||
|
||
commands = | ||
pip install --editable .[dev] | ||
make test |