GraphQL
Monitoring and Observability
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:
- Set up Apollo Studio integration with your GraphQL server
- Implement a custom performance tracking plugin that logs slow resolvers
- Add Sentry error tracking to capture and report GraphQL errors
- Create structured logging with Winston for all GraphQL operations
- Implement health check endpoints for database and cache connections