11

Whenever exceptions are raised they're logged in the console (and in Sentry if it's used).

Many of these exceptions are only intended to be shown to the user. For example, django-graphql-jwt raises the PermissionDenied exception for the login_required decorator.

The problem is this pollutes the console output during testing/development and logs valid errors to Sentry during production. For exceptions such as the example above, that's only intended to be shown to the user, not logged.

As a workaround I've tried writing middleware that catches any exceptions thrown:

class ExceptionFilterMiddleware:
    IGNORED_EXCEPTIONS = (
        # Local exceptions
        ValidationException,
        # Third-party exceptions
        JSONWebTokenExpired,
        PermissionDenied,
    )

    def on_error(self, error):
        if not isinstance(error, self.IGNORED_EXCEPTIONS):
            return error

    def resolve(self, next, *args, **kwargs):
        return next(*args, **kwargs).catch(self.on_error)

But if an exception is caught or not returned, it no longer populates the errors field in query/mutation output. Therefore all errors are logged, there's no way to conditionally log exceptions.

This means the only solution is to create a logging filter like the following:

def skip_valid_exceptions(record):
    """
    Skip exceptions for errors only intended to be displayed to the API user.
    """
    skip: bool = False

    if record.exc_info:
        exc_type, exc_value = record.exc_info[:2]
        skip = isinstance(exc_value, valid_exceptions)

    return not skip

But this doesn't work either because record.exc_info is None whenever an error is thrown with Graphene, therefore it's not possible to conditionally filter out exceptions based on their type.

Is there a solution for this? This seems like it'd be a common issue but I've had trouble finding any solution.

Alternatively I could just not use exceptions for displaying errors to the API user, but this would mean putting errors into the query result's data.errors field instead of errors. This is a standard and requires the front-end logic to be adapted (such as Apollo's error handling), which isn't ideal. It also means no functionality can be used from third-party libraries (like django-graphql-jwt) that throw exceptions.

Daniel
  • 3,115
  • 5
  • 28
  • 39
  • 4
    Did you ever find a solution? I'm dealing with this now myself – Jens Astrup May 11 '20 at 18:23
  • 1
    @JensAstrup this was a pretty long time ago, but IIRC I don't think I found any solution. graphene-django has fairly poor handling for this. I opened an issue on GitHub but nothing has happened: https://github.com/graphql-python/graphene-django/issues/780 – Daniel May 13 '20 at 00:37

1 Answers1

3

Try this. First ensure that intended exceptions are GraphQLError or descendants of it.

Then create a log filter like so:

import logging
from graphql import GraphQLError

class GraphQLLogFilter(logging.Filter):
    """
    Filter GraphQL errors that are intentional.

    Any exceptions of type GraphQLError that are raised on purpose 
    to generate errors for GraphQL responses will be silenced from logs. 
    Other exceptions will be displayed so they can be tracked down.
    """
    def filter(self, record):
        if record.exc_info:
            etype, _, _ = record.exc_info
            if etype == GraphQLError:
                return None
        if record.stack_info and 'GraphQLError' in record.stack_info:
            return None
        if record.msg and 'GraphQLError' in record.msg:
            return None

        return True

Use in your settings:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    # Prevent graphql exception from displaying in console
    'filters': {
        'graphql_log_filter': {
            '()': GraphQLLogFilter,
        }
    },
    'loggers': {
        'graphql.execution.utils': {
            'level': 'WARNING',
            'handlers': ['console'],
            'filters': ['graphql_log_filter'],
        },
    },
}

Related resources:

Matt Sanders
  • 8,023
  • 3
  • 37
  • 49