Refactor DSN and transport configuration

Passing a transport via a scheme prefix is now deprecated in favor of explicit class loading:

>>> Client(dsn, transport=HttpTransport)

Additionally DSN-related configuration is now held in the RemoteConfig helper, which is bound on Client.remote.

Fixes GH-587
This commit is contained in:
David Cramer 2015-03-29 11:34:31 -07:00
parent 23bd04541d
commit 12bb737707
22 changed files with 314 additions and 403 deletions

View File

@ -1,3 +1,11 @@
Version 5.4.0
-------------
* Binding transports via a scheme prefix on DSNs is now deprecated.
* ``raven.conf.load`` has been removed.
* Upstream-related configuration (such as url, project_id, and keys) is now contained in ``RemoteConfig``
attached to ``Client.remote``
Version 5.3.1
-------------

View File

@ -66,11 +66,6 @@ It is composed of six important pieces:
* The project ID which the authenticated user is bound to.
.. note::
Protocol may also contain transporter type: gevent+http, gevent+https, twisted+http, tornado+http, eventlet+http, eventlet+https
For *Python 3.3+* also available: aiohttp+http and aiohttp+https
Client Arguments
----------------

View File

@ -3,7 +3,12 @@ Transports
A transport is the mechanism in which Raven sends the HTTP request to the Sentry server. By default, Raven uses a threaded asynchronous transport, but you can easily adjust this by modifying your ``SENTRY_DSN`` value.
Transport registration is done via the URL prefix, so for example, a synchronous transport is as simple as prefixing your ``SENTRY_DSN`` with the ``sync+`` value.
Transport registration is done as part of the Client configuration:
.. code-block:: python
# Use the synchronous HTTP transport
client = Client('http://public:secret@example.com/1', transport=HTTPTransport)
Options are passed to transports via the querystring.
@ -25,82 +30,38 @@ For example, to increase the timeout and to disable SSL verification:
SENTRY_DSN = 'http://public:secret@example.com/1?timeout=5&verify_ssl=0'
aiohttp
-------
Should only be used within a :pep:`3156` compatible event loops
(*asyncio* itself and others).
::
SENTRY_DSN = 'aiohttp+http://public:secret@example.com/1'
Eventlet
--------
Should only be used within an Eventlet IO loop.
::
SENTRY_DSN = 'eventlet+http://public:secret@example.com/1'
Gevent
------
Should only be used within a Gevent IO loop.
::
SENTRY_DSN = 'gevent+http://public:secret@example.com/1'
Requests
--------
Requires the ``requests`` library. Synchronous.
::
SENTRY_DSN = 'requests+http://public:secret@example.com/1'
Sync
----
A synchronous blocking transport.
::
SENTRY_DSN = 'sync+http://public:secret@example.com/1'
Threaded (Default)
Builtin Transports
------------------
Spawns an async worker for processing messages.
.. data:: sentry.transport.thread.ThreadedHTTPTransport
::
The default transport. Manages a threaded worker for processing messages asynchronous.
SENTRY_DSN = 'threaded+http://public:secret@example.com/1'
.. data:: sentry.transport.http.HTTPTransport
A synchronous blocking transport.
Tornado
-------
.. data:: sentry.transport.aiohttp.AioHttpTransport
Should only be used within a Tornado IO loop.
Should only be used within a :pep:`3156` compatible event loops
(*asyncio* itself and others).
::
.. data:: sentry.transport.eventlet.EventletHTTPTransport
SENTRY_DSN = 'tornado+http://public:secret@example.com/1'
Should only be used within an Eventlet IO loop.
.. data:: sentry.transport.gevent.GeventedHTTPTransport
Twisted
-------
Should only be used within a Gevent IO loop.
Should only be used within a Twisted event loop.
.. data:: sentry.transport.requests.RequestsHTTPTransport
::
A synchronous transport which relies on the ``requests`` library.
SENTRY_DSN = 'twisted+http://public:secret@example.com/1'
.. data:: sentry.transport.tornado.TornadoHTTPTransport
Should only be used within a Tornado IO loop.
.. data:: sentry.transport.twisted.TwistedHTTPTransport
Should only be used within a Twisted event loop.

View File

@ -55,9 +55,9 @@ the optional DSN argument::
You should get something like the following, assuming you're configured everything correctly::
$ raven test sync+http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
$ raven test http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
Using DSN configuration:
sync+http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
Client configuration:
servers : ['http://localhost:9000/api/store/']

View File

@ -14,7 +14,7 @@ from raven.conf import * # NOQA
from raven.versioning import * # NOQA
__all__ = ('VERSION', 'Client', 'load', 'get_version')
__all__ = ('VERSION', 'Client', 'get_version')
try:
VERSION = __import__('pkg_resources') \

View File

@ -24,13 +24,13 @@ from types import FunctionType
import raven
from raven.conf import defaults
from raven.conf.remote import RemoteConfig
from raven.context import Context
from raven.exceptions import APIError, RateLimited
from raven.utils import six, json, get_versions, get_auth_header, merge_dicts
from raven.utils.encoding import to_unicode
from raven.utils.serializer import transform
from raven.utils.stacks import get_stack_info, iter_stack_frames, get_culprit
from raven.utils.urlparse import urlparse
from raven.transport.registry import TransportRegistry, default_transports
__all__ = ('Client',)
@ -117,7 +117,7 @@ class Client(object):
_registry = TransportRegistry(transports=default_transports)
def __init__(self, dsn=None, raise_send_errors=False, **options):
def __init__(self, dsn=None, raise_send_errors=False, transport=None, **options):
global Raven
o = options
@ -134,8 +134,8 @@ class Client(object):
self.error_logger = logging.getLogger('sentry.errors')
self.uncaught_logger = logging.getLogger('sentry.errors.uncaught')
self.dsns = {}
self.set_dsn(dsn, **options)
self._transport_cache = {}
self.set_dsn(dsn, transport)
self.include_paths = set(o.get('include_paths') or [])
self.exclude_paths = set(o.get('exclude_paths') or [])
@ -174,42 +174,27 @@ class Client(object):
self._context = Context()
def set_dsn(self, dsn=None, **options):
o = options
def set_dsn(self, dsn=None, transport=None):
if dsn is None and os.environ.get('SENTRY_DSN'):
msg = "Configuring Raven from environment variable 'SENTRY_DSN'"
self.logger.debug(msg)
dsn = os.environ['SENTRY_DSN']
try:
servers, public_key, secret_key, project, transport_options = self.dsns[dsn]
except KeyError:
if dsn:
# TODO: should we validate other options weren't sent?
urlparts = urlparse(dsn)
self.logger.debug(
"Configuring Raven for host: %s://%s:%s" % (urlparts.scheme,
urlparts.netloc, urlparts.path))
dsn_config = raven.load(dsn, transport_registry=self._registry)
servers = dsn_config['SENTRY_SERVERS']
project = dsn_config['SENTRY_PROJECT']
public_key = dsn_config['SENTRY_PUBLIC_KEY']
secret_key = dsn_config['SENTRY_SECRET_KEY']
transport_options = dsn_config.get('SENTRY_TRANSPORT_OPTIONS', {})
if dsn not in self._transport_cache:
if dsn is None:
result = RemoteConfig(transport=transport)
else:
servers = ()
project = None
public_key = None
secret_key = None
transport_options = {}
self.dsns[dsn] = servers, public_key, secret_key, project, transport_options
result = RemoteConfig.from_string(
dsn,
transport=transport,
transport_registry=self._registry,
)
self._transport_cache[dsn] = result
self.remote = result
else:
self.remote = self._transport_cache[dsn]
self.servers = servers
self.public_key = public_key
self.secret_key = secret_key
self.project = project or defaults.PROJECT
self.transport_options = transport_options
self.logger.debug("Configuring Raven for host: {0}".format(self.remote))
@classmethod
def register_scheme(cls, scheme, transport_class):
@ -252,14 +237,6 @@ class Client(object):
def get_handler(self, name):
return self.module_cache[name](self)
def _get_public_dsn(self):
url = urlparse(self.servers[0])
netloc = url.hostname
if url.port:
netloc += ':%s' % url.port
path = url.path.replace('api/%s/store/' % (self.project,), self.project)
return '//%s@%s%s' % (self.public_key, netloc, path)
def get_public_dsn(self, scheme=None):
"""
Returns a public DSN which is consumable by raven-js
@ -272,7 +249,7 @@ class Client(object):
"""
if not self.is_enabled():
return
url = self._get_public_dsn()
url = self.remote.get_public_dsn()
if not scheme:
return url
return '%s:%s' % (scheme, url)
@ -397,7 +374,7 @@ class Client(object):
data['extra'][k] = self.transform(v)
# It's important date is added **after** we serialize
data.setdefault('project', self.project)
data.setdefault('project', self.remote.project)
data.setdefault('timestamp', date or datetime.utcnow())
data.setdefault('time_spent', time_spent)
data.setdefault('event_id', event_id)
@ -532,7 +509,7 @@ class Client(object):
Return a boolean describing whether the client should attempt to send
events.
"""
return bool(self.servers)
return self.remote.is_active()
def _successful_send(self):
self.state.set_success()
@ -590,9 +567,7 @@ class Client(object):
self._failed_send(e, url, self.decode(data))
try:
parsed = urlparse(url)
transport = self._registry.get_transport(
parsed, **self.transport_options)
transport = self.remote.get_transport()
if transport.async:
transport.async_send(data, headers, self._successful_send,
failed_send)
@ -626,18 +601,22 @@ class Client(object):
protocol=self.protocol_version,
timestamp=timestamp,
client=client_string,
api_key=self.public_key,
api_secret=self.secret_key,
api_key=self.remote.public_key,
api_secret=self.remote.secret_key,
)
for url in self.servers:
headers = {
'User-Agent': client_string,
'X-Sentry-Auth': auth_header,
'Content-Type': 'application/octet-stream',
}
headers = {
'User-Agent': client_string,
'X-Sentry-Auth': auth_header,
'Content-Type': 'application/octet-stream',
}
self.send_remote(url=url, data=message, headers=headers)
self.send_remote(
url=self.remote.store_endpoint,
data=message,
headers=headers,
**kwargs
)
def encode(self, data):
"""

View File

@ -8,9 +8,8 @@ raven.conf
from __future__ import absolute_import
import logging
from raven.utils.urlparse import urlparse
__all__ = ('load', 'setup_logging')
__all__ = ['setup_logging']
EXCLUDE_LOGGER_DEFAULTS = (
'raven',
@ -21,42 +20,6 @@ EXCLUDE_LOGGER_DEFAULTS = (
)
# TODO (vng): this seems weirdly located in raven.conf. Seems like
# it's really a part of raven.transport.TransportRegistry
# Not quite sure what to do with this
def load(dsn, scope=None, transport_registry=None):
"""
Parses a Sentry compatible DSN and loads it
into the given scope.
>>> import raven
>>> dsn = 'https://public_key:secret_key@sentry.local/project_id'
>>> # Apply configuration to local scope
>>> raven.load(dsn, locals())
>>> # Return DSN configuration
>>> options = raven.load(dsn)
"""
if not transport_registry:
from raven.transport import TransportRegistry, default_transports
transport_registry = TransportRegistry(default_transports)
url = urlparse(dsn)
if not transport_registry.supported_scheme(url.scheme):
raise ValueError('Unsupported Sentry DSN scheme: %r' % url.scheme)
if scope is None:
scope = {}
scope_extras = transport_registry.compute_scope(url, scope)
scope.update(scope_extras)
return scope
def setup_logging(handler, exclude=EXCLUDE_LOGGER_DEFAULTS):
"""
Configures logging to pipe to Sentry.

96
raven/conf/remote.py Normal file
View File

@ -0,0 +1,96 @@
from __future__ import absolute_import
import warnings
from raven.exceptions import InvalidDsn
from raven.transport.threaded import ThreadedHTTPTransport
from raven.utils import six
from raven.utils.urlparse import parse_qsl, urlparse
ERR_UNKNOWN_SCHEME = 'Unsupported Sentry DSN scheme: {0}'
DEFAULT_TRANSPORT = ThreadedHTTPTransport
class RemoteConfig(object):
def __init__(self, base_url=None, project=None, public_key=None,
secret_key=None, transport=None, options=None):
if base_url:
base_url = base_url.rstrip('/')
store_endpoint = '%s/api/%s/store/' % (base_url, project)
else:
store_endpoint = None
self.base_url = base_url
self.project = project
self.public_key = public_key
self.secret_key = secret_key
self.options = options or {}
self.store_endpoint = store_endpoint
self._transport_cls = transport or DEFAULT_TRANSPORT
def __unicode__(self):
return six.text_type(self.base_url)
def is_active(self):
return all([self.base_url, self.project, self.public_key, self.secret_key])
# TODO(dcramer): we dont want transports bound to a URL
def get_transport(self):
if not self.store_endpoint:
return
if not hasattr(self, '_transport'):
parsed = urlparse(self.store_endpoint)
self._transport = self._transport_cls(parsed, **self.options)
return self._transport
def get_public_dsn(self):
url = urlparse(self.base_url)
netloc = url.hostname
if url.port:
netloc += ':%s' % url.port
return '//%s@%s%s/%s' % (self.public_key, netloc, url.path, self.project)
@classmethod
def from_string(cls, value, transport=None, transport_registry=None):
url = urlparse(value)
if url.scheme not in ('http', 'https'):
warnings.warn('Transport selection via DSN is deprecated. You should explicitly pass the transport class to Client() instead.')
if transport is None:
if not transport_registry:
from raven.transport import TransportRegistry, default_transports
transport_registry = TransportRegistry(default_transports)
if not transport_registry.supported_scheme(url.scheme):
raise InvalidDsn(ERR_UNKNOWN_SCHEME.format(url.scheme))
transport = transport_registry.get_transport_cls(url.scheme)
netloc = url.hostname
if url.port:
netloc += ':%s' % url.port
path_bits = url.path.rsplit('/', 1)
if len(path_bits) > 1:
path = path_bits[0]
else:
path = ''
project = path_bits[-1]
if not all([netloc, project, url.username, url.password]):
raise InvalidDsn('Invalid Sentry DSN: %r' % url.geturl())
base_url = '%s://%s%s' % (url.scheme, netloc, path)
return cls(
base_url=base_url,
project=project,
public_key=url.username,
secret_key=url.password,
options=dict(parse_qsl(url.query)),
transport=transport,
)

View File

@ -159,7 +159,7 @@ class DjangoClient(Client):
if is_http_request and result:
# attach the sentry object to the request
request.sentry = {
'project_id': data.get('project', self.project),
'project_id': data.get('project', self.remote.project),
'id': self.get_ident(result),
}

View File

@ -41,7 +41,7 @@ class Sentry404CatchMiddleware(object):
return
request.sentry = {
'project_id': data.get('project', client.project),
'project_id': data.get('project', client.remote.project),
'id': client.get_ident(result),
}
return response

View File

@ -22,11 +22,7 @@ class Sentry(Middleware):
def __init__(self, app, config, client_cls=Client):
client = client_cls(
dsn=config.get('sentry.dsn'),
servers=list_from_setting(config, 'sentry.servers'),
name=config.get('sentry.name'),
public_key=config.get('sentry.public_key'),
secret_key=config.get('sentry.secret_key'),
project=config.get('sentry.project'),
site=config.get('sentry.site'),
include_paths=list_from_setting(config, 'sentry.include_paths'),
exclude_paths=list_from_setting(config, 'sentry.exclude_paths'),

View File

@ -7,13 +7,10 @@ raven.contrib.tornado
"""
from __future__ import absolute_import
import time
import raven
from raven.base import Client
from raven.utils import get_auth_header
from tornado.httpclient import AsyncHTTPClient, HTTPError
from raven.base import Client
class AsyncSentryClient(Client):
"""A mixin class that could be used along with request handlers to
@ -49,35 +46,6 @@ class AsyncSentryClient(Client):
return self.send_encoded(message, auth_header=auth_header, callback=callback)
def send_encoded(self, message, auth_header=None, **kwargs):
"""
Given an already serialized message, signs the message and passes the
payload off to ``send_remote`` for each server specified in the servers
configuration.
callback can be specified as a keyword argument
"""
if not auth_header:
timestamp = time.time()
auth_header = get_auth_header(
protocol=self.protocol_version,
timestamp=timestamp,
client='raven-python/%s' % (raven.VERSION,),
api_key=self.public_key,
api_secret=self.secret_key,
)
for url in self.servers:
headers = {
'X-Sentry-Auth': auth_header,
'Content-Type': 'application/octet-stream',
}
self.send_remote(
url=url, data=message, headers=headers,
callback=kwargs.get('callback', None)
)
def send_remote(self, url, data, headers=None, callback=None):
if headers is None:
headers = {}

View File

@ -20,3 +20,11 @@ class RateLimited(APIError):
class InvalidGitRepository(Exception):
pass
class ConfigurationError(ValueError):
pass
class InvalidDsn(ConfigurationError):
pass

View File

@ -49,8 +49,9 @@ def send_test_message(client, options):
sys.stdout.write(' %-15s: %s\n' % (k, getattr(client, k)))
sys.stdout.write('\n')
if not all([client.servers, client.project, client.public_key, client.secret_key]):
sys.stdout.write("Error: All values must be set!\n")
remote_config = client.remote
if not remote_config.is_active():
sys.stdout.write("Error: DSN configuration is not valid!\n")
sys.exit(1)
if not client.is_enabled():

View File

@ -8,7 +8,6 @@ raven.transport.base
from __future__ import absolute_import
from raven.transport.exceptions import InvalidScheme
from raven.utils.compat import urlparse
class Transport(object):
@ -36,38 +35,6 @@ class Transport(object):
"""
raise NotImplementedError
def compute_scope(self, url, scope):
"""
You need to override this to compute the SENTRY specific
additions to the variable scope. See the HTTPTransport for an
example.
"""
netloc = url.hostname
if url.port:
netloc += ':%s' % url.port
path_bits = url.path.rsplit('/', 1)
if len(path_bits) > 1:
path = path_bits[0]
else:
path = ''
project = path_bits[-1]
if not all([netloc, project, url.username, url.password]):
raise ValueError('Invalid Sentry DSN: %r' % url.geturl())
server = '%s://%s%s/api/%s/store/' % (
url.scheme, netloc, path, project)
scope.update({
'SENTRY_SERVERS': [server],
'SENTRY_PROJECT': project,
'SENTRY_PUBLIC_KEY': url.username,
'SENTRY_SECRET_KEY': url.password,
'SENTRY_TRANSPORT_OPTIONS': dict(urlparse.parse_qsl(url.query)),
})
return scope
class AsyncTransport(Transport):
"""

View File

@ -63,6 +63,9 @@ class TransportRegistry(object):
self._transports[full_url] = self._schemes[parsed_url.scheme](parsed_url, **options)
return self._transports[full_url]
def get_transport_cls(self, scheme):
return self._schemes[scheme]
def compute_scope(self, url, scope):
"""
Compute a scope dictionary. This may be overridden by custom

View File

@ -15,3 +15,4 @@ def register_scheme(scheme):
urlparse = _urlparse.urlparse
parse_qsl = _urlparse.parse_qsl

View File

@ -133,7 +133,7 @@ class ClientTest(TestCase):
self.assertEquals(client.state.status, client.state.ONLINE)
self.assertEqual(client.state.retry_after, 0)
@mock.patch('raven.base.Client._registry.get_transport')
@mock.patch('raven.conf.remote.RemoteConfig.get_transport')
@mock.patch('raven.base.ClientState.should_try')
def test_async_send_remote_failover(self, should_try, get_transport):
should_try.return_value = True
@ -238,37 +238,11 @@ class ClientTest(TestCase):
self.assertTrue(type(encoded), str)
self.assertEquals(data, self.client.decode(encoded))
def test_dsn(self):
client = Client(dsn='http://public:secret@example.com/1')
self.assertEquals(client.servers, ['http://example.com/api/1/store/'])
self.assertEquals(client.project, '1')
self.assertEquals(client.public_key, 'public')
self.assertEquals(client.secret_key, 'secret')
def test_dsn_as_first_arg(self):
client = Client('http://public:secret@example.com/1')
self.assertEquals(client.servers, ['http://example.com/api/1/store/'])
self.assertEquals(client.project, '1')
self.assertEquals(client.public_key, 'public')
self.assertEquals(client.secret_key, 'secret')
def test_slug_in_dsn(self):
client = Client('http://public:secret@example.com/slug-name')
self.assertEquals(client.servers, ['http://example.com/api/slug-name/store/'])
self.assertEquals(client.project, 'slug-name')
self.assertEquals(client.public_key, 'public')
self.assertEquals(client.secret_key, 'secret')
def test_get_public_dsn(self):
client = Client('threaded+http://public:secret@example.com/1')
client = Client('http://public:secret@example.com/1')
public_dsn = client.get_public_dsn()
self.assertEquals(public_dsn, '//public@example.com/1')
def test_get_public_dsn_override_scheme(self):
client = Client('threaded+http://public:secret@example.com/1')
public_dsn = client.get_public_dsn('https')
self.assertEquals(public_dsn, 'https://public@example.com/1')
def test_explicit_message_on_message_event(self):
self.client.captureMessage(message='test', data={
'message': 'foo'

119
tests/conf/tests.py Normal file
View File

@ -0,0 +1,119 @@
from __future__ import with_statement
import logging
import mock
from raven.conf import setup_logging
from raven.conf.remote import RemoteConfig
from raven.exceptions import InvalidDsn
from raven.utils.testutils import TestCase
class RemoteConfigTest(TestCase):
def test_path(self):
dsn = 'https://foo:bar@sentry.local/app/1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'https://sentry.local/app'
assert res.store_endpoint == 'https://sentry.local/app/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {}
def test_http(self):
dsn = 'http://foo:bar@sentry.local/1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'http://sentry.local'
assert res.store_endpoint == 'http://sentry.local/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {}
def test_http_with_port(self):
dsn = 'http://foo:bar@sentry.local:9000/1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'http://sentry.local:9000'
assert res.store_endpoint == 'http://sentry.local:9000/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {}
def test_https(self):
dsn = 'https://foo:bar@sentry.local/1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'https://sentry.local'
assert res.store_endpoint == 'https://sentry.local/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {}
def test_https_with_port(self):
dsn = 'https://foo:bar@sentry.local:9000/app/1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'https://sentry.local:9000/app'
assert res.store_endpoint == 'https://sentry.local:9000/app/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {}
def test_options(self):
dsn = 'http://foo:bar@sentry.local/1?timeout=1'
res = RemoteConfig.from_string(dsn)
assert res.project == '1'
assert res.base_url == 'http://sentry.local'
assert res.store_endpoint == 'http://sentry.local/api/1/store/'
assert res.public_key == 'foo'
assert res.secret_key == 'bar'
assert res.options == {'timeout': '1'}
def test_missing_netloc(self):
dsn = 'https://foo:bar@/1'
self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
def test_missing_project(self):
dsn = 'https://foo:bar@example.com'
self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
def test_missing_public_key(self):
dsn = 'https://:bar@example.com'
self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
def test_missing_secret_key(self):
dsn = 'https://bar@example.com'
self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
def test_invalid_scheme(self):
dsn = 'ftp://foo:bar@sentry.local/1'
self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
def test_get_public_dsn(self):
res = RemoteConfig(
base_url='http://example.com',
project='1',
public_key='public',
secret_key='secret',
)
public_dsn = res.get_public_dsn()
assert public_dsn == '//public@example.com/1'
class SetupLoggingTest(TestCase):
def test_basic_not_configured(self):
with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
logger = getLogger()
logger.handlers = []
handler = mock.Mock()
result = setup_logging(handler)
self.assertTrue(result)
def test_basic_already_configured(self):
with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
handler = mock.Mock()
logger = getLogger()
logger.handlers = [handler]
result = setup_logging(handler)
self.assertFalse(result)

View File

@ -1,128 +0,0 @@
from __future__ import with_statement
import logging
import mock
from raven.conf import load, setup_logging
from raven.utils.testutils import TestCase
class LoadTest(TestCase):
def test_basic(self):
dsn = 'https://foo:bar@sentry.local/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_path(self):
dsn = 'https://foo:bar@sentry.local/app/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['https://sentry.local/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_port(self):
dsn = 'https://foo:bar@sentry.local:9000/app/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['https://sentry.local:9000/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_scope_is_optional(self):
dsn = 'https://foo:bar@sentry.local/1'
res = load(dsn)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_http(self):
dsn = 'http://foo:bar@sentry.local/app/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['http://sentry.local/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_http_with_port(self):
dsn = 'http://foo:bar@sentry.local:9000/app/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['http://sentry.local:9000/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_options(self):
dsn = 'http://foo:bar@sentry.local:9001/1?timeout=1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
'SENTRY_SERVERS': ['http://sentry.local:9001/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
'SENTRY_TRANSPORT_OPTIONS': {'timeout': '1'},
})
def test_missing_netloc(self):
dsn = 'https://foo:bar@/1'
self.assertRaises(ValueError, load, dsn)
def test_missing_project(self):
dsn = 'https://foo:bar@example.com'
self.assertRaises(ValueError, load, dsn)
def test_missing_public_key(self):
dsn = 'https://:bar@example.com'
self.assertRaises(ValueError, load, dsn)
def test_missing_secret_key(self):
dsn = 'https://bar@example.com'
self.assertRaises(ValueError, load, dsn)
def test_invalid_scheme(self):
dsn = 'ftp://foo:bar@sentry.local/1'
self.assertRaises(ValueError, load, dsn)
class SetupLoggingTest(TestCase):
def test_basic_not_configured(self):
with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
logger = getLogger()
logger.handlers = []
handler = mock.Mock()
result = setup_logging(handler)
self.assertTrue(result)
def test_basic_already_configured(self):
with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
handler = mock.Mock()
logger = getLogger()
logger.handlers = [handler]
result = setup_logging(handler)
self.assertFalse(result)

View File

@ -6,6 +6,7 @@ from raven.base import Client
# Some internal stuff to extend the transport layer
from raven.transport import Transport
from raven.transport.exceptions import DuplicateScheme
# Simplify comparing dicts with primitive values:
from raven.utils import json
@ -38,7 +39,7 @@ class TransportTest(TestCase):
def setUp(self):
try:
Client.register_scheme('mock', DummyScheme)
except:
except DuplicateScheme:
pass
def test_basic_config(self):
@ -46,7 +47,7 @@ class TransportTest(TestCase):
dsn="mock://some_username:some_password@localhost:8143/1?timeout=1",
name="test_server"
)
assert c.transport_options == {
assert c.remote.options == {
'timeout': '1',
}
@ -56,10 +57,9 @@ class TransportTest(TestCase):
data = dict(a=42, b=55, c=list(range(50)))
c.send(**data)
expected_message = zlib.decompress(base64.b64decode(c.encode(data)))
self.assertIn('mock://localhost:8143/api/1/store/', Client._registry._transports)
mock_cls = Client._registry._transports['mock://localhost:8143/api/1/store/']
mock_cls = c._transport_cache['mock://some_username:some_password@localhost:8143/1'].get_transport()
expected_message = zlib.decompress(base64.b64decode(c.encode(data)))
actual_message = zlib.decompress(base64.b64decode(mock_cls._data))
# These loads()/dumps() pairs order the dict keys before comparing the string.