Search code examples
spring-bootgraphqlspring-boot-actuatordatadogspring-micrometer

GraphQL + Spring Boot: how to collect (error) metrics?


I've been working on adding monitoring metrics in our GraphQL gateway recently.

We're using graphql-spring-boot starter for the gateway.

After reading the following documentations, I manage to send the basic graphql.timer.query.* metrics to Datadog

What I've achieved so far is, when I send a GraphQL query/mutation, I'd collect the request count and time accordingly. e.g. sending the query below

query HelloWorldQuery {
  greeting(
    name: "Bob"
  ) {
    message
  }
}

I'll see metrics graphql.timer.query.count / graphql.timer.query.sum with tags operationName=HelloWorldQuery

It works like perfectly, until I want to test a query with errors. I realise there is no metrics/tags related to a failed query. For example, if I the above query returns null data and some GraphQL errors, I'd still collect graphql.timer.query.count (operationName=HelloWorldQuery), but there's no additional tags for me to tell there is an error for that query.

In the gateway, I have implemented a custom GraphQLErrorHandler, so I was thinking maybe I should add error counter (via MeterRegistry) in that class, but I am unable to get the operationName simply from GraphQLError type. the best I can get is error.getPath() which gives the method name (e.g. greeting) rather than the custom query name (HelloWorldQuery - to be consistent with what graphql.timer.query.* provides).

My question is, how to solve the above problem? And generally what is the best way of collecting GraphQL query metrics (including errors)?

------------------- Update -------------------

2019-12-31 I read a bit more about GraphQL Instrumentation here and checked the MetricsInstrumentation implementation in graphql-spring-boot repo, the I have an idea of extending the MetricsInstrumentation class by adding error metrics there.

2020-01-02 I tried to ingest my CustomMetricsInstrumentation class, but with no luck. There is internal AutoConfiguration wiring, which I cannot insert my auto configuration in the middle.


Solution

  • You can override the default TracingInstrumentation with your own implementation. It will be picked automatically due to the @ConditionalOnMissingBean annotation in the GraphQLInstrumentationAutoConfiguration class. Here is a simple example that adds two custom metrics: graphql.counter.query.success and graphql.counter.query.error:

    @Component
    public class CustomMetricsInstrumentation extends TracingInstrumentation {
    
        private static final String QUERY_STATUS_COUNTER_METRIC_NAME = "graphql.counter.query";
        private static final String OPERATION_NAME_TAG = "operationName";
        private static final String UNKNOWN_OPERATION_NAME = "unknown";
    
        private MeterRegistry meterRegistry;
    
        public CustomMetricsInstrumentation(MeterRegistry meterRegistry) {
            this.meterRegistry = meterRegistry;
        }
    
        @Override
        public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult,
                                                                            InstrumentationExecutionParameters parameters) {
    
            String status = CollectionUtils.isEmpty(executionResult.getErrors()) ? "success" : "error";
            String operation = parameters.getOperation() != null ? parameters.getOperation() : UNKNOWN_OPERATION_NAME;
            Collection<Tag> tags = Arrays.asList(Tag.of(OPERATION_NAME_TAG, operation));
    
            meterRegistry.counter(QUERY_STATUS_COUNTER_METRIC_NAME + "." + status, tags).increment();
    
            return super.instrumentExecutionResult(executionResult, parameters);
        }
    }
    

    My application.yaml, just in case:

    graphql:
      servlet:
        tracing-enabled: true
        actuator-metrics: true
    management:
      endpoint:
      metrics:
        enabled: true
      endpoints:
        web:
          exposure:
            include: health,metrics
    

    I'm using spring-boot-starter-parent:2.2.2.RELEASE, graphql-spring-boot-starter:6.0.0

    I hope it helps.