Skip to content

GraphQL Logging — Structured Logging, Tracing, and Error Tracking for APIs

DodaTech Updated 2026-06-28 4 min read

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

  1. Logging sensitive data — never log passwords, tokens, or personal data in query variables or response data. Mask or redact sensitive fields.
  2. Logging at the wrong level — use debug for resolver internals, info for operation summaries, warn for slow queries, error for failures.
  3. Blocking the request with synchronous logging — logging should never increase response latency. Use async loggers or background queues.
  4. Logging without correlation IDs — each request should have a unique ID that links logs across resolvers, database calls, and external services.
  5. 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

  1. What information should every GraphQL log entry contain?
  2. How do you redact sensitive fields from logs?
  3. What is the difference between logging and tracing?
  4. How do correlation IDs help with debugging?
  5. 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 is the best log format for GraphQL?

JSON structured logging with fields: timestamp, correlationId, operationName, query (truncated), duration, userId, errors, and resolverTimings for each field.

How do I trace a slow GraphQL query?

Use OpenTelemetry or Apollo Studio tracing. Each span shows resolver timing, database calls, and external API calls. The trace pinpoints the bottleneck.

Should I log query variables?

Log query variables but redact sensitive fields (passwords, credit cards, tokens). Use a allowlist of safe fields or a denylist of sensitive ones.

How do I handle high-volume logging?

Use async logging (write to a buffer, flush periodically), sample debug logs (log 1 in 100), and aggregate metrics (p99 latency, error rate) instead of logging every request.

What is the difference between Apollo Studio and custom logging?

Apollo Studio provides managed reporting with metrics, traces, and schema checks. Custom logging gives you full control over what you store and how you query it.

What's Next

Learn about GraphQL API monitoring and alerting

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro