Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -42,6 +42,9 @@ object DgsMetrics {
* This is useful if you want to find data loaders that might be responsible for poor query performance.
*/
DATA_LOADER("gql.dataLoader"),

/** _Counter_ that captures the number of GraphQL errors encountered during query execution. */
PERSISTED_QUERY_NOT_FOUND("gql.persistedQueryNotFound"),
}

/** Defines the tags applied to the [GqlMetric] emitted by the framework. */
Expand Down Expand Up @@ -91,6 +94,12 @@ object DgsMetrics {
* Absent in case the query failed to pass GraphQL validation.
*/
QUERY_SIG_HASH("gql.query.sig.hash"),

/** The persisted query Id in case of using automated persisted queries*/
PERSISTED_QUERY_ID("gql.persistedQueryId"),

/** Type of query, i.e. persisted query, full persisted query or not a persisted query.*/
PERSISTED_QUERY_TYPE("gql.persistedQueryType")
}

@Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperat
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters
import graphql.execution.preparsed.persisted.PersistedQueryNotFound
import graphql.language.Field
import graphql.language.FragmentSpread
import graphql.language.InlineFragment
Expand Down Expand Up @@ -76,7 +77,7 @@ class DgsGraphQLMetricsInstrumentation(

state.operationNameValue = parameters.operation
state.isIntrospectionQuery = QueryUtils.isIntrospectionQuery(parameters.executionInput)

state.queryTypeValue = getPersistedQueryType(parameters.executionInput).name
return SimpleInstrumentationContext.whenCompleted { result, exc ->
val tags =
buildList {
Expand All @@ -96,7 +97,22 @@ class DgsGraphQLMetricsInstrumentation(
): CompletableFuture<ExecutionResult> {
require(state is MetricsInstrumentationState)

val errorTagValues = ErrorUtils.sanitizeErrorPaths(executionResult)
// if this is an error due to PersistedQueryNotFound, we exclude from the gql.error metric
// this is captured in a separate counter instead
val persistedQueryNotFoundErrors = executionResult.errors.filter { it.errorType is PersistedQueryNotFound }
if (persistedQueryNotFoundErrors.isNotEmpty()) { val registry = registrySupplier.get()
persistedQueryNotFoundErrors.forEach {
val errorTags = buildList {
add(Tag.of(GqlTag.PERSISTED_QUERY_ID.key, it.extensions["persistedQueryId"].toString()))
}
registry
.counter(GqlMetric.PERSISTED_QUERY_NOT_FOUND.key, errorTags)
.increment()
}
return CompletableFuture.completedFuture(executionResult)
}

val errorTagValues = ErrorUtils.sanitizeErrorPaths(executionResult.errors)
if (errorTagValues.isNotEmpty()) {
val baseTags =
buildList {
Expand Down Expand Up @@ -252,6 +268,21 @@ class DgsGraphQLMetricsInstrumentation(
)
}

enum class PersistedQueryType {
NOT_APQ,
FULL_APQ,
APQ
}
private fun getPersistedQueryType(executionInput: ExecutionInput) : PersistedQueryType {
if (executionInput.query == "PersistedQueryMarker" && executionInput.extensions.containsKey("persistedQuery")) {
return PersistedQueryType.APQ
} else if (executionInput.query != "PersistedQueryMarker" && executionInput.extensions.containsKey("persistedQuery")) {
return PersistedQueryType.FULL_APQ
} else {
return PersistedQueryType.NOT_APQ
}
}

class MetricsInstrumentationState(
private val registry: MeterRegistry,
private val limitedTagMetricResolver: LimitedTagMetricResolver,
Expand All @@ -263,7 +294,7 @@ class DgsGraphQLMetricsInstrumentation(
internal var operationValue: Operation? = null
internal var operationNameValue: String? = null
internal var querySignatureValue: QuerySignatureRepository.QuerySignature? = null

internal var queryTypeValue: String? = PersistedQueryType.NOT_APQ.name
val queryComplexity: Optional<Int> get() = Optional.ofNullable(queryComplexityValue)
val operation: Optional<String> get() = Optional.ofNullable(operationValue?.name)
val operationName: Optional<String> get() = Optional.ofNullable(operationNameValue)
Expand Down Expand Up @@ -302,6 +333,11 @@ class DgsGraphQLMetricsInstrumentation(
GqlTag.QUERY_SIG_HASH.key,
querySignatureValue?.hash ?: TagUtils.TAG_VALUE_NONE,
)
tags +=
Tag.of(
GqlTag.PERSISTED_QUERY_TYPE.key,
queryTypeValue ?: PersistedQueryType.NOT_APQ.name,
)

return tags
}
Expand All @@ -315,8 +351,8 @@ class DgsGraphQLMetricsInstrumentation(
internal object ComplexityUtils {
private val complexityCalculator: FieldComplexityCalculator =
FieldComplexityCalculator {
_,
childComplexity,
_,
childComplexity,
->
childComplexity + 1
}
Expand Down Expand Up @@ -367,9 +403,9 @@ class DgsGraphQLMetricsInstrumentation(
}

internal object ErrorUtils {
fun sanitizeErrorPaths(executionResult: ExecutionResult): Collection<ErrorTagValues> {
fun sanitizeErrorPaths(errors: List<GraphQLError>): Collection<ErrorTagValues> {
val dedupeErrorPaths = mutableMapOf<String, ErrorTagValues>()
executionResult.errors.forEach { error ->
errors.forEach { error ->
val errorPath: List<Any>
val errorType: String
val errorDetail = errorDetailExtension(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.Executor

@SpringBootTest(properties = ["${DgsGraphQLMicrometerAutoConfiguration.AUTO_CONF_QUERY_SIG_PREFIX}.enabled=false"])
@SpringBootTest(properties = ["${DgsGraphQLMicrometerAutoConfiguration.AUTO_CONF_QUERY_SIG_PREFIX}.enabled=false", "dgs.graphql.apq.enabled:true"])
@EnableAutoConfiguration
@AutoConfigureMockMvc
@Execution(ExecutionMode.SAME_THREAD)
Expand Down Expand Up @@ -314,6 +314,117 @@ class MicrometerServletSmokeTest {
)
}

@Test
fun `Assert metrics for a persisted query not found error`() {
val uriBuilder =
MockMvcRequestBuilders
.post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
|{
| "extensions":{
| "persistedQuery":{
| "version":1,
| "sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
| }
| }
| }
|
""".trimMargin(),
)
mvc
.perform(uriBuilder)
.andExpect(status().isOk)
.andExpect(
content().json(
"""
|{
| "errors":[
| {
| "message":"PersistedQueryNotFound",
| "locations":[],
| "extensions":{
| "persistedQueryId":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38",
| "generatedBy":"graphql-java",
| "classification":"PersistedQueryNotFound"
| }
| }
| ]
| }
|
""".trimMargin(),
),
)

val meters = fetchMeters("gql.")

assertThat(meters).containsOnlyKeys("gql.query", "gql.persistedQueryNotFound")

assertThat(meters["gql.query"]).isNotNull.hasSize(1)
assertThat(meters["gql.query"]?.first()?.id?.tags)
.containsAll(
Tags
.of("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.APQ.name),
)

assertThat(meters["gql.persistedQueryNotFound"]).isNotNull.hasSize(1)
assertThat((meters["gql.persistedQueryNotFound"]?.first() as CumulativeCounter).count()).isEqualTo(1.0)
assertThat(meters["gql.persistedQueryNotFound"]?.first()?.id?.tags)
.containsAll(
Tags
.of("gql.persistedQueryId", "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"),
)
}

@Test
fun `Assert metrics for a full persisted query `() {
val uriBuilder =
MockMvcRequestBuilders
.post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
|{
| "query": "{__typename}",
| "extensions":{
| "persistedQuery":{
| "version":1,
| "sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
| }
| }
| }
|
""".trimMargin(),
)
mvc
.perform(uriBuilder)
.andExpect(status().isOk)
.andExpect(
content().json(
"""
| {
| "data": {
| "__typename":"Query"
| }
| }
|
""".trimMargin(),
),
)

val meters = fetchMeters("gql.")

assertThat(meters).containsKeys("gql.query")

assertThat(meters["gql.query"]).isNotNull.hasSize(1)
assertThat(meters["gql.query"]?.first()?.id?.tags)
.containsAll(
Tags
.of("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.FULL_APQ.name),
)
}

@Test
fun `Metrics for a query with a data fetcher with disabled instrumentation`() {
mvc
Expand All @@ -339,7 +450,8 @@ class MicrometerServletSmokeTest {
.and("gql.operation", "QUERY")
.and("gql.operation.name", "-someTrivialThings")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash),
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name),
)
}

Expand Down Expand Up @@ -403,8 +515,10 @@ class MicrometerServletSmokeTest {
.and("gql.operation", "none")
.and("gql.operation.name", "anonymous")
.and("gql.query.complexity", "none")
.and("gql.query.sig.hash", "none"),
)
.and("gql.query.sig.hash", "none")
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name),

)
}

@Test
Expand Down Expand Up @@ -468,7 +582,8 @@ class MicrometerServletSmokeTest {
.and("gql.operation", "QUERY")
.and("gql.operation.name", "-triggerInternalFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash),
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name),
)

logMeters(meters["gql.resolver"])
Expand Down Expand Up @@ -543,7 +658,8 @@ class MicrometerServletSmokeTest {
.and("gql.operation", "QUERY")
.and("gql.operation.name", "-triggerBadRequestFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash),
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name),
)

assertThat(meters["gql.resolver"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
Expand Down Expand Up @@ -619,8 +735,10 @@ class MicrometerServletSmokeTest {
.and("gql.operation", "QUERY")
.and("gql.operation.name", "-triggerCustomFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash),
)
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name),

)

assertThat(meters["gql.resolver"]).isNotNull.hasSizeGreaterThanOrEqualTo(1)
assertThat(meters["gql.resolver"]?.first()?.id?.tags)
Expand Down Expand Up @@ -681,6 +799,7 @@ class MicrometerServletSmokeTest {
.and("gql.operation.name", "-triggerInternalFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name)
.toList(),
Tags
.of("execution-tag", "foo")
Expand All @@ -693,6 +812,7 @@ class MicrometerServletSmokeTest {
.and("gql.operation.name", "-triggerInternalFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name)
.toList(),
Tags
.of("execution-tag", "foo")
Expand All @@ -705,6 +825,7 @@ class MicrometerServletSmokeTest {
.and("gql.operation.name", "-triggerInternalFailure")
.and("gql.query.complexity", "5")
.and("gql.query.sig.hash", MOCKED_QUERY_SIGNATURE.hash)
.and("gql.persistedQueryType", DgsGraphQLMetricsInstrumentation.PersistedQueryType.NOT_APQ.name)
.toList(),
)
}
Expand Down Expand Up @@ -739,32 +860,33 @@ class MicrometerServletSmokeTest {
@Bean
open fun querySignatureRepository(): QuerySignatureRepository =
QuerySignatureRepository {
_,
_,
_,
_,
->
Optional.of(MOCKED_QUERY_SIGNATURE)
}

@Bean
open fun contextualTagProvider(): DgsContextualTagCustomizer = DgsContextualTagCustomizer { Tags.of("contextual-tag", "foo") }
open fun contextualTagProvider(): DgsContextualTagCustomizer =
DgsContextualTagCustomizer { Tags.of("contextual-tag", "foo") }

@Bean
open fun executionTagCustomizer(): DgsExecutionTagCustomizer =
DgsExecutionTagCustomizer {
_,
_,
_,
_,
_,
_,
_,
_,
->
Tags.of("execution-tag", "foo")
}

@Bean
open fun fieldFetchTagCustomizer(): DgsFieldFetchTagCustomizer =
DgsFieldFetchTagCustomizer {
_,
_,
_,
_,
_,
_,
->
Tags.of("field-fetch-tag", "foo")
}
Expand Down Expand Up @@ -882,7 +1004,8 @@ class MicrometerServletSmokeTest {
}

@Bean
open fun customDataFetchingExceptionHandler(): DataFetcherExceptionHandler = CustomDataFetchingExceptionHandler()
open fun customDataFetchingExceptionHandler(): DataFetcherExceptionHandler =
CustomDataFetchingExceptionHandler()

@Bean
open fun dataLoaderTaskExecutor(): Executor {
Expand Down
Loading