在GraphQL-Java中,使operationName成为必需项

激励

当使用NewRelic或Datadog等APM产品对GraphQL API进行导入时,可能会遇到的问题是追踪名称的问题。
对于REST API,可以按照HTTP方法和路径(URL)对追踪进行分组,但是GraphQL仅凭端点的路径无法确定客户端想要执行的操作,通常需要查看查询的内容才能确定。

这里有一个在 GraphQL 中有用的操作名(operationName)的参数。
这个参数应该包含着客户端想要执行的操作的简洁名称(应该是的),所以我认为可以将其作为查询的标识符来使用。

请确保客户端不会故意指定具有高基数的字符串,或者给定超长的字符串,或者指定可能导致APM遭受注入攻击的字符串。有任何疑虑请采取相应措施。

在GraphQL.org上也有这样的说明。
从追踪性的角度来说,这非常方便。

从https://graphql.org/learn/queries/#operation-name 上得知:

操作名称是对操作的一个有意义且明确的名称。只有在多个操作的文档中才是必需的,但建议使用它,因为它对于调试和服务器端日志非常有帮助。

operationName 的规格是什么?

如果一个查询中包含多个操作,操作名是必需的。

 

通过修改任意项为必选项,将偏离GraphQL API的标准行为。
例如,如果通过GraphiQL等工具发出无名称查询,会导致错误,因此如果已经将API与该类生态系统结合使用,则在引入时需要进行确认。
如果是用于内部网络的API,可能可以进行调整,但如果要应用于公开API,则需要注意。

实施

不好意思廢話太多了,無論如何我都想要把operationName設為必需項目,所以我做了相應的實現。

如果使用了 Spring for GraphQL (spring-graphql),只需要将其作为 @Bean 进行注册即可实现功能。
(已在 spring-graphql v1.1.2 中确认)

package ore.exam.graphql;

import graphql.ErrorType;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.execution.AbortExecutionException;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.SimpleInstrumentation;
import graphql.execution.instrumentation.SimpleInstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import java.util.List;
import org.apache.commons.lang3.StringUtils;

/**
 * オペレーション名 (<a href="https://graphql.org/learn/serving-over-http/#post-request">operationName</a>)
 * を必須化する Instrumentation.<br>
 * トレーサビリティの観点でオペレーション名が付いていた方がベターなので、それをクライアント側に強制する。
 *
 * @see <a href="https://graphql.org/learn/serving-over-http/#post-request">GraphQL HTTP Request
 *     specification</a>
 * @see <a href="https://graphql.org/learn/queries/#operation-name">Operation name is very helpful
 *     for debugging and server-side logging.</a>
 */
public class RequiredOperationNameInstrumentation extends SimpleInstrumentation {
  @Override
  public InstrumentationContext<ExecutionResult> beginExecution(
      InstrumentationExecutionParameters parameters) {
    final String operationName = parameters.getOperation();
    if (StringUtils.isBlank(operationName)) {
      GraphQLError error =
          GraphqlErrorBuilder.newError()
              .errorType(ErrorType.ExecutionAborted)
              .message(
                  "Anonymous queries are not supported. You must specify an `operationName` parameter.")
              .build();
      throw new AbortExecutionException(List.of(error));
    }
    return SimpleInstrumentationContext.noOp();
  }
}

执行结果

### 無名クエリの場合
$ curl localhost:8080/graphql \
    -H 'Content-Type: application/json' \
    -d '{"query": "{foo{id}}"}' | jq .
{
  "errors": [
    {
      "message": "Anonymous queries are not supported. You must specify an `operationName` parameter.",
      "locations": [],
      "extensions": {
        "classification": "ExecutionAborted"
      }
    }
  ]
}

### マルチクエリで operationName 未指定の場合
$ curl localhost:8080/graphql \
    -H 'Content-Type: application/json' \
    -d '{"query": "query x {foo{id}} query y {foo{id}}", "operationName": ""}' | jq .
{
  "errors": [
    {
      "message": "Anonymous queries are not supported. You must specify an `operationName` parameter.",
      "locations": [],
      "extensions": {
        "classification": "ExecutionAborted"
      }
    }
  ]
}

总结

我已经介绍了如何实现一个GraphQL客户端的Instrumentation来强制指定operationName的功能。

在NewRelic的网站上,解释了将追踪名称进行分组的有用性以及卡迪纳利提过大的问题,称为”度量分组问题(MGI)”。
bannerAds