Share on Twitter
Share on Facebook
Share on HackerNews

Guest Post: Performance Monitoring in GraphQL

Enrique Fueyo Ramírez is the Co-founder and CTO of Lang.ai. Here’s a post on how him and his team at Lang.ai instrumented performance monitoring for GraphQL resolvers.

Tech Stack

  • At Lang.ai we use Sentry in our systems for Error Monitoring and recently added Performance Monitoring
  • Lang.ai’s User Interface is a React App using a GraphQL API.
  • We use Apollo 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.
  • 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. Create a Transaction

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.

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
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:

performance_lang.ai

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

spans_lang.ai

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.

Your code is broken. Let's Fix it.
Get Started

Do you like corporate newsletters?

Neither do we. Sign up anyway.

© 2021 • Sentry is a registered Trademark
of Functional Software, Inc.