Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import com.netflix.graphql.types.errors.ErrorType
import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.GraphQLError
import graphql.GraphQLException
import graphql.InvalidSyntaxError
import graphql.analysis.FieldComplexityCalculator
import graphql.analysis.QueryComplexityCalculator
import graphql.execution.DataFetcherResult
import graphql.execution.ExecutionContext
import graphql.execution.instrumentation.InstrumentationContext
import graphql.execution.instrumentation.InstrumentationState
Expand Down Expand Up @@ -172,18 +174,18 @@ class DgsGraphQLMetricsInstrumentation(
try {
val result = dataFetcher.get(environment)
if (result is CompletionStage<*>) {
result.whenComplete { _, error ->
result.whenComplete { value, error ->
recordDataFetcherMetrics(
registry,
sampler,
state,
parameters,
error,
checkResponseForErrors(value, error),
baseTags,
)
}
} else {
recordDataFetcherMetrics(registry, sampler, state, parameters, null, baseTags)
recordDataFetcherMetrics(registry, sampler, state, parameters, checkResponseForErrors(result, null), baseTags)
}
result
} catch (exc: Exception) {
Expand All @@ -193,6 +195,15 @@ class DgsGraphQLMetricsInstrumentation(
}
}

private fun checkResponseForErrors(
value: Any?,
error: Throwable?,
): Throwable? =
error
?: (value as? DataFetcherResult<*>)
?.takeIf { it.hasErrors() }
?.let { GraphQLException("GraphQL errors in response: ${it.errors}") }

/**
* Port the implementation from MaxQueryComplexityInstrumentation in graphql-java and store the computed complexity
* in the MetricsInstrumentationState for access to add tags to metrics.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import graphql.GraphQLError
import graphql.execution.DataFetcherExceptionHandler
import graphql.execution.DataFetcherExceptionHandlerParameters
import graphql.execution.DataFetcherExceptionHandlerResult
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
Expand Down Expand Up @@ -678,6 +679,106 @@ class MicrometerServletSmokeTest {
)
}

@Test
fun `Assert metrics for a successful async response with errors`() {
mvc
.perform(
MockMvcRequestBuilders
.post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content("""{ "query": "{triggerSuccessfulRequestWithErrorAsync}" }"""),
).andExpect(status().isOk)
.andExpect(
content().json(
"""
|{
| "errors":[
| {"message":"Exception triggered."}
| ],
| "data":{"triggerSuccessfulRequestWithErrorAsync":"Some data..."}
|}
""".trimMargin(),
false,
),
)

val meters = fetchMeters("gql.")

assertThat(meters).containsOnlyKeys("gql.error", "gql.query", "gql.resolver")

assertThat(meters["gql.error"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat((meters["gql.error"]?.first() as CumulativeCounter).count()).isEqualTo(1.0)
assertThat(meters["gql.error"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)

assertThat(meters["gql.query"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat(meters["gql.query"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)

assertThat(meters["gql.resolver"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat(meters["gql.resolver"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)
}

@Test
fun `Assert metrics for a successful sync response with errors`() {
mvc
.perform(
MockMvcRequestBuilders
.post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content("""{ "query": "{triggerSuccessfulRequestWithErrorSync}" }"""),
).andExpect(status().isOk)
.andExpect(
content().json(
"""
|{
| "errors":[
| {"message":"Exception triggered."}
| ],
| "data":{"triggerSuccessfulRequestWithErrorSync":"Some data..."}
|}
""".trimMargin(),
false,
),
)

val meters = fetchMeters("gql.")

assertThat(meters).containsOnlyKeys("gql.error", "gql.query", "gql.resolver")

assertThat(meters["gql.error"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat((meters["gql.error"]?.first() as CumulativeCounter).count()).isEqualTo(1.0)
assertThat(meters["gql.error"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)

assertThat(meters["gql.query"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat(meters["gql.query"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)

assertThat(meters["gql.resolver"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat(meters["gql.resolver"]?.first()?.id?.tags)
.containsAll(
Tags
.of("outcome", "failure"),
)
}

@Test
fun `Assert metrics for custom error`() {
mvc
Expand Down Expand Up @@ -914,6 +1015,8 @@ class MicrometerServletSmokeTest {
| triggerInternalFailure: String
| triggerBadRequestFailure:String
| triggerCustomFailure: String
| triggerSuccessfulRequestWithErrorAsync:String
| triggerSuccessfulRequestWithErrorSync:String
|}
|
|type Mutation{
Expand Down Expand Up @@ -956,6 +1059,24 @@ class MicrometerServletSmokeTest {
@DgsQuery
fun triggerBadRequestFailure(): String = throw DgsBadRequestException("Exception triggered.")

@DgsQuery
fun triggerSuccessfulRequestWithErrorAsync(): CompletableFuture<DataFetcherResult<String>> =
CompletableFuture.supplyAsync {
DataFetcherResult
.newResult<String>()
.data("Some data...")
.error(TypedGraphQLError("Exception triggered.", null, null, null, null))
.build()
}

@DgsQuery
fun triggerSuccessfulRequestWithErrorSync(): DataFetcherResult<String> =
DataFetcherResult
.newResult<String>()
.data("Some data...")
.error(TypedGraphQLError("Exception triggered.", null, null, null, null))
.build()

@DgsQuery
fun triggerCustomFailure(): String = throw CustomException("Exception triggered.")

Expand Down
Loading