Performance Monitoring in GraphQL
Tags: performance, graphql, monitoring
Published: 2021-08-31 22:51:59

By: Enrique Fueyo

_[Enrique Fueyo Ramírez](https://www.linkedin.com/in/efueyo/) is the Co-founder and CTO of [Lang.ai](www.lang.ai). Here’s how he and his team at Lang.ai instrumented performance monitoring for GraphQL resolvers._

#Tech Stack#

- At Lang.ai we use [Sentry](www.sentry.io) in our systems for Error Monitoring and recently added Performance Monitoring

- Lang.ai’s User Interface is a React App using a [GraphQL](https://graphql.org/) API.

- We use [Apollo](https://www.apollographql.com/) to connect the frontend and the GraphQL backend.

#Problem#

- Some months ago we started experiencing some degradation in a few graphql queries. The queries were complex (big) queries and it was tricky to debug which part of the request was slowing down the response.

#Goal#

- We wanted to monitor the performance of each [Graphql Resolver](https://www.apollographql.com/docs/apollo-server/data/resolvers/).

- Ideally, we wanted to monitor the performance of every resolver without explicitly adding it (we didn’t want to proactively add a start and stop lines of code all around our functions’ bodies).

#Solution#

- Create a Sentry transaction for each graphql request.

Each Sentry transaction is initialized with its own [context](https://docs.sentry.io/platforms/javascript/enriching-events/context/). [Create a Transaction](https://docs.sentry.io/platforms/javascript/performance/instrumentation/custom-instrumentation/)

```javascript
import { Transaction } from "@sentry/types"

export interface Context {
  // ... other context fields for your context
  transaction: Transaction
}

export async function createContext(): Promise<Context> { {
  // ... create other context fields
  const transaction = Sentry.startTransaction({
    op: "gql",
    name: "GraphQLTransaction", // this will be the default name, unless the gql query has a name
  })
  return { transaction };
}
```

- Add a span for each resolver

To intercept the life-cycle of each resolver and create and finish a span, we needed to create an Apollo Plugin.

```javascript
import { ApolloServerPlugin } from "apollo-server-plugin-base"
import { Context } from "./context"

const plugin: ApolloServerPlugin<Context> = {
  requestDidStart({ request, context }) {
    if (!!request.operationName) { // set the transaction Name if we have named queries
      context.transaction.setName(request.operationName!)
    }
    return {
      willSendResponse({ context }) { // hook for transaction finished
        context.transaction.finish()
      },
      executionDidStart() {
        return {
          willResolveField({ context, info }) { // hook for each new resolver
            const span = context.transaction.startChild({
              op: "resolver",
              description: `${info.parentType.name}.${info.fieldName}`,
            })
            return () => { // this will execute once the resolver is finished
              span.finish()
            }
          },
        }
      },
    }
  },
}

export default plugin
```

- And then we have to connect all the pieces on server initialization

```javascript
import { ApolloServer } from "apollo-server-micro"

import { createContext } from "./context"
import SentryPlugin from "./sentry-plugin"

const apolloServer = new ApolloServer({
  // ... your ApolloServer options
  // Create context function
  context: ({ req, connection }) => createContext({ req, connection }),
  // Add our sentry plugin
  plugins: [SentryPlugin],
})
```

Once your server starts receiving requests it will send every transaction info to your configured Sentry account. You should see something like this:

![successful setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gz88m71zeln440276xox.png)

And you can also see the detail of each individual transaction with its resolvers:

![transaction detail](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k5qq0ee16r8zlozeoqy1.png)

#One last consideration#

We could have created the transaction directly in the plugin, inside the `requestDidStart` function and omit any references to the `Context`. But, if we make the `transaction` accessible from the `Context`, each resolver can access it and we can create more `spans` inside the resolvers for more fine grained information.

Accessing the transaction from the resolver should also be helpful for Sentry’s [Distributed Tracing](https://docs.sentry.io/product/performance/distributed-tracing/).