GraphQL Logging — Structured Logging, Tracing, and Error Tracking for APIs
In this tutorial, you will learn about Graphql Logging. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL logging captures every operation's details — query, variables, duration, errors, and user context — in a structured format for debugging, monitoring, auditing, and performance analysis.
What You'll Learn
- Structured logging for GraphQL resolvers
- Logging query details and execution time
- Apollo Studio reporting and metrics
- OpenTelemetry distributed tracing
- Audit logging for sensitive operations
- Error tracking and alerting
Why It Matters
Without logging, debugging GraphQL issues is like finding a needle in a haystack. Structured logging lets you search by query, user, duration, or error type. DodaTech's Durga Antivirus Pro logs every GraphQL operation with execution time, user ID, and query hash — storing 30 days of logs for auditing and 7 days of traces for debugging.
Real-World Use
A user reports that their dashboard takes 10 seconds to load. The operations team searches logs for that user's session, finds a slow query that fetches all 50,000 devices instead of the first 20, and identifies the pagination bug in the client.
flowchart LR
A["GraphQL Request"] --> B["Log: Operation Started"]
B --> C["Resolve Fields"]
C --> D["Log: Resolver Duration"]
D --> E["Log: DB Query Duration"]
E --> F{"Errors?"}
F -->|Yes| G["Log: Error + Stack Trace"]
F -->|No| H["Log: Operation Complete"]
G --> I["Alert: PagerDuty/Slack"]
H --> J["Metrics: Histogram"]
style G fill:#fecaca,stroke:#dc2626
style I fill:#fef3c7,stroke:#d97706
Code Examples
Example 1: Structured Logging Plugin
const { ApolloServerPlugin } = require('apollo-server-plugin-base');
const loggingPlugin = {
async requestDidStart(requestContext) {
const start = Date.now();
const { request, context } = requestContext;
console.log(JSON.stringify({
event: 'request_start',
operationName: request.operationName,
query: request.query?.substring(0, 200),
variables: JSON.stringify(request.variables),
userId: context.user?.id || 'anonymous',
timestamp: new Date().toISOString(),
}));
return {
async willSendResponse(responseContext) {
const duration = Date.now() - start;
console.log(JSON.stringify({
event: 'request_complete',
operationName: request.operationName,
duration,
errors: responseContext.errors?.length || 0,
userId: context.user?.id || 'anonymous',
}));
},
};
},
};
Example 2: Resolver-Level Logging Middleware
function logResolver(resolver, name) {
return async (parent, args, context, info) => {
const start = Date.now();
context.logger.debug(`Resolving ${name}`, {
args: JSON.stringify(args).substring(0, 500),
path: info.path,
});
try {
const result = await resolver(parent, args, context, info);
const duration = Date.now() - start;
context.logger.info(`Resolved ${name} in ${duration}ms`);
if (duration > 1000) {
context.logger.warn(`Slow resolver: ${name}`, { duration });
}
return result;
} catch (error) {
const duration = Date.now() - start;
context.logger.error(`Resolver ${name} failed`, {
error: error.message,
duration,
stack: error.stack,
});
throw error;
}
};
}
// Apply to resolvers
const resolvers = {
Query: {
devices: logResolver(devicesResolver, 'Query.devices'),
threats: logResolver(threatsResolver, 'Query.threats'),
},
};
Example 3: OpenTelemetry Tracing
const { trace, context } = require('@opentelemetry/api');
const { ApolloServer } = require('apollo-server');
const tracer = trace.getTracer('graphql-server');
const tracingPlugin = {
async requestDidStart() {
const span = tracer.startSpan('graphql.request');
return {
async executionDidStart(executionContext) {
const ctx = trace.setSpan(context.active(), span);
return {
willResolveField({ source, args, contextValue, info }) {
const childSpan = tracer.startSpan(
`graphql.resolve.${info.parentType.name}.${info.fieldName}`,
{ parent: ctx }
);
return () => {
childSpan.end();
};
},
};
},
async willSendResponse() {
span.end();
},
};
},
};
Common Mistakes
- Logging sensitive data — never log passwords, tokens, or personal data in query variables or response data. Mask or redact sensitive fields.
- Logging at the wrong level — use debug for resolver internals, info for operation summaries, warn for slow queries, error for failures.
- Blocking the request with synchronous logging — logging should never increase response latency. Use async loggers or background queues.
- Logging without correlation IDs — each request should have a unique ID that links logs across resolvers, database calls, and external services.
- Ignoring log volume — a busy API generates millions of log lines per day. Use sampling for debug logs and store aggregated metrics instead of raw logs.
Practice Questions
- What information should every GraphQL log entry contain?
- How do you redact sensitive fields from logs?
- What is the difference between logging and tracing?
- How do correlation IDs help with debugging?
- When should you sample logs instead of logging every request?
Challenge: Build a GraphQL logging system that logs every operation with a correlation ID, measures resolver-level timing, masks sensitive fields (password, token, ssn), sends slow-query alerts to Slack, and stores aggregated metrics in Prometheus.
Mini Project
Create a comprehensive logging solution for a GraphQL API: structured JSON logs with correlation IDs, resolver-level timing, Apollo Studio reporting, OpenTelemetry tracing, sensitive data redaction, and a dashboard for viewing recent slow queries and error rates.
FAQ
What's Next
Learn about GraphQL API monitoring and alerting
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro