Python用GraphQLライブラリStrawberryのログ出力 ~スタックトレースを出力させない方法~

FastAPIの公式ドキュメントには、[GraphQLのライブラリ](https://fastapi.tiangolo.com/advanced/graphql/?h=graphql)が紹介されています。

そこでおすすめのGraphQLのライブラリとして紹介されているのが、[Strawberry](https://strawberry.rocks/)です。

今回はStarawberryのライブラリを使ってGraphQLのAPIを実装しているときに、ログ出力で苦労したことがあったので、何が問題なのかとその解決方法を書きます。

# Strawberryについて
改めてStrawberryとはPython用のGraphQLライブラリです。Python用のGraphQLのライブラリはGrapheneが古くからあるライブラリでドキュメントを豊富です。対してStrawberryは比較的新しいライブラリです。
FastAPIのドキュメントには、Strawberryを使用することをお勧めしています。
FastAPIとの設計思想が似ているらしいです。

Strawberryはまだまだ開発中で[GitHub](https://github.com/strawberry-graphql/strawberry)では活発にcommitされています。
なのでGraphQLの機能として十分ではないところもあります。
私の場合はログ出力周りは、Starawberryの公式ドキュメントにもログ出力について詳しく書かれていないい部分で非常に苦労しました...

# スタックトレースのログ出力
ログ出力はアプリの運用段階で非常に重要になります。
ただしStrawberryの公式ドキュメント通りに実装をしていくと、いざ本番デプロイしたときのログ出力がスタックトレースまみれになってしまいます...
それはなぜかというと、FastAPIが使用するASGIサーバーとStrawberryの仕様に問題があるからです.

例えばユーザー一覧を取得する処理があるとして、取得できなかった場合はraiseで例外を発生させればStarawberryがキャッチしてスキーマに入力する処理をせずにGraphQLの画面に結果を返してくれるので、ハンドリングが簡単です。ですが、コンソールは...スタックトレースまみれ...
```
Traceback (most recent call last):
  File "/root/.cache/pypoetry/virtualenvs/app-9TtSrW0h-py3.9/lib/python3.9/site-packages/graphql/execution/execute.py", line 625, in await_result
    return_type, field_nodes, info, path, await result
  File "/root/.cache/pypoetry/virtualenvs/app-9TtSrW0h-py3.9/lib/python3.9/site-packages/strawberry/extensions/directives.py", line 19, in resolve
    result = await await_maybe(_next(root, info, *args, **kwargs))
  File "/root/.cache/pypoetry/virtualenvs/app-9TtSrW0h-py3.9/lib/python3.9/site-packages/strawberry/schema/schema_converter.py", line 392, in _resolver
    return _get_result(_source, strawberry_info, **kwargs)
  File "/root/.cache/pypoetry/virtualenvs/app-9TtSrW0h-py3.9/lib/python3.9/site-packages/strawberry/schema/schema_converter.py", line 384, in _get_result
```

これはraiseをASGIサーバーもキャッチしてしまうからです。Strawberry的には正常な処理なのにASGIサーバー的には異常な処理になってしまうズレがあるからです。


# スタックトレースを出力させない方法

スタックトレースを出力させないようにするには、`Extension`と`GraphQLRouter`を継承してカスタマイズする必要があります。
完成コードは以下。

```
import strawberry

from fastapi import FastAPI, Request
from strawberry.fastapi import GraphQLRouter
from strawberry.http import GraphQLHTTPResponse, process_result
from strawberry.types import ExecutionResult
from graphql.error.graphql_error import GraphQLError

from strawberry.extensions import Extension

class MyExtension(Extension):
    def get_results(self):
        # resolver, mutationの処理が終わるとGraphQLRouterの処理の前にここの処理が実行される
        # エラーが格納されているか判定
        errors = self.execution_context.result.errors
        if errors is not None and len(errors) > 0:
            # errosにGraphQLErrorが格納されているとGraphQLRouterの処理に移る時にトレースバックが出力されるためdataに移し替え
            self.execution_context.result.data = self.execution_context.result.errors
            self.execution_context.result.errors = []


class MyGraphQLRouter(GraphQLRouter):
  async def process_result(
        self, request: Request, result: ExecutionResult
    ) -> GraphQLHTTPResponse:
        if type(result.data[0]) is GraphQLError:
            # dataに移し替えたGraphQLErrorを再度errorsに移し替え
            result.errors = result.data
            result.errors[0].message = result.data
            result.data = None
        # GrphQLのレスポンスフォーマットに変換する
        data: GraphQLHTTPResponse = process_result(result)
        return data

@strawberry.type
class Query:
    @strawberry.field
    def users(self) -> str:
        users = get_users()
        if len(users) == 0:
            Exception("ユーザー一覧を取得できませんでした") 
        return "ユーザー一覧取得しました"

schema = strawberry.Schema(query=Query, extensions=[MyExtension])
graphql_app = MyGraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
```

`Extension`では、resolver、mutationの処理が終わった後に何か処理を差し込むことができます。
`GraphQLRouter`ではレスポンスを作成する時に処理を差し込むことができます。
処理の順番は`Extensionの後にGraphQLRouter`です。
そしてスタックトレースは`Extension`と`GraphQLRouter`の間で出力されます。
なのでExtensionでスタックトレースを出力させないように処理をする必要があります。
スタックトレースが出力される条件は、`self.execution_context.result.errors`にエラーが格納されていることなので、このエラーを`self.execution_context.result.data`に避難させます。

そして、このままでは正常処理としてレスポンスが返ってしまうので、`GraphQLRouter`で再度`self.execution_context.result.errors`に戻す処理を行います。

# まとめ
FastAPIがおすすめしているGraphQLのライブラリStrawberryを使った場合に、ログ出力周りでスタックトレースが出力されまくって苦労したので、回避方法をまとめました。
公式ドキュメントにも載っていないテクニックなので参考にしてくれたらうれしいです。
Strawberryの開発が進むとこの辺りも解決するはず...?