DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Performance Monitoring in GraphQL
Rahul Chhabria for Sentry

Posted on • Originally published at blog.sentry.io

Performance Monitoring in GraphQL

By: Enrique Fueyo

Enrique Fueyo RamΓ­rez is the Co-founder and CTO of 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 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 };
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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],
})
Enter fullscreen mode Exit fullscreen mode

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

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

transaction detail

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.

Top comments (0)

DEV has this feature:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. πŸ›