On this page
← All product updates

Yoga Adopts Hive Logger, Supports the Latest defer/stream Spec, and Drops Legacy Node and GraphQL-JS

Emily Goodwin
Emily Goodwin

GraphQL Yoga v6 replaces its console-based logger with Hive Logger.

Every request now gets its own logger tagged with a requestId, and Yoga logs more of the request lifecycle out of the box. Yoga v6 requires Node.js 22 or later and GraphQL 16 or later.

Yoga has gone through some major changes to clean up technical debt that built up over time and to improve the overall logging experience.

These changes can be grouped into three large changes:

  • Hive Logger Migration
  • GraphQL + Node Version Updates and Cleanup
  • GraphQL Tools Major Update

Hive Logger

Yoga now uses Hive Logger, the same structured logger used across the Hive platform. You get log levels, attributes, child loggers, redaction and pluggable writers.

import { Logger } from 'graphql-yoga';

const logging = new Logger({ level: 'debug' });

createYoga({
  // ...
  logging,
  // ...
});

Per-request logging with context.log

Each request gets a child logger, available as context.log. It carries the request’s requestId: Yoga uses a host-assigned id first, then the incoming x-request-id header if trusted, and otherwise generates one.

Use the logger from the context whenever you are handling a request. Unlike console, a new Logger() or the server-wide yoga.log, it automatically associates your messages with that request. Your application logs, Yoga’s lifecycle logs and masked errors can then be correlated by the same id. You do not need to create a logger or add the request id to each message yourself.

The logger is already available in your context factory, as well as in resolvers. Returning your own context fields extends Yoga’s context, so you do not need to return log again:

const yoga = createYoga({
  context: ({ log }) => {
    log.debug('building application context');
    return { greeting: 'world' };
  },
  schema: createSchema({
    typeDefs,
    resolvers: {
      Query: {
        hello: (_, __, context) => {
          context.log.debug('resolving hello');
          return context.greeting;
        },
      },
    },
  }),
});

If a service or helper called by a resolver needs to log, pass context.log to it rather than having it use a separate global logger. That keeps its messages tied to the request too.

Logging from plugin hooks

The request logger is also available throughout the plugin lifecycle. HTTP hooks such as onRequestParse and onResponse expose it as serverContext.log. GraphQL execution hooks expose the GraphQL context through args.contextValue, so use args.contextValue.log in onExecute and onSubscribe.

Read the logger from each hook’s payload rather than capturing yoga.log during plugin initialization:

createYoga({
  schema,
  plugins: [
    {
      onYogaInit({ yoga }) {
        yoga.log.info('logging plugin initialized');
      },
      onRequestParse({ serverContext }) {
        serverContext.log.debug('preparing to parse the request');
      },
      onExecute({ args }) {
        const { log } = args.contextValue;
        log.debug('executing the operation');

        return {
          onExecuteDone() {
            log.debug('operation execution finished');
          },
        };
      },
      onResponse({ serverContext, response }) {
        serverContext.log.debug(`sending HTTP response with status ${response.status}`);
      },
    },
  ],
});

onYogaInit is the exception in this example: it runs at server initialization, not for a request, so yoga.log is appropriate there. Inside request hooks, use the context logger instead. Keeping it in a local variable for a completion callback, as above, preserves that request’s correlation; storing it in a plugin-wide variable would mix up concurrent requests.

More logging out of the box

New debug-level log lines were added for:

  • the request path, operationName and variables count when processing GraphQL parameters
  • the resolved GraphQL operation type after parsing, and parser errors (previously indistinguishable from other unexpected errors)
  • the response status code once a result has been processed into an HTTP Response
  • a successfully fetched signing key in @graphql-yoga/plugin-jwt
  • a successfully loaded supergraph schema in @graphql-yoga/apollo-managed-federation (info level, mirroring the existing failure log)

A one-line info-level “Request processed” summary is now logged once per HTTP request, carrying the method, path, operationName and operationType (or batchedOperations count for a batched request), the response status, the GraphQL errorCount, and the request’s durationMs.

Errors masked by useMaskedErrors are now logged through the request’s correlated logger too, so a masked error can be traced back to the request that produced it.

Smarter request ID correlation

Yoga’s useRequestId plugin (enabled by default) now prefers a request id your host framework already assigned to the request (for example Fastify’s request.id) over minting its own. This means useRequestId can stay enabled alongside a host’s own request-id setup without the two generating conflicting ids, and Yoga’s logs agree with whatever correlation id your framework or proxy already uses. The resolved id is also written to the outgoing response’s x-request-id header.

If no host-assigned id is available, Yoga falls back to the incoming x-request-id header, and finally to a generated id. This is a change in precedence: previously, the x-request-id header took priority over everything.

If your server is directly reachable by clients rather than only through a proxy or gateway that sets the header itself, disable trusting the header with the new trustHeader option, so a client can’t pick its own id and have it correlated across your logs as if it were trusted:

createYoga({
  requestId: {
    trustHeader: false,
  },
});

allowedHeaders.response now applies after CORS and plugins

allowedHeaders.response is now applied after the cors option and after your own plugins’ onResponse hooks, instead of before. Headers added by cors or by a plugin are now subject to the allow-list and will be stripped unless included in it. If you use allowedHeaders.response together with cors or a plugin that sets response headers, add those header names to the allow-list.

@graphql-tools v13 and removed re-exports

Yoga’s @graphql-tools/utils, @graphql-tools/schema and @graphql-tools/executor dependencies are now on their current major versions (^13.1.1, ^11.0.0 and ^4.1.2). Since Yoga is already taking a major version bump for v6, this pulls in the breaking changes from those packages directly instead of re-exporting removed helpers for compatibility.

Yoga no longer re-exports the following upstream helpers — import them from their own packages instead:

  • createGraphQLError → new GraphQLError(message, options) from graphql. To attach Yoga’s experimental schema coordinate to an error, use the new withSchemaCoordinate(error, coordinate) helper (read it back with getSchemaCoordinate from @graphql-tools/utils).
  • mergeSchemas → @graphql-tools/schema
  • DisposableSymbols → @whatwg-node/server
  • mapAsyncIterator, isPromise, mapMaybePromise, MaybePromise → @whatwg-node/promise-helpers (mapMaybePromise is now named handleMaybePromise)

@envelop/core’s utils export no longer re-exports mapAsyncIterator or isPromise either — import both from @whatwg-node/promise-helpers.

As part of the same bump, Yoga now maps the Accept header’s incremental delivery spec parameter onto @graphql-tools/executor’s incrementalSpec through a lookup table (INCREMENTAL_DELIVERY_SPECS from @graphql-tools/utils), so a future incremental delivery spec no longer needs a Yoga-specific Accept/Content-Type patch.

Support for the latest incremental delivery (defer/stream) spec

Yoga can now speak the current graphql-js incremental delivery format (pending / id / subPath / completed entries), in addition to the legacy one (path / label). Request it with an incrementalSpec parameter on the Accept header:

Accept: multipart/mixed;incrementalSpec=v0.2

Yoga echoes incrementalSpec=v0.2 back on the response’s Content-Type so clients can confirm which format they received. A bare multipart/mixed Accept header, or one with deferSpec=20220824, still gets the legacy v0.1 format, so existing clients are unaffected. An unrecognized spec parameter value gets a 406 Not Acceptable instead of a silently mismatched response.

This is Yoga-only for now — Hive Gateway support for incrementalSpec=v0.2 is coming in a follow-up release.

Dropped support for Node.js 18 and 20

Node.js 18 and 20 have reached end of life. Yoga v6 requires Node.js 22 or later, and future releases will only support Node.js versions that are still in LTS.

Dropped support for GraphQL 14 and 15

The minimum supported version of the graphql-js package is now 16.

Migrating to v6

  1. Upgrade to Node.js 22 or later.

  2. Upgrade graphql-js to version 16 or later.

  3. Update graphql-yoga and any @graphql-yoga/* packages to their latest versions.

  4. If you passed a custom logger to Yoga or relied on the Logger type or createLogger, update that code to use Hive Logger:

    - import { createLogger } from 'graphql-yoga';
    + import { Logger } from 'graphql-yoga';
    
    - const logging = createLogger('debug');
    + const logging = new Logger({ level: 'debug' });
    
      createYoga({
        logging,
        // ...
      });
  5. Rename context.logger to context.log, and yoga.logger to yoga.log. Use yoga.log for server-level messages only. For request-specific messages, read context.log, serverContext.log or args.contextValue.log from the resolver or hook payload, as shown above:

      const plugin = {
        onYogaInit({ yoga }) {
    -     logger = yoga.logger;
    +     log = yoga.log;
        },
      };
  6. If you depended on the standalone @graphql-yoga/logger package directly, it has been removed — install @graphql-hive/logger instead (or keep importing Logger/MemoryLogWriter from graphql-yoga, which re-exports both):

    - pnpm add @graphql-yoga/logger
    + pnpm add @graphql-hive/logger
    - import { Logger, MemoryLogWriter } from '@graphql-yoga/logger';
    + import { Logger, MemoryLogWriter } from '@graphql-hive/logger';
  7. If you passed a logger option to the built-in GraphiQL or health-check plugin config, or to @envelop/newrelic, @envelop/auth0, @envelop/resource-limitations, @envelop/statsd, @envelop/graphql-jit, @envelop/response-cache or @envelop/response-cache-cloudflare-kv, rename it to log. @envelop/apollo-federation’s useApolloFederation keeps its logger option name, but it’s now typed as Logger from @graphql-hive/logger instead of a loosely-typed { warn, debug, info, error } interface, and defaults to new Logger() instead of the global console.

  8. If you use @graphql-yoga/nestjs, it now logs through Nest’s own Logger by default instead of not logging at all. Pass logging: false to keep the previous behavior, or, to keep logging through Fastify’s Pino instance, pass it explicitly:

    logging: new Logger({ writers: [new PinoLogWriter(app.log)] });
  9. If you imported createGraphQLError, mergeSchemas, DisposableSymbols, mapAsyncIterator, isPromise, mapMaybePromise or the MaybePromise type from graphql-yoga, import them from their upstream packages instead:

    - import { createGraphQLError } from 'graphql-yoga';
    + import { GraphQLError } from 'graphql';
    
    - import { mergeSchemas } from 'graphql-yoga';
    + import { mergeSchemas } from '@graphql-tools/schema';
    
    - import { DisposableSymbols } from 'graphql-yoga';
    + import { DisposableSymbols } from '@whatwg-node/server';
    
    - import { mapAsyncIterator, isPromise, mapMaybePromise, type MaybePromise } from 'graphql-yoga';
    + import {
    +   mapAsyncIterator,
    +   isPromise,
    +   handleMaybePromise,
    +   type MaybePromise,
    + } from '@whatwg-node/promise-helpers';
  10. If you use @envelop/core’s mapAsyncIterator or isPromise, import them from @whatwg-node/promise-helpers instead.

  11. If you depend on @graphql-tools/utils, @graphql-tools/schema or @graphql-tools/executor directly, update them to ^13.1.1, ^11.0.0 and ^4.1.2 respectively.