Skip to content

Improve metrics for _entities and fieldsΒ #1297

@rickbijkerk

Description

@rickbijkerk

There's 2 improvements i see:

  1. Currently metrics like: graphql_datafetcher_seconds_count group all _entities interactions under _entities. For applications which have several entityMappings defined you cant differentiate between which entity is being resolved.
  2. In applications which have multiple schema mappings for the same field but different objects, it currently all gets combined under the field name, which makes it impossible to separate which type this field was being called on.

I have done some testing and the following seems to solve both issues

import graphql.schema.GraphQLNamedType
import io.micrometer.common.KeyValues
import io.micrometer.observation.ObservationRegistry
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.graphql.observation.DataFetcherObservationContext
import org.springframework.graphql.observation.DataFetcherObservationConvention
import org.springframework.graphql.observation.DefaultDataFetcherObservationConvention
import org.springframework.graphql.observation.GraphQlObservationDocumentation.DataFetcherLowCardinalityKeyNames
import org.springframework.graphql.observation.GraphQlObservationInstrumentation

class CustomGraphQLDataFetcherObservationConvention(
    private val delegate: DataFetcherObservationConvention = DefaultDataFetcherObservationConvention(),
) : DataFetcherObservationConvention {

    override fun getName(): String? {
        return delegate.name
    }

    override fun getLowCardinalityKeyValues(ctx: DataFetcherObservationContext): KeyValues {
        val base = delegate.getLowCardinalityKeyValues(ctx)
        val env = ctx.environment
        val parent = (env.parentType as? GraphQLNamedType)?.name
        val field = env.field.name

        val enrichedField: String? = if (field == "_entities") {
            // Normally all _entities queries are processed as _entities but since we have so many different entities we want
            // to know exactly which entities is being called
            (env.getArgument<Any?>("representations") as? List<*>)
                ?.firstNotNullOfOrNull { rep -> (rep as? Map<*, *>)?.get("__typename") as? String }
                ?.let { "$field($it)" } ?: field
        } else {
            // Converts a field to a <parent>.<field> metric for cases where we want to differentiate between fields
            // with the same name defined on different types, which would otherwise all be aggregated under the field name
            parent?.let { "$it.$field" } ?: field
        }

        return KeyValues.of(
            DataFetcherLowCardinalityKeyNames.FIELD_NAME.withValue(enrichedField),
            DataFetcherLowCardinalityKeyNames.OUTCOME.withValue(base.firstOrNull { it.key == DataFetcherLowCardinalityKeyNames.OUTCOME.asString() }?.value),
            DataFetcherLowCardinalityKeyNames.ERROR_TYPE.withValue(base.firstOrNull { it.key == DataFetcherLowCardinalityKeyNames.ERROR_TYPE.asString() }?.value)
        )
    }
}

@Configuration
class GraphQLObservationConfig {

    @Bean
    fun graphQlObservationCustomizer(registry: ObservationRegistry): GraphQlSourceBuilderCustomizer {
        val customConvention = CustomGraphQLDataFetcherObservationConvention()
        return GraphQlSourceBuilderCustomizer { builder ->
            builder.instrumentation(
                listOf(
                    GraphQlObservationInstrumentation(
                        registry,
                        null,
                        customConvention
                    )
                )
            )
        }
    }
}

it changes

  • e.g. the graphql_field_name for _entities queries to _entities(<entityName>)
  • the graphql_field_name for fields to <parent>.<fieldName>

There's however something im struggling with and im not sure if thats possible to be solved from my end.
The code above seems to duplicate metrics e.g. i now see both _entities metrics appearing and _entities(<entityName>)

Any advice on how to fix that would be appreciated but even better if both these features could be implemented natively into spring graphql!

Let me know if any clarifications are needed!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions