通过使用Python的GraphQL库Strawberry来禁止输出日志和堆栈跟踪

在FastAPI的官方文档中介绍了GraphQL的库。

推荐的GraphQL库是Strawberry。

这次我使用Strawberry库来实现GraphQL的API时遇到了一些困难,主要是关于日志输出的问题。下面我将阐述具体问题及其解决方法。

关于草莓的事情

改写后的句子:改一改,Strawberry就是Python用的GraphQL库。Python用的GraphQL库中,Graphene是一个文档全面而且历史悠久的库。相比之下,Strawberry是一个相对较新的库。
FastAPI的文档推荐使用Strawberry。
听说它的设计理念与FastAPI很相似。

草莓仍在积极开发中,在GitHub上有频繁的提交记录。因此,作为GraphQL的功能还不够充分。就我个人而言,日志输出方面,我遇到了一些麻烦,因为草莓官方文档没有详细介绍日志输出。

输出堆栈跟踪日志

在应用程序的运维阶段,日志输出非常重要。
然而,如果按照Strawberry的官方文档来实施,那么在进行生产部署时,日志输出会被堆满堆栈跟踪信息…
这是因为FastAPI使用的ASGI服务器与Strawberry的规范存在问题。

例如,假设有一段代码来获取用户列表,如果无法获取到列表,就可以通过raise语句引发异常,然后Strawberry会捕获该异常并返回结果到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

由于这个问题,raises也被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的开发进展,这个问题应该会得到解决…?

bannerAds