[Django]-How to see exceptions raised from a channels consumer

2👍

I found a solution to my problem. I first define a decorator:

import traceback
def catch_exception(f):
    def wrapper(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except StopConsumer:
            raise
        except Exception as e:
            print(traceback.format_exc().strip('\n'), '<--- from consumer')
            raise
    return wrapper

Then I define a base class for all my consumers, that uses this decorator this way:

import inspect
class BaseConsumer(JsonWebsocketConsumer):
    def __getattribute__(self, name):
        value = object.__getattribute__(self, name)
        if inspect.ismethod(value):
            return catch_exception(value)
        return value

But 2 problems persist:

  • Exceptions normally shown appear twice
  • Other exceptions are repeated 3 or 4 times! (as if each level of class hierarchy fires)

Exemple of the first case (KeyError):

Traceback (most recent call last):
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 31, in wrapper
    result = f(owner, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 110, in refresh
    data = super().refresh.__wrapped__(self)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 73, in refresh
    pvalue = round(data['toto'] * 100, 1)
KeyError: 'toto' <--- from consumer
Exception in thread Thread-3:
Traceback (most recent call last):
  File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/home/alain/ADN/simutool/dbsimu/utils.py", line 193, in repeat
    self.repeat_func()
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 31, in wrapper
    result = f(owner, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 110, in refresh
    data = super().refresh.__wrapped__(self)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 73, in refresh
    pvalue = round(data['toto'] * 100, 1)
KeyError: 'toto'

Example of the second case (misspelled variable):

WebSocket CONNECT /ws/dbsimu/Simuflow_progress/ [127.0.0.1:55866]
Traceback (most recent call last):
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 57, in receive_json
    return getattr(self, icommand)(**data)
NameError: name 'icommand' is not defined <--- from consumer
Traceback (most recent call last):
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/.local/lib/python3.6/site-packages/channels/generic/websocket.py", line 125, in receive
    self.receive_json(self.decode_json(text_data), **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 57, in receive_json
    return getattr(self, icommand)(**data)
NameError: name 'icommand' is not defined <--- from consumer
Traceback (most recent call last):
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/.local/lib/python3.6/site-packages/channels/generic/websocket.py", line 60, in websocket_receive
    self.receive(text_data=message["text"])
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/.local/lib/python3.6/site-packages/channels/generic/websocket.py", line 125, in receive
    self.receive_json(self.decode_json(text_data), **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 19, in wrapper
    return f(*args, **kwargs)
  File "/home/alain/ADN/simutool/dbsimu/consumers.py", line 57, in receive_json
    return getattr(self, icommand)(**data)
NameError: name 'icommand' is not defined <--- from consumer

If somebody has any idea to fix that, please advise.

👤albar

7👍

Starting with albar’s answer, the solution I reached was to define a decorator like so

from functools import wraps
from logging import getLogger

from channels.exceptions import AcceptConnection, DenyConnection, StopConsumer

logger = getLogger("foo-logger")

def log_exceptions(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        try:
            return await f(*args, **kwargs)
        except (AcceptConnection, DenyConnection, StopConsumer):
            raise
        except Exception as exception:
            if not getattr(exception, "logged_by_wrapper", False):
                logger.error(
                    "Unhandled exception occurred in {}:".format(f.__qualname__),
                    exc_info=exception,
                )
                setattr(exception, "logged_by_wrapper", True)
            raise

    return wrapper

This has several improvements:

  • Uses functools.wraps which makes the wrapped function more closely resemble the original function.
  • Uses async/await syntax since I’m using Async Consumers (remove if you aren’t)
  • Doesn’t log for several exceptions django-channels raises on purpose.
  • Only logs the exception if it doesn’t have the attribute logged_by_wrapper set. This causes the exception to only be logged once since we set the attribute after logging the first time.
  • Uses python’s built-in logging module to log the errors. This automatically formats the exception and the traceback since we provide the exception in exc_info=exception.

Then instead of a base Class I defined a class decorator to apply this to a Consumer’s methods

from inspect import iscoroutinefunction

def log_consumer_exceptions(klass):
    for method_name, method in list(klass.__dict__.items()):
        if iscoroutinefunction(method):
            setattr(klass, method_name, log_exceptions(method))

    return klass

This applies log_exceptions to all async methods defined in the Consumer, though not to methods it inherits – ie only to our custom methods for the Consumer.

Leave a comment