PythonのGraphQLライブラリであるStrawberryを使用していて、困ったことがあります。それは、GraphQLの実行エラーがすべてエラーログに吐き出されてしまうことです。その結果、クライアントエラーやハンドリングされていない未知のエラーも含めて、全てのエラーがクラッシュレポートツール(Sentry)に報告されてしまう問題が発生しました。
ここでは、Strawberryを使用して特定のエラーのみをエラーログに出力する方法を示します。
動作を確認した環境
- Python3.9
 - Strawberry 0.187
 
解決方法
解決方法は、CustomSchemaを作成し、process_errorsをオーバーライドし、そこでエラーの種類に応じてエラーログの出力を制御することです。
class CustomSchema(Schema):
    def __init__(self, not_log_target_errors: List[Any], *args, **kwargs):
        self.not_target_errors = not_log_target_errors
        super().__init__(*args, **kwargs)
    def is_log_target_error(self, obj) -> bool:
        return not any(isinstance(obj, not_target_error) for not_target_error in self.not_target_errors)
    def process_errors(self, errors: List[GraphQLError], execution_context: Optional[ExecutionContext] = None):
        log_target_errors = [error for error in errors if self.is_log_target_error(error.original_error)]
        for log_target_error in log_target_errors:
            StrawberryLogger.error(log_target_error, execution_context)
schema = CustomSchema(query=Query, not_log_target_errors=not_log_target_errors_for_app)
以下のコードでunite test済みです。
from textwrap import dedent
import strawberry
import CustomSchema
class TestCustomSchema:
    def _get_schema_and_query(self):
        class TargetError(Exception):
            pass
        class NotTargetError1(Exception):
            pass
        class NotTargetError2(Exception):
            pass
        not_target_errors = [NotTargetError1, NotTargetError2]
        @strawberry.type
        class Query:
            @strawberry.field
            def target_error_endpoint(self) -> str:
                raise TargetError("this is TargetError message")
            @strawberry.field
            def not_target_error_endpoint_1(self) -> str:
                raise NotTargetError1("this is NotTargetError1 message")
            @strawberry.field
            def not_target_error_endpoint_2(self) -> str:
                raise NotTargetError2("this is NotTargetError2 message")
        schema = CustomSchema(query=Query, not_log_target_errors=not_target_errors)
        return schema, Query
    def test_process_errors_target_errors(self, caplog):
        schema, query = self._get_schema_and_query()
        query_target_error_endpoint = dedent(
            """
            query {
                targetErrorEndpoint
            }
        """
        ).strip()
        result = schema.execute_sync(
            query_target_error_endpoint,
            root_value=query(),
        )
        assert len(result.errors) == 1
        assert result.errors[0].message == "this is TargetError message"
        assert len(caplog.records) == 1
        record = caplog.records[0]
        assert record.levelname == "ERROR"
        assert record.name == "strawberry.execution"
        assert "this is TargetError message" in caplog.records[0].message
    def test_process_errors_not_target_errors_1(self, caplog):
        schema, query = self._get_schema_and_query()
        query_not_target_error_endpoint_1 = dedent(
            """
            query {
                notTargetErrorEndpoint1
            }
        """
        ).strip()
        result = schema.execute_sync(
            query_not_target_error_endpoint_1,
            root_value=query(),
        )
        assert len(result.errors) == 1
        assert result.errors[0].message == "this is NotTargetError1 message"
        assert len(caplog.records) == 0
    def test_process_errors_not_target_errors_2(self, caplog):
        schema, query = self._get_schema_and_query()
        query_not_target_error_endpoint_2 = dedent(
            """
            query {
                notTargetErrorEndpoint2
            }
        """
        ).strip()
        result = schema.execute_sync(
            query_not_target_error_endpoint_2,
            root_value=query(),
        )
        assert len(result.errors) == 1
        assert result.errors[0].message == "this is NotTargetError2 message"
        assert len(caplog.records) == 0
参考↓↓
override する前の process_errorsです。
(出典: https://strawberry.rocks/docs/types/schema#handling-execution-errors)
GraphQLError を全てエラーログ出力する実装になっています。
# strawberry/schema/base.py
from strawberry.types import ExecutionContext
 
logger = logging.getLogger("strawberry.execution")
class BaseSchema:
    ...
    def process_errors(
        self,
        errors: List[GraphQLError],
        execution_context: Optional[ExecutionContext] = None,
    ) -> None:
        for error in errors:
            StrawberryLogger.error(error, execution_context)
# strawberry/utils/logging.py
from strawberry.types import ExecutionContext
class StrawberryLogger:
    logger: Final[logging.Logger] = logging.getLogger("strawberry.execution")
    @classmethod
    def error(
        cls,
        error: GraphQLError,
        execution_context: Optional[ExecutionContext] = None,
        # https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values
        **logger_kwargs: Any,
    ) -> None:
        # "stack_info" is a boolean; check for None explicitly
        if logger_kwargs.get("stack_info") is None:
            logger_kwargs["stack_info"] = True
        # stacklevel was added in version 3.8
        # https://docs.python.org/3/library/logging.html#logging.Logger.debug
        if sys.version_info >= (3, 8):
            logger_kwargs["stacklevel"] = 3
        cls.logger.error(error, exc_info=error.original_error, **logger_kwargs)
宣伝
最後まで読んで頂きありがとうございました。
先日、 Strawberry GraphQL にコミットできました。
Strawberry GraphQL を使っていく皆様、フォローをお願いします。
PRマージされた🎉
— nassy@PythonとAzure (@n_nassy20) December 2, 2022
文化圏が全く違う方とのやりとりめっちゃ緊張した。
例外を足しただけだが、良い経験させてもらった。 https://t.co/fiCkyNbkAe