summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Cramer <dcramer@gmail.com>2013-12-09 23:26:27 -0800
committerDavid Cramer <dcramer@gmail.com>2013-12-09 23:29:28 -0800
commit02df9576cf334d80fe14ef6a374b2a2a86ae4bb2 (patch)
treefeb32fab7789bad7ac25af06d2f63c55008fcf4b
parent09bae5754798a0e3509d6cf4f3b9d688bb7db975 (diff)
downloadraven-transport-refactor.tar.gz
Refactor transport namespacetransport-refactor
- Deprecate non-DSN configurations - Break each transport into its own module space - Generically handle querystrings-as-options
-rw-r--r--raven/base.py7
-rw-r--r--raven/transport/__init__.py17
-rw-r--r--raven/transport/base.py347
-rw-r--r--raven/transport/eventlet.py49
-rw-r--r--raven/transport/gevent.py55
-rw-r--r--raven/transport/http.py41
-rw-r--r--raven/transport/registry.py12
-rw-r--r--raven/transport/requests.py33
-rw-r--r--raven/transport/threaded.py5
-rw-r--r--raven/transport/tornado.py43
-rw-r--r--raven/transport/twisted.py56
-rw-r--r--raven/transport/udp.py78
-rw-r--r--tests/base/tests.py2
-rw-r--r--tests/config/tests.py31
-rw-r--r--tests/transport/gevent/tests.py4
-rw-r--r--tests/transport/tests.py33
-rw-r--r--tests/transport/threaded/tests.py2
17 files changed, 426 insertions, 389 deletions
diff --git a/raven/base.py b/raven/base.py
index b0620c0..946d923 100644
--- a/raven/base.py
+++ b/raven/base.py
@@ -152,16 +152,20 @@ class Client(object):
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', {})
else:
+ warnings.warn('Manually configured connections are deprecated. Switch to a DSN.', DeprecationWarning)
servers = o.get('servers')
project = o.get('project')
public_key = o.get('public_key')
secret_key = o.get('secret_key')
+ transport_options = {}
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.include_paths = set(o.get('include_paths') or [])
self.exclude_paths = set(o.get('exclude_paths') or [])
@@ -525,7 +529,8 @@ class Client(object):
try:
parsed = urlparse(url)
- transport = self._registry.get_transport(parsed)
+ transport = self._registry.get_transport(
+ parsed, **self.transport_options)
if transport.async:
transport.async_send(data, headers, self._successful_send,
failed_send)
diff --git a/raven/transport/__init__.py b/raven/transport/__init__.py
index 5d0e14e..033af78 100644
--- a/raven/transport/__init__.py
+++ b/raven/transport/__init__.py
@@ -5,9 +5,18 @@ raven.transport
:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
+# TODO: deprecate this namespace and force non-default (sync + threaded) to
+# manually import/register transports somehow
from __future__ import absolute_import
-from raven.transport.base import (Transport, AsyncTransport, HTTPTransport, GeventedHTTPTransport, TwistedHTTPTransport, # NOQA
- TornadoHTTPTransport, RequestsHTTPTransport, UDPTransport, EventletHTTPTransport) # NOQA
-from raven.transport.exceptions import InvalidScheme, DuplicateScheme # NOQA
-from raven.transport.registry import TransportRegistry, default_transports # NOQA
+from raven.transport.base import * # NOQA
+from raven.transport.eventlet import * # NOQA
+from raven.transport.exceptions import * # NOQA
+from raven.transport.gevent import * # NOQA
+from raven.transport.http import * # NOQA
+from raven.transport.requests import * # NOQA
+from raven.transport.registry import * # NOQA
+from raven.transport.twisted import * # NOQA
+from raven.transport.threaded import * # NOQA
+from raven.transport.tornado import * # NOQA
+from raven.transport.udp import * # NOQA
diff --git a/raven/transport/base.py b/raven/transport/base.py
index 81aa8c9..b11dc01 100644
--- a/raven/transport/base.py
+++ b/raven/transport/base.py
@@ -1,63 +1,12 @@
"""
-raven.transport.builtins
-~~~~~~~~~~~~~~~~~~~~~~~~
+raven.transport.base
+~~~~~~~~~~~~~~~~~~~~
:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import absolute_import
-import logging
-import sys
-from raven.utils import compat, six
-
-try:
- # Google App Engine blacklists parts of the socket module, this will prevent
- # it from blowing up.
- from socket import socket, AF_INET, AF_INET6, SOCK_DGRAM, has_ipv6, getaddrinfo, error as socket_error
- has_socket = True
-except:
- has_socket = False
-
-try:
- import gevent
- # gevent 1.0bN renamed coros to lock
- try:
- from gevent.lock import Semaphore
- except ImportError:
- from gevent.coros import Semaphore # NOQA
- has_gevent = True
-except:
- has_gevent = None
-
-try:
- import twisted.web.client
- import twisted.internet.protocol
- has_twisted = True
-except:
- has_twisted = False
-
-try:
- import requests
- has_requests = True
-except:
- has_requests = False
-
-try:
- from tornado import ioloop
- from tornado.httpclient import AsyncHTTPClient, HTTPClient
- has_tornado = True
-except:
- has_tornado = False
-
-try:
- import eventlet
- from eventlet.green import urllib2 as eventlet_urllib2
- has_eventlet = True
-except:
- has_eventlet = False
-
-from raven.conf import defaults
from raven.transport.exceptions import InvalidScheme
from raven.utils.compat import urlparse
@@ -74,6 +23,7 @@ class Transport(object):
"""
async = False
+ scheme = []
def check_scheme(self, url):
if url.scheme not in self.scheme:
@@ -92,146 +42,8 @@ class Transport(object):
additions to the variable scope. See the HTTPTransport for an
example.
"""
- raise NotImplementedError
-
-
-class AsyncTransport(Transport):
- """
- All asynchronous transport implementations should subclass this
- class.
-
- You must implement a async_send method (and the compute_scope
- method as describe on the base Transport class).
- """
-
- async = True
-
- def async_send(self, data, headers, success_cb, error_cb):
- """
- Override this method for asynchronous transports. Call
- `success_cb()` if the send succeeds or `error_cb(exception)`
- if the send fails.
- """
- raise NotImplementedError
-
-
-class BaseUDPTransport(Transport):
- def __init__(self, parsed_url):
- super(BaseUDPTransport, self).__init__()
- self.check_scheme(parsed_url)
- self._parsed_url = parsed_url
-
- def _get_addr_info(self, host, port):
- """
- Selects the address to connect to, based on the supplied host/port
- information. This method prefers v4 addresses, and will only return
- a v6 address if it's the only option.
- """
- addresses = getaddrinfo(host, port)
- if has_ipv6:
- v6_addresses = [info for info in addresses if info[0] == AF_INET6]
- v4_addresses = [info for info in addresses if info[0] == AF_INET]
- if v6_addresses and not v4_addresses:
- # The only time we return a v6 address is if it's the only option
- return v6_addresses[0]
- return v4_addresses[0]
-
- def send(self, data, headers):
- auth_header = headers.get('X-Sentry-Auth')
-
- if auth_header is None:
- # silently ignore attempts to send messages without an auth header
- return
-
- host, port = self._parsed_url.netloc.rsplit(':')
- addr_info = self._get_addr_info(host, int(port))
- self._send_data(auth_header + '\n\n' + data, addr_info)
-
- def compute_scope(self, url, scope):
- path_bits = url.path.rsplit('/', 1)
- if len(path_bits) > 1:
- path = path_bits[0]
- else:
- path = ''
- project = path_bits[-1]
-
- if not all([url.port, project, url.username, url.password]):
- raise ValueError('Invalid Sentry DSN: %r' % url.geturl())
-
netloc = url.hostname
- netloc += ':%s' % url.port
-
- 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,
- })
- return scope
-
-
-class UDPTransport(BaseUDPTransport):
- scheme = ['udp']
-
- def __init__(self, parsed_url):
- super(UDPTransport, self).__init__(parsed_url)
- if not has_socket:
- raise ImportError('UDPTransport requires the socket module')
-
- def _send_data(self, data, addr_info):
- udp_socket = None
- af = addr_info[0]
- addr = addr_info[4]
- try:
- udp_socket = socket(af, SOCK_DGRAM)
- udp_socket.setblocking(False)
- udp_socket.sendto(data, addr)
- except socket_error:
- # as far as I understand things this simply can't happen,
- # but still, it can't hurt
- pass
- finally:
- # Always close up the socket when we're done
- if udp_socket is not None:
- udp_socket.close()
- udp_socket = None
-
-
-class HTTPTransport(Transport):
-
- scheme = ['http', 'https']
-
- def __init__(self, parsed_url):
- self.check_scheme(parsed_url)
-
- self._parsed_url = parsed_url
- self._url = parsed_url.geturl()
-
- opts = urlparse.parse_qs(parsed_url.query)
-
- timeout = opts.get('timeout', defaults.TIMEOUT)
- if isinstance(timeout, six.string_types):
- timeout = int(timeout)
- self.timeout = timeout
-
- def send(self, data, headers):
- """
- Sends a request to a remote webserver using HTTP POST.
- """
- req = compat.Request(self._url, headers=headers)
-
- if sys.version_info < (2, 6):
- response = compat.urlopen(req, data).read()
- else:
- response = compat.urlopen(req, data, self.timeout).read()
- return response
-
- def compute_scope(self, url, scope):
- netloc = url.hostname
- if url.port and (url.scheme, url.port) not in \
- (('http', 80), ('https', 443)):
+ if url.port:
netloc += ':%s' % url.port
path_bits = url.path.rsplit('/', 1)
@@ -246,153 +58,32 @@ class HTTPTransport(Transport):
server = '%s://%s%s/api/%s/store/' % (
url.scheme, netloc, path, project)
- if url.query:
- server += '?%s' % url.query
+
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 GeventedHTTPTransport(AsyncTransport, HTTPTransport):
-
- scheme = ['gevent+http', 'gevent+https']
-
- def __init__(self, parsed_url, maximum_outstanding_requests=100):
- if not has_gevent:
- raise ImportError('GeventedHTTPTransport requires gevent.')
- self._lock = Semaphore(maximum_outstanding_requests)
-
- super(GeventedHTTPTransport, self).__init__(parsed_url)
-
- # remove the gevent+ from the protocol, as it is not a real protocol
- self._url = self._url.split('+', 1)[-1]
-
- def async_send(self, data, headers, success_cb, failure_cb):
- """
- Spawn an async request to a remote webserver.
- """
- # this can be optimized by making a custom self.send that does not
- # read the response since we don't use it.
- self._lock.acquire()
- return gevent.spawn(
- super(GeventedHTTPTransport, self).send, data, headers
- ).link(lambda x: self._done(x, success_cb, failure_cb))
-
- def _done(self, greenlet, success_cb, failure_cb, *args):
- self._lock.release()
- if greenlet.successful():
- success_cb()
- else:
- failure_cb(greenlet.exception)
-
-
-class TwistedHTTPTransport(AsyncTransport, HTTPTransport):
-
- scheme = ['twisted+http', 'twisted+https']
-
- def __init__(self, parsed_url):
- if not has_twisted:
- raise ImportError('TwistedHTTPTransport requires twisted.web.')
-
- super(TwistedHTTPTransport, self).__init__(parsed_url)
- self.logger = logging.getLogger('sentry.errors')
-
- # remove the twisted+ from the protocol, as it is not a real protocol
- self._url = self._url.split('+', 1)[-1]
-
- def async_send(self, data, headers, success_cb, failure_cb):
- d = twisted.web.client.getPage(self._url, method='POST', postdata=data,
- headers=headers)
- d.addCallback(lambda r: success_cb())
- d.addErrback(lambda f: failure_cb(f.value))
-
-
-class TwistedUDPTransport(BaseUDPTransport):
- scheme = ['twisted+udp']
-
- def __init__(self, parsed_url):
- super(TwistedUDPTransport, self).__init__(parsed_url)
- if not has_twisted:
- raise ImportError('TwistedUDPTransport requires twisted.')
- self.protocol = twisted.internet.protocol.DatagramProtocol()
- twisted.internet.reactor.listenUDP(0, self.protocol)
-
- def _send_data(self, data, addr):
- self.protocol.transport.write(data, addr)
-
-
-class TornadoHTTPTransport(HTTPTransport):
-
- scheme = ['tornado+http']
-
- def __init__(self, parsed_url):
- if not has_tornado:
- raise ImportError('TornadoHTTPTransport requires tornado.')
-
- super(TornadoHTTPTransport, self).__init__(parsed_url)
-
- # remove the tornado+ from the protocol, as it is not a real protocol
- self._url = self._url.split('+', 1)[-1]
-
- def send(self, data, headers):
- kwargs = dict(method='POST', headers=headers, body=data)
-
- # only use async if ioloop is running, otherwise it will never send
- if ioloop.IOLoop.initialized():
- client = AsyncHTTPClient()
- kwargs['callback'] = None
- else:
- client = HTTPClient()
-
- client.fetch(self._url, **kwargs)
-
-
-class RequestsHTTPTransport(HTTPTransport):
-
- scheme = ['requests+http']
-
- def __init__(self, parsed_url):
- if not has_requests:
- raise ImportError('RequestsHTTPTransport requires requests.')
-
- super(RequestsHTTPTransport, self).__init__(parsed_url)
-
- # remove the requests+ from the protocol, as it is not a real protocol
- self._url = self._url.split('+', 1)[-1]
-
- def send(self, data, headers):
- requests.post(self._url, data=data, headers=headers)
-
-
-class EventletHTTPTransport(HTTPTransport):
-
- scheme = ['eventlet+http', 'eventlet+https']
+class AsyncTransport(Transport):
+ """
+ All asynchronous transport implementations should subclass this
+ class.
- def __init__(self, parsed_url, pool_size=100):
- if not has_eventlet:
- raise ImportError('EventletHTTPTransport requires eventlet.')
- super(EventletHTTPTransport, self).__init__(parsed_url)
- # remove the eventlet+ from the protocol, as it is not a real protocol
- self._url = self._url.split('+', 1)[-1]
+ You must implement a async_send method (and the compute_scope
+ method as describe on the base Transport class).
+ """
- def _send_payload(self, payload):
- req = eventlet_urllib2.Request(self._url, headers=payload[1])
- try:
- if sys.version_info < (2, 6):
- response = eventlet_urllib2.urlopen(req, payload[0]).read()
- else:
- response = eventlet_urllib2.urlopen(req, payload[0],
- self.timeout).read()
- return response
- except Exception as err:
- return err
+ async = True
- def send(self, data, headers):
+ def async_send(self, data, headers, success_cb, error_cb):
"""
- Spawn an async request to a remote webserver.
+ Override this method for asynchronous transports. Call
+ `success_cb()` if the send succeeds or `error_cb(exception)`
+ if the send fails.
"""
- eventlet.spawn(self._send_payload, (data, headers))
+ raise NotImplementedError
diff --git a/raven/transport/eventlet.py b/raven/transport/eventlet.py
new file mode 100644
index 0000000..d05799d
--- /dev/null
+++ b/raven/transport/eventlet.py
@@ -0,0 +1,49 @@
+"""
+raven.transport.eventlet
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+import sys
+
+from raven.transport.http import HTTPTransport
+
+try:
+ import eventlet
+ from eventlet.green import urllib2 as eventlet_urllib2
+ has_eventlet = True
+except:
+ has_eventlet = False
+
+
+class EventletHTTPTransport(HTTPTransport):
+
+ scheme = ['eventlet+http', 'eventlet+https']
+
+ def __init__(self, parsed_url, pool_size=100):
+ if not has_eventlet:
+ raise ImportError('EventletHTTPTransport requires eventlet.')
+ super(EventletHTTPTransport, self).__init__(parsed_url)
+ # remove the eventlet+ from the protocol, as it is not a real protocol
+ self._url = self._url.split('+', 1)[-1]
+
+ def _send_payload(self, payload):
+ req = eventlet_urllib2.Request(self._url, headers=payload[1])
+ try:
+ if sys.version_info < (2, 6):
+ response = eventlet_urllib2.urlopen(req, payload[0]).read()
+ else:
+ response = eventlet_urllib2.urlopen(req, payload[0],
+ self.timeout).read()
+ return response
+ except Exception as err:
+ return err
+
+ def send(self, data, headers):
+ """
+ Spawn an async request to a remote webserver.
+ """
+ eventlet.spawn(self._send_payload, (data, headers))
diff --git a/raven/transport/gevent.py b/raven/transport/gevent.py
new file mode 100644
index 0000000..76ac559
--- /dev/null
+++ b/raven/transport/gevent.py
@@ -0,0 +1,55 @@
+"""
+raven.transport.gevent
+~~~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+from raven.transport.base import AsyncTransport
+from raven.transport.http import HTTPTransport
+
+try:
+ import gevent
+ # gevent 1.0bN renamed coros to lock
+ try:
+ from gevent.lock import Semaphore
+ except ImportError:
+ from gevent.coros import Semaphore # NOQA
+ has_gevent = True
+except:
+ has_gevent = None
+
+
+class GeventedHTTPTransport(AsyncTransport, HTTPTransport):
+
+ scheme = ['gevent+http', 'gevent+https']
+
+ def __init__(self, parsed_url, maximum_outstanding_requests=100):
+ if not has_gevent:
+ raise ImportError('GeventedHTTPTransport requires gevent.')
+ self._lock = Semaphore(maximum_outstanding_requests)
+
+ super(GeventedHTTPTransport, self).__init__(parsed_url)
+
+ # remove the gevent+ from the protocol, as it is not a real protocol
+ self._url = self._url.split('+', 1)[-1]
+
+ def async_send(self, data, headers, success_cb, failure_cb):
+ """
+ Spawn an async request to a remote webserver.
+ """
+ # this can be optimized by making a custom self.send that does not
+ # read the response since we don't use it.
+ self._lock.acquire()
+ return gevent.spawn(
+ super(GeventedHTTPTransport, self).send, data, headers
+ ).link(lambda x: self._done(x, success_cb, failure_cb))
+
+ def _done(self, greenlet, success_cb, failure_cb, *args):
+ self._lock.release()
+ if greenlet.successful():
+ success_cb()
+ else:
+ failure_cb(greenlet.exception)
diff --git a/raven/transport/http.py b/raven/transport/http.py
new file mode 100644
index 0000000..973e4bd
--- /dev/null
+++ b/raven/transport/http.py
@@ -0,0 +1,41 @@
+"""
+raven.transport.http
+~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+import sys
+
+from raven.conf import defaults
+from raven.transport.base import Transport
+from raven.utils import six
+from raven.utils.compat import urlopen, Request
+
+
+class HTTPTransport(Transport):
+
+ scheme = ['http', 'https']
+
+ def __init__(self, parsed_url, timeout=defaults.TIMEOUT):
+ self.check_scheme(parsed_url)
+
+ self._parsed_url = parsed_url
+ self._url = parsed_url.geturl()
+ if isinstance(timeout, six.string_types):
+ timeout = int(timeout)
+ self.timeout = timeout
+
+ def send(self, data, headers):
+ """
+ Sends a request to a remote webserver using HTTP POST.
+ """
+ req = Request(self._url, headers=headers)
+
+ if sys.version_info < (2, 6):
+ response = urlopen(req, data).read()
+ else:
+ response = urlopen(req, data, self.timeout).read()
+ return response
diff --git a/raven/transport/registry.py b/raven/transport/registry.py
index 5eadbcf..40608fe 100644
--- a/raven/transport/registry.py
+++ b/raven/transport/registry.py
@@ -7,12 +7,16 @@ raven.transport.registry
"""
from __future__ import absolute_import
-from raven.transport.base import (
- HTTPTransport, GeventedHTTPTransport, TwistedHTTPTransport,
- TornadoHTTPTransport, UDPTransport, EventletHTTPTransport,
- RequestsHTTPTransport)
+# TODO(dcramer): we really should need to import all of these by default
+from raven.transport.eventlet import EventletHTTPTransport
from raven.transport.exceptions import DuplicateScheme
+from raven.transport.http import HTTPTransport
+from raven.transport.gevent import GeventedHTTPTransport
+from raven.transport.requests import RequestsHTTPTransport
from raven.transport.threaded import ThreadedHTTPTransport
+from raven.transport.twisted import TwistedHTTPTransport
+from raven.transport.tornado import TornadoHTTPTransport
+from raven.transport.udp import UDPTransport
from raven.utils import urlparse
diff --git a/raven/transport/requests.py b/raven/transport/requests.py
new file mode 100644
index 0000000..a0f5923
--- /dev/null
+++ b/raven/transport/requests.py
@@ -0,0 +1,33 @@
+"""
+raven.transport.requests
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+from raven.transport.http import HTTPTransport
+
+try:
+ import requests
+ has_requests = True
+except:
+ has_requests = False
+
+
+class RequestsHTTPTransport(HTTPTransport):
+
+ scheme = ['requests+http']
+
+ def __init__(self, parsed_url):
+ if not has_requests:
+ raise ImportError('RequestsHTTPTransport requires requests.')
+
+ super(RequestsHTTPTransport, self).__init__(parsed_url)
+
+ # remove the requests+ from the protocol, as it is not a real protocol
+ self._url = self._url.split('+', 1)[-1]
+
+ def send(self, data, headers):
+ requests.post(self._url, data=data, headers=headers)
diff --git a/raven/transport/threaded.py b/raven/transport/threaded.py
index 55473b5..ac08717 100644
--- a/raven/transport/threaded.py
+++ b/raven/transport/threaded.py
@@ -12,9 +12,10 @@ import logging
import time
import threading
import os
-from raven.utils.compat import Queue
-from raven.transport.base import HTTPTransport, AsyncTransport
+from raven.transport.base import AsyncTransport
+from raven.transport.http import HTTPTransport
+from raven.utils.compat import Queue
DEFAULT_TIMEOUT = 10
diff --git a/raven/transport/tornado.py b/raven/transport/tornado.py
new file mode 100644
index 0000000..9ea3a5b
--- /dev/null
+++ b/raven/transport/tornado.py
@@ -0,0 +1,43 @@
+"""
+raven.transport.tornado
+~~~~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+from raven.transport.http import HTTPTransport
+
+try:
+ from tornado import ioloop
+ from tornado.httpclient import AsyncHTTPClient, HTTPClient
+ has_tornado = True
+except:
+ has_tornado = False
+
+
+class TornadoHTTPTransport(HTTPTransport):
+
+ scheme = ['tornado+http']
+
+ def __init__(self, parsed_url):
+ if not has_tornado:
+ raise ImportError('TornadoHTTPTransport requires tornado.')
+
+ super(TornadoHTTPTransport, self).__init__(parsed_url)
+
+ # remove the tornado+ from the protocol, as it is not a real protocol
+ self._url = self._url.split('+', 1)[-1]
+
+ def send(self, data, headers):
+ kwargs = dict(method='POST', headers=headers, body=data)
+
+ # only use async if ioloop is running, otherwise it will never send
+ if ioloop.IOLoop.initialized():
+ client = AsyncHTTPClient()
+ kwargs['callback'] = None
+ else:
+ client = HTTPClient()
+
+ client.fetch(self._url, **kwargs)
diff --git a/raven/transport/twisted.py b/raven/transport/twisted.py
new file mode 100644
index 0000000..fcfbbc6
--- /dev/null
+++ b/raven/transport/twisted.py
@@ -0,0 +1,56 @@
+"""
+raven.transport.twisted
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+import logging
+
+from raven.transport.base import AsyncTransport
+from raven.transport.http import HTTPTransport
+from raven.transport.udp import BaseUDPTransport
+
+try:
+ import twisted.web.client
+ import twisted.internet.protocol
+ has_twisted = True
+except:
+ has_twisted = False
+
+
+class TwistedHTTPTransport(AsyncTransport, HTTPTransport):
+
+ scheme = ['twisted+http', 'twisted+https']
+
+ def __init__(self, parsed_url):
+ if not has_twisted:
+ raise ImportError('TwistedHTTPTransport requires twisted.web.')
+
+ super(TwistedHTTPTransport, self).__init__(parsed_url)
+ self.logger = logging.getLogger('sentry.errors')
+
+ # remove the twisted+ from the protocol, as it is not a real protocol
+ self._url = self._url.split('+', 1)[-1]
+
+ def async_send(self, data, headers, success_cb, failure_cb):
+ d = twisted.web.client.getPage(self._url, method='POST', postdata=data,
+ headers=headers)
+ d.addCallback(lambda r: success_cb())
+ d.addErrback(lambda f: failure_cb(f.value))
+
+
+class TwistedUDPTransport(BaseUDPTransport):
+ scheme = ['twisted+udp']
+
+ def __init__(self, parsed_url):
+ super(TwistedUDPTransport, self).__init__(parsed_url)
+ if not has_twisted:
+ raise ImportError('TwistedUDPTransport requires twisted.')
+ self.protocol = twisted.internet.protocol.DatagramProtocol()
+ twisted.internet.reactor.listenUDP(0, self.protocol)
+
+ def _send_data(self, data, addr):
+ self.protocol.transport.write(data, addr)
diff --git a/raven/transport/udp.py b/raven/transport/udp.py
new file mode 100644
index 0000000..d7064e6
--- /dev/null
+++ b/raven/transport/udp.py
@@ -0,0 +1,78 @@
+"""
+raven.transport.udp
+~~~~~~~~~~~~~~~~~~~
+
+:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+"""
+from __future__ import absolute_import
+
+from raven.transport.base import Transport
+
+try:
+ # Google App Engine blacklists parts of the socket module, this will prevent
+ # it from blowing up.
+ from socket import socket, AF_INET, AF_INET6, SOCK_DGRAM, has_ipv6, getaddrinfo, error as socket_error
+ has_socket = True
+except Exception:
+ has_socket = False
+
+
+class BaseUDPTransport(Transport):
+ def __init__(self, parsed_url):
+ super(BaseUDPTransport, self).__init__()
+ self.check_scheme(parsed_url)
+ self._parsed_url = parsed_url
+
+ def _get_addr_info(self, host, port):
+ """
+ Selects the address to connect to, based on the supplied host/port
+ information. This method prefers v4 addresses, and will only return
+ a v6 address if it's the only option.
+ """
+ addresses = getaddrinfo(host, port)
+ if has_ipv6:
+ v6_addresses = [info for info in addresses if info[0] == AF_INET6]
+ v4_addresses = [info for info in addresses if info[0] == AF_INET]
+ if v6_addresses and not v4_addresses:
+ # The only time we return a v6 address is if it's the only option
+ return v6_addresses[0]
+ return v4_addresses[0]
+
+ def send(self, data, headers):
+ auth_header = headers.get('X-Sentry-Auth')
+
+ if auth_header is None:
+ # silently ignore attempts to send messages without an auth header
+ return
+
+ host, port = self._parsed_url.netloc.rsplit(':')
+ addr_info = self._get_addr_info(host, int(port))
+ self._send_data(auth_header + '\n\n' + data, addr_info)
+
+
+class UDPTransport(BaseUDPTransport):
+ scheme = ['udp']
+
+ def __init__(self, parsed_url):
+ super(UDPTransport, self).__init__(parsed_url)
+ if not has_socket:
+ raise ImportError('UDPTransport requires the socket module')
+
+ def _send_data(self, data, addr_info):
+ udp_socket = None
+ af = addr_info[0]
+ addr = addr_info[4]
+ try:
+ udp_socket = socket(af, SOCK_DGRAM)
+ udp_socket.setblocking(False)
+ udp_socket.sendto(data, addr)
+ except socket_error:
+ # as far as I understand things this simply can't happen,
+ # but still, it can't hurt
+ pass
+ finally:
+ # Always close up the socket when we're done
+ if udp_socket is not None:
+ udp_socket.close()
+ udp_socket = None
diff --git a/tests/base/tests.py b/tests/base/tests.py
index 1d8bd1d..be18340 100644
--- a/tests/base/tests.py
+++ b/tests/base/tests.py
@@ -77,7 +77,7 @@ class ClientTest(TestCase):
assert base.Raven is client
assert client is not client2
- @mock.patch('raven.transport.base.HTTPTransport.send')
+ @mock.patch('raven.transport.http.HTTPTransport.send')
@mock.patch('raven.base.ClientState.should_try')
def test_send_remote_failover(self, should_try, send):
should_try.return_value = True
diff --git a/tests/config/tests.py b/tests/config/tests.py
index d60c362..7fd6dfe 100644
--- a/tests/config/tests.py
+++ b/tests/config/tests.py
@@ -15,6 +15,7 @@ class LoadTest(TestCase):
'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
+ 'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_path(self):
@@ -26,6 +27,7 @@ class LoadTest(TestCase):
'SENTRY_SERVERS': ['https://sentry.local/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
+ 'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_port(self):
@@ -37,6 +39,7 @@ class LoadTest(TestCase):
'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):
@@ -47,6 +50,7 @@ class LoadTest(TestCase):
'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
+ 'SENTRY_TRANSPORT_OPTIONS': {},
})
def test_http(self):
@@ -58,6 +62,7 @@ class LoadTest(TestCase):
'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):
@@ -69,39 +74,31 @@ class LoadTest(TestCase):
'SENTRY_SERVERS': ['http://sentry.local:9000/app/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
+ 'SENTRY_TRANSPORT_OPTIONS': {},
})
- def test_https_port_443(self):
- dsn = 'https://foo:bar@sentry.local:443/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',
- })
-
- def test_https_port_80(self):
- dsn = 'https://foo:bar@sentry.local:80/app/1'
+ def test_udp(self):
+ dsn = 'udp://foo:bar@sentry.local:9001/1'
res = {}
load(dsn, res)
self.assertEquals(res, {
'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['https://sentry.local:80/app/api/1/store/'],
+ 'SENTRY_SERVERS': ['udp://sentry.local:9001/api/1/store/'],
'SENTRY_PUBLIC_KEY': 'foo',
'SENTRY_SECRET_KEY': 'bar',
+ 'SENTRY_TRANSPORT_OPTIONS': {},
})
- def test_udp(self):
- dsn = 'udp://foo:bar@sentry.local:9001/1'
+ 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': ['udp://sentry.local:9001/api/1/store/'],
+ '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):
diff --git a/tests/transport/gevent/tests.py b/tests/transport/gevent/tests.py
index 0d58d74..78b0fb1 100644
--- a/tests/transport/gevent/tests.py
+++ b/tests/transport/gevent/tests.py
@@ -21,8 +21,8 @@ class GeventTransportTest(TestCase):
reload(socket)
reload(time)
- @mock.patch('raven.transport.base.GeventedHTTPTransport._done')
- @mock.patch('raven.transport.base.HTTPTransport.send')
+ @mock.patch('raven.transport.gevent.GeventedHTTPTransport._done')
+ @mock.patch('raven.transport.http.HTTPTransport.send')
def test_does_send(self, send, done):
self.client.captureMessage(message='foo')
time.sleep(0)
diff --git a/tests/transport/tests.py b/tests/transport/tests.py
index 344d9c8..0766c28 100644
--- a/tests/transport/tests.py
+++ b/tests/transport/tests.py
@@ -27,32 +27,6 @@ class DummyScheme(Transport):
self._data = data
self._headers = headers
- def compute_scope(self, url, scope):
- netloc = url.hostname
- 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/store/' % (url.scheme, netloc, path)
-
- # Note that these variables in the scope are not actually used
- # for anything w.r.t the DummyTransport
- scope.update({
- 'SENTRY_SERVERS': [server],
- 'SENTRY_PROJECT': project,
- 'SENTRY_PUBLIC_KEY': url.username,
- 'SENTRY_SECRET_KEY': url.password,
- })
- return scope
-
class TransportTest(TestCase):
def setUp(self):
@@ -68,12 +42,13 @@ class TransportTest(TestCase):
c.send(**data)
expected_message = c.encode(data)
- self.assertIn('mock://localhost:8143/api/store/', Client._registry._transports)
- mock_cls = Client._registry._transports['mock://localhost:8143/api/store/']
+ self.assertIn('mock://localhost:8143/api/1/store/', Client._registry._transports)
+ mock_cls = Client._registry._transports['mock://localhost:8143/api/1/store/']
assert mock_cls._data == expected_message
def test_build_then_send(self):
- c = Client(dsn="mock://some_username:some_password@localhost:8143/1",
+ c = Client(
+ dsn="mock://some_username:some_password@localhost:8143/1",
name="test_server")
mydate = datetime.datetime(2012, 5, 4, tzinfo=pytz.utc)
diff --git a/tests/transport/threaded/tests.py b/tests/transport/threaded/tests.py
index c78eff1..df787ca 100644
--- a/tests/transport/threaded/tests.py
+++ b/tests/transport/threaded/tests.py
@@ -24,7 +24,7 @@ class ThreadedTransportTest(TestCase):
dsn="threaded+http://some_username:some_password@localhost:8143/1",
)
- @mock.patch('raven.transport.base.HTTPTransport.send')
+ @mock.patch('raven.transport.http.HTTPTransport.send')
def test_does_send(self, send):
self.client.captureMessage(message='foo')