Skip to content

Commit

Permalink
Add exclude_log_exceptions parameter to rpc servers
Browse files Browse the repository at this point in the history
  • Loading branch information
asvetlov committed Jul 3, 2014
1 parent 0411e54 commit 18debe5
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 37 deletions.
9 changes: 7 additions & 2 deletions aiozmq/rpc/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,14 @@ def connection_lost(self, exc):
class _BaseServerProtocol(_BaseProtocol):

def __init__(self, loop, handler, *,
translation_table=None, log_exceptions=False):
translation_table=None, log_exceptions=False,
exclude_log_exceptions=()):
super().__init__(loop, translation_table=translation_table)
if not isinstance(handler, AbstractHandler):
raise TypeError('handler must implement AbstractHandler ABC')
self.handler = handler
self.log_exceptions = log_exceptions
self.exclude_log_exceptions = exclude_log_exceptions
self.pending_waiters = set()

def connection_lost(self, exc):
Expand Down Expand Up @@ -230,8 +232,11 @@ def check_args(self, func, args, kwargs):
def try_log(self, fut, name, args, kwargs):
try:
fut.result()
except Exception:
except Exception as exc:
if self.log_exceptions:
for e in self.exclude_log_exceptions:
if isinstance(exc, e):
return
logger.exception(textwrap.dedent("""\
An exception from method %r call occurred.
args = %s
Expand Down
18 changes: 12 additions & 6 deletions aiozmq/rpc/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,38 @@ def connect_pipeline(*, connect=None, bind=None, loop=None,

@asyncio.coroutine
def serve_pipeline(handler, *, connect=None, bind=None, loop=None,
translation_table=None, log_exceptions=False):
translation_table=None, log_exceptions=False,
exclude_log_exceptions=()):
"""A coroutine that creates and connects/binds Pipeline server instance.
Usually for this function you need to use *bind* parameter, but
ZeroMQ does not forbid to use *connect*.
handler -- an object which processes incoming pipeline calls.
Usually you like to pass AttrHandler instance.
Usually you like to pass AttrHandler instance.
log_exceptions -- log exceptions from remote calls if True.
translation_table -- an optional table for custom value translators.
loop -- an optional parameter to point
ZmqEventLoop instance. If loop is None then default
event loop will be given by asyncio.get_event_loop() call.
exclude_log_exceptions -- sequence of exception classes than should not
be logged.
loop -- an optional parameter to point ZmqEventLoop instance. If
loop is None then default event loop will be given by
asyncio.get_event_loop() call.
Returns Service instance.
"""
if loop is None:
loop = asyncio.get_event_loop()

trans, proto = yield from loop.create_zmq_connection(
lambda: _ServerProtocol(loop, handler,
translation_table=translation_table,
log_exceptions=log_exceptions),
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions),
zmq.PULL, connect=connect, bind=bind)
return Service(loop, proto)

Expand Down
31 changes: 19 additions & 12 deletions aiozmq/rpc/pubsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ def connect_pubsub(*, connect=None, bind=None, loop=None,
translation_table -- an optional table for custom value translators.
loop -- an optional parameter to point
ZmqEventLoop. If loop is None then default
event loop will be given by asyncio.get_event_loop() call.
loop -- an optional parameter to point ZmqEventLoop. If loop is
None then default event loop will be given by
asyncio.get_event_loop() call.
Returns PubSubClient instance.
"""
if loop is None:
loop = asyncio.get_event_loop()
Expand All @@ -41,38 +42,44 @@ def connect_pubsub(*, connect=None, bind=None, loop=None,

@asyncio.coroutine
def serve_pubsub(handler, *, subscribe=None, connect=None, bind=None,
loop=None, translation_table=None, log_exceptions=False):
loop=None, translation_table=None, log_exceptions=False,
exclude_log_exceptions=()):
"""A coroutine that creates and connects/binds pubsub server instance.
Usually for this function you need to use *bind* parameter, but
ZeroMQ does not forbid to use *connect*.
handler -- an object which processes incoming pipeline calls.
Usually you like to pass AttrHandler instance.
Usually you like to pass AttrHandler instance.
log_exceptions -- log exceptions from remote calls if True.
subscribe -- subscription specification.
Subscribe server to topics.
Allowed parameters are str, bytes, iterable of str or bytes.
subscribe -- subscription specification. Subscribe server to
topics. Allowed parameters are str, bytes, iterable
of str or bytes.
translation_table -- an optional table for custom value translators.
loop -- an optional parameter to point
ZmqEventLoop. If loop is None then default
event loop will be given by asyncio.get_event_loop() call.
exclude_log_exceptions -- sequence of exception classes than should not
be logged.
loop -- an optional parameter to point ZmqEventLoop. If loop is
None then default event loop will be given by
asyncio.get_event_loop() call.
Returns PubSubService instance.
Raises OSError on system error.
Raises TypeError if arguments have inappropriate type.
"""
if loop is None:
loop = asyncio.get_event_loop()

transp, proto = yield from loop.create_zmq_connection(
lambda: _ServerProtocol(loop, handler,
translation_table=translation_table,
log_exceptions=log_exceptions),
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions),
zmq.SUB, connect=connect, bind=bind)
serv = PubSubService(loop, proto)
if subscribe is not None:
Expand Down
24 changes: 16 additions & 8 deletions aiozmq/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,32 +72,38 @@ def connect_rpc(*, connect=None, bind=None, loop=None,

@asyncio.coroutine
def serve_rpc(handler, *, connect=None, bind=None, loop=None,
translation_table=None, log_exceptions=False):
translation_table=None, log_exceptions=False,
exclude_log_exceptions=()):
"""A coroutine that creates and connects/binds RPC server instance.
Usually for this function you need to use *bind* parameter, but
ZeroMQ does not forbid to use *connect*.
handler -- an object which processes incoming RPC calls.
Usually you like to pass AttrHandler instance.
handler -- an object which processes incoming RPC calls. Usually
you like to pass AttrHandler instance.
log_exceptions -- log exceptions from remote calls if True.
exclude_log_exceptions -- sequence of exception classes than should not
be logged.
translation_table -- an optional table for custom value translators.
loop -- an optional parameter to point ZmqEventLoop instance. If
loop is None then default event loop will be given by
asyncio.get_event_loop call.
loop is None then default event loop will be given by
asyncio.get_event_loop call.
Returns Service instance.
"""
if loop is None:
loop = asyncio.get_event_loop()

transp, proto = yield from loop.create_zmq_connection(
lambda: _ServerProtocol(loop, handler,
translation_table=translation_table,
log_exceptions=log_exceptions),
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions),
zmq.ROUTER, connect=connect, bind=bind)
return Service(loop, proto)

Expand Down Expand Up @@ -211,10 +217,12 @@ class _ServerProtocol(_BaseServerProtocol):
RESP_SUFFIX = struct.Struct('=Ld?')

def __init__(self, loop, handler, *,
translation_table=None, log_exceptions=False):
translation_table=None, log_exceptions=False,
exclude_log_exceptions=()):
super().__init__(loop, handler,
translation_table=translation_table,
log_exceptions=log_exceptions)
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions)
self.prefix = self.RESP_PREFIX.pack(os.getpid() % 0x10000,
random.randrange(0x10000))

Expand Down
38 changes: 35 additions & 3 deletions docs/rpc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ The basic usage is::
Usually you like to pass :class:`AttrHandler` instance.

:param bool log_exceptions:
log exceptions from remote calls if *True*.
log exceptions from remote calls if ``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

:param sequence exclude_log_exceptions: sequence of exception
types that should not to be logged if *log_exceptions* is
``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

Expand Down Expand Up @@ -240,7 +246,13 @@ The basic usage is::
Usually you like to pass :class:`AttrHandler` instance.

:param bool log_exceptions:
log exceptions from remote calls if *True*.
log exceptions from remote calls if ``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

:param sequence exclude_log_exceptions: sequence of exception
types that should not to be logged if *log_exceptions* is
``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

Expand Down Expand Up @@ -340,7 +352,13 @@ The basic usage is::
Usually you like to pass :class:`AttrHandler` instance.

:param bool log_exceptions:
log exceptions from remote calls if *True*.
log exceptions from remote calls if ``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

:param sequence exclude_log_exceptions: sequence of exception
types that should not to be logged if *log_exceptions* is
``True``.

.. seealso:: :ref:`aiozmq-rpc-log-exceptions`

Expand Down Expand Up @@ -666,6 +684,20 @@ If, say, you make PubSub server as::
then exceptions raised from *handler* remote calls will be logged by
standard :attr:`aiozmq.rpc.logger`.

But sometimes you don't want to log exceptions of some types.

Say, you use your own exceptions as part of public API to report about
expected failures. In this case you probably want to pass that
exceptions over the log, but record all other unexpected errors.

For that case you can use *exclude_log_exceptions* parameter::

server = yield from rpc.serve_rpc(handler,
bind='tcp://127.0.0.1:7777',
log_exceptions=True,
exclude_log_exceptions=(MyError,
OtherError))


Exceptions
--------------
Expand Down
6 changes: 4 additions & 2 deletions tests/rpc_pipeline_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,17 @@ def close(self, service):
def exception_handler(self, loop, context):
self.err_queue.put_nowait(context)

def make_pipeline_pair(self, log_exceptions=False):
def make_pipeline_pair(self, log_exceptions=False,
exclude_log_exceptions=()):

@asyncio.coroutine
def create():
server = yield from aiozmq.rpc.serve_pipeline(
MyHandler(self.queue, self.loop),
bind='tcp://127.0.0.1:*',
loop=self.loop,
log_exceptions=log_exceptions)
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions)
connect = next(iter(server.transport.bindings()))
client = yield from aiozmq.rpc.connect_pipeline(
connect=connect,
Expand Down
6 changes: 4 additions & 2 deletions tests/rpc_pubsub_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def close(self, service):
service.close()
self.loop.run_until_complete(service.wait_closed())

def make_pubsub_pair(self, subscribe=None, log_exceptions=False):
def make_pubsub_pair(self, subscribe=None, log_exceptions=False,
exclude_log_exceptions=()):

@asyncio.coroutine
def create():
Expand All @@ -87,7 +88,8 @@ def create():
subscribe=subscribe,
bind='tcp://127.0.0.1:*',
loop=self.loop,
log_exceptions=log_exceptions)
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions)
connect = next(iter(server.transport.bindings()))
client = yield from aiozmq.rpc.connect_pubsub(
connect=connect,
Expand Down
30 changes: 28 additions & 2 deletions tests/rpc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def cancelled_fut(self):
ret.cancel()
return ret

@aiozmq.rpc.method
def exc2(self, arg):
raise ValueError("bad arg", arg)


class Protocol(aiozmq.ZmqProtocol):

Expand Down Expand Up @@ -130,15 +134,16 @@ def close(self, server):
self.loop.run_until_complete(server.wait_closed())

def make_rpc_pair(self, *, error_table=None, timeout=None,
log_exceptions=False):
log_exceptions=False, exclude_log_exceptions=()):
@asyncio.coroutine
def create():
port = find_unused_port()
server = yield from aiozmq.rpc.serve_rpc(
MyHandler(self.loop),
bind='tcp://127.0.0.1:{}'.format(port),
loop=self.loop,
log_exceptions=log_exceptions)
log_exceptions=log_exceptions,
exclude_log_exceptions=exclude_log_exceptions)
client = yield from aiozmq.rpc.connect_rpc(
connect='tcp://127.0.0.1:{}'.format(port),
loop=self.loop, error_table=error_table, timeout=timeout)
Expand Down Expand Up @@ -627,6 +632,27 @@ def communicate():

self.loop.run_until_complete(communicate())

@mock.patch('aiozmq.rpc.base.logger')
def test_exclude_log_exceptions(self, m_log):
client, server = self.make_rpc_pair(
log_exceptions=True,
exclude_log_exceptions=(MyException,))

@asyncio.coroutine
def communicate():
with self.assertRaises(RuntimeError):
yield from client.call.exc(1)
m_log.exception.assert_called_with(
'An exception from method %r call occurred.\n'
'args = %s\nkwargs = %s\n',
mock.ANY, mock.ANY, mock.ANY)
m_log.reset_mock()
with self.assertRaises(ValueError):
yield from client.call.exc2()
self.assertFalse(m_log.called)

self.loop.run_until_complete(communicate())


class AbstractHandlerTests(unittest.TestCase):

Expand Down

0 comments on commit 18debe5

Please sign in to comment.