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:
parent
23bd04541d
commit
12bb737707
8
CHANGES
8
CHANGES
|
@ -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
|
||||
-------------
|
||||
|
||||
|
|
|
@ -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
|
||||
----------------
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/']
|
||||
|
|
|
@ -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') \
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -20,3 +20,11 @@ class RateLimited(APIError):
|
|||
|
||||
class InvalidGitRepository(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigurationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDsn(ConfigurationError):
|
||||
pass
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,3 +15,4 @@ def register_scheme(scheme):
|
|||
|
||||
|
||||
urlparse = _urlparse.urlparse
|
||||
parse_qsl = _urlparse.parse_qsl
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue