FastAPIの公式ドキュメントには、GraphQLのライブラリが紹介されています。
そこでおすすめのGraphQLのライブラリとして紹介されているのが、Strawberryです。
今回はStarawberryのライブラリを使ってGraphQLのAPIを実装しているときに、ログ出力で苦労したことがあったので、何が問題なのかとその解決方法を書きます。
Strawberryについて
改めてStrawberryとはPython用のGraphQLライブラリです。Python用のGraphQLのライブラリはGrapheneが古くからあるライブラリでドキュメントを豊富です。対してStrawberryは比較的新しいライブラリです。
FastAPIのドキュメントには、Strawberryを使用することをお勧めしています。
FastAPIとの設計思想が似ているらしいです。
Strawberryはまだまだ開発中でGitHubでは活発に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の開発が進むとこの辺りも解決するはず...?