GraphQL-Java で operationName を必須にする

# モチベーション
GraphQL API に対して NewRelic や Datadog などの APM 製品を導入したときに問題になるのが、トレース名の問題です。
REST API であれば HTTP メソッドと PATH (URL) 単位でトレースをグルーピングできますが、
GraphQL はエンドポイントの PATH だけではクライアントが何をしたいか判別できず、通常はクエリの内容を見てみないとわかりません。[^1]

ここで役に立つのが GraphQL のオペレーション名 (operationName) です。
このパラメータには、常識的にはクライアントがやりたいことを端的に表した名称が入る(はず)なので、これをクエリの識別子として利用できそうだと考えました。

:::note warn
悪意のあるクライアントがカーディナリティの高い文字列を指定したり、クッッッソ長い文字列を指定したり、APM 側でインジェクション系の問題に繋がるような文字列を指定することは **考えないものとする**。気になる人は対策してネ
:::

[GraphQL.org](https://graphql.org) にもこんな記載があります。
トレーサビリティの観点でいろいろ便利なんですよね。

https://graphql.org/learn/queries/#operation-name より：

> The operation name is a meaningful and explicit name for your operation. It is only required in multi-operation documents, but its use is encouraged because it is very helpful for debugging and server-side logging.

[^1]: トレース名をグルーピング化することの有用性と、そのカーディナリティが多くなりすぎることの問題点は「MGI (Metric Grouping Issues)」として NewRelic のサイトで解説されています。
https://docs.newrelic.com/jp/docs/new-relic-solutions/solve-common-issues/troubleshooting/metric-grouping-issues/


# そもそも operationName の仕様って・・・？

operationName は、[一つのクエリに複数のオペレーションが含まれている場合に必須](https://graphql.org/learn/serving-over-http/#post-request)とあります。

https://graphql.org/learn/serving-over-http/#post-request

本来任意の項目を必須に変えることで、 GraphQL API の標準的な振る舞いから逸脱することになります。
例えば Graph<i>i</i>QL などのツールから無名クエリを発行したらエラーになるので、既にそういったエコシステムと組み合わせて API を運用している場合は、導入時に確認が必要でしょう。
イントラネット内で使うような API なら調整できそうですが、一般公開されている API に適用する場合は注意してください。



# 実装
前置きが長くなりましたが・・・
とにかく私は operationName を必須にしたかったので実装を用意しました。

[Spring for GraphQL (spring-graphql)](https://github.com/spring-projects/spring-graphql) を使っている場合は、これを `@Bean` として登録すれば機能します。
(spring-graphql `v1.1.2` で確認)

```java:RequiredOperationNameInstrumentation.java
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();
  }
}
```

# 実行結果
```shell
### 無名クエリの場合
$ 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 クライアントに operationName の指定を強制する Instrumentation の実装をご紹介しました。