diff options
| author | David Cramer <dcramer@gmail.com> | 2013-12-09 23:26:27 -0800 |
|---|---|---|
| committer | David Cramer <dcramer@gmail.com> | 2013-12-09 23:29:28 -0800 |
| commit | 02df9576cf334d80fe14ef6a374b2a2a86ae4bb2 (patch) | |
| tree | feb32fab7789bad7ac25af06d2f63c55008fcf4b | |
| parent | 09bae5754798a0e3509d6cf4f3b9d688bb7db975 (diff) | |
| download | raven-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.py | 7 | ||||
| -rw-r--r-- | raven/transport/__init__.py | 17 | ||||
| -rw-r--r-- | raven/transport/base.py | 347 | ||||
| -rw-r--r-- | raven/transport/eventlet.py | 49 | ||||
| -rw-r--r-- | raven/transport/gevent.py | 55 | ||||
| -rw-r--r-- | raven/transport/http.py | 41 | ||||
| -rw-r--r-- | raven/transport/registry.py | 12 | ||||
| -rw-r--r-- | raven/transport/requests.py | 33 | ||||
| -rw-r--r-- | raven/transport/threaded.py | 5 | ||||
| -rw-r--r-- | raven/transport/tornado.py | 43 | ||||
| -rw-r--r-- | raven/transport/twisted.py | 56 | ||||
| -rw-r--r-- | raven/transport/udp.py | 78 | ||||
| -rw-r--r-- | tests/base/tests.py | 2 | ||||
| -rw-r--r-- | tests/config/tests.py | 31 | ||||
| -rw-r--r-- | tests/transport/gevent/tests.py | 4 | ||||
| -rw-r--r-- | tests/transport/tests.py | 33 | ||||
| -rw-r--r-- | tests/transport/threaded/tests.py | 2 |
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') |
