GraphQL

Monitoring and Observability

16 min Lesson 28 of 35

Monitoring and Observability

Learn how to monitor, debug, and optimize your GraphQL API using industry-standard tools and practices.

Apollo Studio

Apollo Studio provides comprehensive monitoring and analytics for GraphQL APIs:

const { ApolloServer } = require('apollo-server'); const server = new ApolloServer({ typeDefs, resolvers, // Enable Apollo Studio integration apollo: { key: process.env.APOLLO_KEY, graphRef: process.env.APOLLO_GRAPH_REF, // e.g., "my-graph@production" }, // Enable usage reporting plugins: [ require('apollo-server-core').ApolloServerPluginUsageReporting({ sendVariableValues: { all: true }, sendHeaders: { all: true }, }), ], }); server.listen().then(({ url }) => { console.log(`Server ready at ${url}`); console.log(`Apollo Studio: https://studio.apollographql.com/`); });
Note: Get your Apollo Studio API key from studio.apollographql.com. Never commit API keys to version control.

Schema Registry

Register your schema with Apollo Studio for version tracking and validation:

# Install Rover CLI npm install -g @apollo/rover # Authenticate with Apollo Studio rover config auth # Publish schema to registry rover graph publish my-graph@production \ --schema ./schema.graphql \ --key $APOLLO_KEY # Check schema changes before publishing rover graph check my-graph@production \ --schema ./schema.graphql \ --key $APOLLO_KEY # Download schema from registry rover graph fetch my-graph@production \ --key $APOLLO_KEY > schema.graphql

Operation Monitoring

Track query performance, errors, and usage patterns:

const { ApolloServer } = require('apollo-server'); const { ApolloServerPluginUsageReporting } = require('apollo-server-core'); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ // Report all operations sendVariableValues: { all: true }, sendHeaders: { all: true }, // Custom operation name generateClientInfo: ({ request }) => { const headers = request.http?.headers; return { clientName: headers.get('apollographql-client-name'), clientVersion: headers.get('apollographql-client-version'), }; }, // Exclude sensitive operations sendReportsImmediately: true, }), ], }); // Client-side operation naming import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: 'http://localhost:4000/graphql', cache: new InMemoryCache(), name: 'web-app', version: '1.0.0', headers: { 'apollographql-client-name': 'web-app', 'apollographql-client-version': '1.0.0', }, });

Error Tracking

Integrate error tracking services like Sentry for comprehensive error monitoring:

const { ApolloServer } = require('apollo-server'); const Sentry = require('@sentry/node'); // Initialize Sentry Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, }); // Custom error formatting plugin const sentryPlugin = { async requestDidStart() { return { async didEncounterErrors({ errors, context }) { for (const error of errors) { // Ignore client errors if (error.extensions?.code === 'BAD_USER_INPUT') { continue; } // Report to Sentry Sentry.withScope((scope) => { scope.setTag('kind', 'graphql'); scope.setContext('query', { query: context.request.query, variables: context.request.variables, }); scope.setUser({ id: context.user?.id, email: context.user?.email, }); Sentry.captureException(error); }); } }, }; }, }; const server = new ApolloServer({ typeDefs, resolvers, plugins: [sentryPlugin], formatError: (error) => { // Don't expose internal errors to clients if (process.env.NODE_ENV === 'production') { return new Error('Internal server error'); } return error; }, });

Performance Metrics

Track resolver execution times and identify performance bottlenecks:

const { ApolloServer } = require('apollo-server'); const responseTime = require('response-time'); // Custom performance tracking plugin const performancePlugin = { async requestDidStart() { const start = Date.now(); const resolverTimings = new Map(); return { async executionDidStart() { return { willResolveField({ info }) { const fieldStart = Date.now(); return () => { const duration = Date.now() - fieldStart; const path = info.path.key; if (!resolverTimings.has(path)) { resolverTimings.set(path, []); } resolverTimings.get(path).push(duration); }; }, }; }, async willSendResponse({ response }) { const totalDuration = Date.now() - start; // Log slow queries if (totalDuration > 1000) { console.warn('Slow query detected:', { duration: totalDuration, resolvers: Array.from(resolverTimings.entries()) .map(([name, timings]) => ({ name, avg: timings.reduce((a, b) => a + b) / timings.length, max: Math.max(...timings), })) .sort((a, b) => b.avg - a.avg), }); } // Add timing header response.http.headers.set('X-Response-Time', `${totalDuration}ms`); }, }; }, }; const server = new ApolloServer({ typeDefs, resolvers, plugins: [performancePlugin], });
Tip: Set up alerts in Apollo Studio for slow queries (> 2s), high error rates (> 5%), or unusual traffic patterns.

Logging

Implement structured logging for better debugging and analysis:

const winston = require('winston'); // Configure Winston logger const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), })); } // Logging plugin const loggingPlugin = { async requestDidStart({ request, context }) { const start = Date.now(); logger.info('GraphQL request started', { operationName: request.operationName, userId: context.user?.id, }); return { async didEncounterErrors({ errors }) { errors.forEach((error) => { logger.error('GraphQL error', { message: error.message, path: error.path, extensions: error.extensions, }); }); }, async willSendResponse() { const duration = Date.now() - start; logger.info('GraphQL request completed', { operationName: request.operationName, duration, }); }, }; }, };

Health Checks

Implement health check endpoints for monitoring and load balancers:

const express = require('express'); const { ApolloServer } = require('apollo-server-express'); const app = express(); // Health check endpoint app.get('/health', async (req, res) => { const health = { uptime: process.uptime(), timestamp: Date.now(), status: 'ok', checks: {}, }; try { // Check database connection await db.raw('SELECT 1'); health.checks.database = 'ok'; } catch (error) { health.status = 'error'; health.checks.database = 'error'; } try { // Check Redis connection await redis.ping(); health.checks.redis = 'ok'; } catch (error) { health.status = 'error'; health.checks.redis = 'error'; } const statusCode = health.status === 'ok' ? 200 : 503; res.status(statusCode).json(health); }); // Readiness check app.get('/ready', (req, res) => { res.status(200).json({ ready: true }); }); // Liveness check app.get('/live', (req, res) => { res.status(200).json({ alive: true }); }); const server = new ApolloServer({ typeDefs, resolvers, }); server.applyMiddleware({ app }); app.listen(4000, () => { console.log('Server running on http://localhost:4000'); });
Warning: Never expose sensitive information (API keys, database credentials, internal errors) in health check endpoints or logs.
Exercise:
  1. Set up Apollo Studio integration with your GraphQL server
  2. Implement a custom performance tracking plugin that logs slow resolvers
  3. Add Sentry error tracking to capture and report GraphQL errors
  4. Create structured logging with Winston for all GraphQL operations
  5. Implement health check endpoints for database and cache connections