Back to Blog Home

Performance Monitoring in GraphQL

Enrique Fueyo Ramírez  image

Enrique Fueyo Ramírez -

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

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 * as Sentry from '@sentry/node'; import { Transaction } from '@sentry/tracing'; 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:

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

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.

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Dashboards

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
© 2024 • Sentry is a registered Trademark of Functional Software, Inc.