Repository: netflix/dgs-framework
Issue: bug: gql.operation.name needs to identify operation name properly

Let's look at a simple graphql query
```
query simpleschemacheck{
  __schema {
    types {
      name
    }
  }
}
```
Summarizing these 2 websites
Web1: https://graphql.org/learn/queries/#operation-name
Web2: https://graphql.org/learn/serving-over-http/#post-request
- `operationName` key within the HTTP POST query is **optional** for single queries and mandatory for multiple queries.
- in the above example, `simpleschemacheck` can be deemed as the operation name as it is a single query. We need not state this in the operationName POST Query.

## Expected behavior
`gql.operation.name` metric is anoynomous

## Actual behavior
`gql.operation.name` metric should be `simplequerycheck` as there is only 1 query.

## Steps to reproduce
- Just use my query and send it via postman.
- graphiql sends operationName even though it has 1 query. It follows the `simpleschemacheck` operation name. However, if there is no `simplequerycheck`, it will not send a `operationName` within the POST request. In this case, anonynomous is definetly correct. (Use chrome debugger and view the network requests)
- Other libraries may not send this operationName within the POST as it is an optional field even though `simpleschemacheck` is the operation name.

## What should be done
- `gql.operation.name` should do the following check.

Check if operationName is present.
```
If operationName is present - gql.operation.name will be operationName
If operationName is not present, this has to be a 1 query post request.
   Check the single query's operation name
      If query's operation name is nonempty, `gql.operation.name` is this operation name
      ElseIf the query operation name is empty, `gql.operation.name` will be anonynomous.
```
In other words, there has to be a proper mapping with operationName and query <operation_name>

## Why do we need this?
- Libraries are sending healthchecks without operationName while having a `query operationname`.
- DGS metrics is marking `gql.operation.name` as `anoynomous`. It makes metrics very hard to read as we do not know the actual operation name that is in the query and thus, we can't filter out healthchecks and normal requests.

Changed files:
graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt
graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt

diff --git a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
index 494130c7..2d9f934b 100644
--- a/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/main/kotlin/com/netflix/graphql/dgs/metrics/micrometer/DgsGraphQLMetricsInstrumentation.kt
@@ -16,6 +16,8 @@ import graphql.execution.instrumentation.SimpleInstrumentationContext.whenComple
 import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
 import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
 import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters
+import graphql.language.Document
+import graphql.language.OperationDefinition
 import graphql.schema.DataFetcher
 import graphql.schema.GraphQLNonNull
 import graphql.schema.GraphQLObjectType
@@ -162,8 +164,11 @@ class DgsGraphQLMetricsInstrumentation(
                 return@whenCompleted
             }
             val state: MetricsInstrumentationState = parameters.getInstrumentationState()
-            // compute the Query signatures.
+            // compute fields that require a document
             if (parameters.document != null) {
+                state.operationName =
+                    if (state.operationName.isPresent) state.operationName
+                    else TagUtils.resolveOperationNameFromUniqueOperationDefinition(parameters.document)
                 state.querySignature = optQuerySignatureRepository.flatMap { it.get(parameters.document, parameters) }
             }
             // compute the query complexity.
@@ -291,6 +296,19 @@ class DgsGraphQLMetricsInstrumentation(
         const val TAG_VALUE_NONE = "none"
         const val TAG_VALUE_UNKNOWN = "unknown"
 
+        fun resolveOperationNameFromUniqueOperationDefinition(document: Document): Optional<String> {
+            return if (document.definitions.isNotEmpty()) {
+                val operationDefinitions = document.definitions.filterIsInstance<OperationDefinition>()
+                if (operationDefinitions.size == 1) {
+                    ofNullable(operationDefinitions.first().name)
+                } else {
+                    empty()
+                }
+            } else {
+                empty()
+            }
+        }
+
         fun resolveDataFetcherTagValue(parameters: InstrumentationFieldFetchParameters): String {
             val type = parameters.executionStepInfo.parent.type
             val parentType = if (type is GraphQLNonNull) {
diff --git a/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt b/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt
index 7d08b816..1fed0bbe 100644
--- a/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt
+++ b/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt
@@ -93,7 +93,48 @@ class MicrometerServletSmokeTest {
     }
 
     @Test
-    fun `Metrics for a successful request`() {
+    fun `Metrics for a successful request, implicit operation name`() {
+        mvc.perform(
+            // Note that the query below uses an aliased field, aliasing `ping` to `op_name`.
+            // We will also assert that the tag reflected by the metric is not affected by the alias.
+            MockMvcRequestBuilders
+                .post("/graphql")
+                .content("""{ "query": "query my_op_1{ping}" }""")
+        ).andExpect(status().isOk)
+            .andExpect(content().json("""{"data":{"ping":"pong"}}""", false))
+
+        val meters = fetchMeters()
+
+        assertThat(meters).containsOnlyKeys("gql.query", "gql.resolver")
+
+        assertThat(meters["gql.query"]).isNotNull.hasSize(1)
+        assertThat(meters["gql.query"]?.first()?.id?.tags)
+            .containsAll(
+                Tags.of("execution-tag", "foo")
+                    .and("contextual-tag", "foo")
+                    .and("outcome", "success")
+                    .and("gql.operation", "QUERY")
+                    .and("gql.operation.name", "my_op_1")
+                    .and("gql.query.complexity", "5")
+                    .and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
+            )
+
+        assertThat(meters["gql.resolver"]).isNotNull.hasSize(1)
+        assertThat(meters["gql.resolver"]?.first()?.id?.tags)
+            .containsAll(
+                Tags.of("field-fetch-tag", "foo")
+                    .and("contextual-tag", "foo")
+                    .and("gql.field", "Query.ping")
+                    .and("outcome", "success")
+                    .and("gql.operation", "QUERY")
+                    .and("gql.operation.name", "my_op_1")
+                    .and("gql.query.complexity", "5")
+                    .and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
+            )
+    }
+
+    @Test
+    fun `Metrics for a successful request with explicit operation name`() {
         mvc.perform(
             // Note that the query below uses an aliased field, aliasing `ping` to `op_name`.
             // We will also assert that the tag reflected by the metric is not affected by the alias.