Skip to content

Commit

Permalink
Don't install the usage reporting plugin by default in subgraphs (#7184)
Browse files Browse the repository at this point in the history
The usage reporting plugin is designed for monolithic graphs and
Gateways, not for subgraphs. You will get strange results in
GraphOS/Studio if you send usage reports from a subgraph to Studio. This
PR changes the defaults so that subgraphs do not automatically install
the usage reporting plugin even if API key and graph ref are provided;
in this case, a warning is logged.

You still can explicitly install ApolloServerPluginUsageReporting in
subgraphs, though a warning will be logged.

Fixes #7121.

Also, update terminology around subgraphs. Various log messages and
internal
symbol names and comments referred to "federated service" rather than
the
newer and clearer "subgraph".

Co-authored-by: Danielle Man <man.danielleh@gmail.com>
Co-authored-by: Trevor Scheer <trevor.scheer@gmail.com>
  • Loading branch information
3 people committed Nov 23, 2022
1 parent 46af825 commit b1548c1
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-steaks-smell.md
@@ -0,0 +1,5 @@
---
'@apollo/server': minor
---

Don't automatically install the usage reporting plugin in servers that appear to be hosting a federated subgraph (based on the existence of a field `_Service.sdl: String`). This is generally a misconfiguration. If an API key and graph ref are provided to the subgraph, log a warning and do not enable the usage reporting plugin. If the usage reporting plugin is explicitly installed in a subgraph, log a warning but keep it enabled.
12 changes: 9 additions & 3 deletions docs/source/api/plugin/usage-reporting.mdx
Expand Up @@ -5,16 +5,20 @@ api_reference: true

Apollo Server's built-in usage reporting plugin gathers data on how your clients use the operations and fields in your GraphQL schema. The plugin also handles pushing this usage data to [Apollo Studio](/studio/), as described in [Metrics and logging](../../monitoring/metrics/).

This plugin is designed to be used in an Apollo Gateway or in a monolithic server; it is not designed to be used from a subgraph. In a supergraph running Apollo Federation, the Apollo Gateway or Apollo Router will send usage reports to Apollo's cloud. Subgraphs don't need to also send usage reports to Apollo's cloud; instead, they send it to the Router via [inline traces](./inline-trace/) and the Router combines execution information across all subgraphs and sends summarized reports to the cloud.

## Default installation

> 📣 **New in Apollo Server 4**: error details are not included in traces by default. For more details, see [Error Handling](../../data/errors/#masking-and-logging-errors).
Apollo Server automatically installs and enables this plugin with default settings if you [provide a graph API key and a graph ref to Apollo Server](../../monitoring/metrics/#connecting-to-apollo-studio). You usually do this by setting the `APOLLO_KEY` and `APOLLO_GRAPH_REF` (or `APOLLO_GRAPH_ID` and `APOLLO_GRAPH_VARIANT`) environment variables. No other action is required.
Apollo Server automatically installs and enables this plugin with default settings if you [provide a graph API key and a graph ref to Apollo Server](../../monitoring/metrics/#connecting-to-apollo-studio) and your server is not a federated subgraph. You usually do this by setting the `APOLLO_KEY` and `APOLLO_GRAPH_REF` (or `APOLLO_GRAPH_ID` and `APOLLO_GRAPH_VARIANT`) environment variables. No other action is required.

If you don't provide an API key and graph ref, this plugin is not installed.
If you don't provide an API key and graph ref, or if your server is a federated subgraph, this plugin is not automatically installed.

If you provide an API key but _don't_ provide a graph ref, a warning is logged. You can [disable the plugin](#disabling-the-plugin) to hide the warning.

If you provide an API key and graph ref but your server _is_ a federated subgraph, a warning is logged. You can [disable the plugin](#disabling-the-plugin) to hide the warning.

## Custom installation

If you want to configure the usage reporting plugin, import it and pass it to your `ApolloServer` constructor's `plugins` array:
Expand All @@ -38,6 +42,8 @@ const server = new ApolloServer({

</MultiCodeBlock>

> While you can install the usage reporting plugin in a server that is a federated subgraph, this is not recommended, and a warning will be logged.
Supported configuration options are listed below.

#### Options
Expand Down Expand Up @@ -529,4 +535,4 @@ const server = new ApolloServer({

</MultiCodeBlock>

This also disables the warning log if you provide an API key but do not provide a graph ref.
This also disables the warning log if you provide an API key but do not provide a graph ref, or if you provide an API key and graph ref and your server is a federated subgraph.
9 changes: 9 additions & 0 deletions docs/source/migration.mdx
Expand Up @@ -1743,6 +1743,15 @@ new ApolloServer({

(As [described above](#rewriteerror-plugin-option), the `rewriteError` option has been replaced by a `transform` option on `sendErrors` or `includeErrors`.)


### Usage reporting plugin is off by default on subgraphs

In an Apollo Federation supergraph, your Apollo Gateway or Apollo Router sends [usage reports](./api/plugin/usage-reporting/) to Apollo's servers; information about what happens inside individual subgraph servers is sent from the subgraphs to the Gateway or Router via [inline traces](./api/plugin/inline-trace/). That is to say: the usage reporting plugin is *not* designed for use in federated subgraphs.

In Apollo Server 3, if you provide an Apollo API key and graph ref and do not explicitly install the `ApolloServerPluginUsageReporting` or `ApolloServerPluginUsageReportingDisabled` plugins, the `ApolloServerPluginUsageReporting` plugin will be installed with its default configuration, even if the server is a subgraph.

In Apollo Server 4, this automatic installation does not occur in federated subgraphs. You still can explicitly install `ApolloServerPluginUsageReporting` in your subgraph, though this is not recommended and a warning will be logged.

## Renamed packages

The following packages have been renamed in Apollo Server 4:
Expand Down
8 changes: 6 additions & 2 deletions packages/server/src/ApolloServer.ts
Expand Up @@ -869,7 +869,11 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
const { ApolloServerPluginUsageReporting } = await import(
'./plugin/usageReporting/index.js'
);
plugins.unshift(ApolloServerPluginUsageReporting());
plugins.unshift(
ApolloServerPluginUsageReporting({
__onlyIfSchemaIsNotSubgraph: true,
}),
);
} else {
this.logger.warn(
'You have specified an Apollo key but have not specified a graph ref; usage ' +
Expand Down Expand Up @@ -917,7 +921,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
'./plugin/inlineTrace/index.js'
);
plugins.push(
ApolloServerPluginInlineTrace({ __onlyIfSchemaIsFederated: true }),
ApolloServerPluginInlineTrace({ __onlyIfSchemaIsSubgraph: true }),
);
}
}
Expand Down
18 changes: 9 additions & 9 deletions packages/server/src/plugin/inlineTrace/index.ts
Expand Up @@ -2,7 +2,7 @@ import { Trace } from '@apollo/usage-reporting-protobuf';
import { TraceTreeBuilder } from '../traceTreeBuilder.js';
import type { SendErrorsOptions } from '../usageReporting/index.js';
import { internalPlugin } from '../../internalPlugin.js';
import { schemaIsFederated } from '../schemaIsFederated.js';
import { schemaIsSubgraph } from '../schemaIsSubgraph.js';
import type { ApolloServerPlugin } from '../../externalTypes/index.js';

export interface ApolloServerPluginInlineTraceOptions {
Expand All @@ -29,14 +29,14 @@ export interface ApolloServerPluginInlineTraceOptions {
/**
* This option is for internal use by `@apollo/server` only.
*
* By default we want to enable this plugin for federated schemas only, but we
* By default we want to enable this plugin for subgraph schemas only, but we
* need to come up with our list of plugins before we have necessarily loaded
* the schema. So (unless the user installs this plugin or
* ApolloServerPluginInlineTraceDisabled themselves), `@apollo/server`
* always installs this plugin and uses this option to make sure traces are
* only included if the schema appears to be federated.
* ApolloServerPluginInlineTraceDisabled themselves), `@apollo/server` always
* installs this plugin and uses this option to make sure traces are only
* included if the schema appears to be a subgraph.
*/
__onlyIfSchemaIsFederated?: boolean;
__onlyIfSchemaIsSubgraph?: boolean;
}

// This ftv1 plugin produces a base64'd Trace protobuf containing only the
Expand All @@ -47,7 +47,7 @@ export interface ApolloServerPluginInlineTraceOptions {
export function ApolloServerPluginInlineTrace(
options: ApolloServerPluginInlineTraceOptions = Object.create(null),
): ApolloServerPlugin {
let enabled: boolean | null = options.__onlyIfSchemaIsFederated ? null : true;
let enabled: boolean | null = options.__onlyIfSchemaIsSubgraph ? null : true;
return internalPlugin({
__internal_plugin_id__: 'InlineTrace',
__is_disabled_plugin__: false,
Expand All @@ -57,10 +57,10 @@ export function ApolloServerPluginInlineTrace(
// like the log line, just install `ApolloServerPluginInlineTrace()` in
// `plugins` yourself.
if (enabled === null) {
enabled = schemaIsFederated(schema);
enabled = schemaIsSubgraph(schema);
if (enabled) {
logger.info(
'Enabling inline tracing for this federated service. To disable, use ' +
'Enabling inline tracing for this subgraph. To disable, use ' +
'ApolloServerPluginInlineTraceDisabled.',
);
}
Expand Down
33 changes: 0 additions & 33 deletions packages/server/src/plugin/schemaIsFederated.ts

This file was deleted.

32 changes: 32 additions & 0 deletions packages/server/src/plugin/schemaIsSubgraph.ts
@@ -0,0 +1,32 @@
import { GraphQLSchema, isObjectType, isScalarType } from 'graphql';

// Returns true if it appears that the schema was appears to be of a subgraph
// (eg, returned from @apollo/subgraph's buildSubgraphSchema). This strategy
// avoids depending explicitly on @apollo/subgraph or relying on something that
// might not survive transformations like monkey-patching a boolean field onto
// the schema.
//
// This is used for two things:
// 1) Determining whether traces should be added to responses if requested with
// an HTTP header. If you want to include these traces even for non-subgraphs
// (when requested via header, eg for Apollo Explorer's trace view) you can
// use ApolloServerPluginInlineTrace explicitly; if you want to never include
// these traces even for subgraphs you can use
// ApolloServerPluginInlineTraceDisabled.
// 2) Determining whether schema-reporting should be allowed; subgraphs cannot
// report schemas, and we accordingly throw if it's attempted.
export function schemaIsSubgraph(schema: GraphQLSchema): boolean {
const serviceType = schema.getType('_Service');
if (!isObjectType(serviceType)) {
return false;
}
const sdlField = serviceType.getFields().sdl;
if (!sdlField) {
return false;
}
const sdlFieldType = sdlField.type;
if (!isScalarType(sdlFieldType)) {
return false;
}
return sdlFieldType.name == 'String';
}
10 changes: 5 additions & 5 deletions packages/server/src/plugin/schemaReporting/index.ts
Expand Up @@ -3,7 +3,7 @@ import { internalPlugin } from '../../internalPlugin.js';
import { v4 as uuidv4 } from 'uuid';
import { printSchema, validateSchema, buildSchema } from 'graphql';
import { SchemaReporter } from './schemaReporter.js';
import { schemaIsFederated } from '../schemaIsFederated.js';
import { schemaIsSubgraph } from '../schemaIsSubgraph.js';
import type { SchemaReport } from './generated/operations.js';
import type { ApolloServerPlugin } from '../../externalTypes/index.js';
import type { Fetcher } from '@apollo/utils.fetcher';
Expand Down Expand Up @@ -108,12 +108,12 @@ export function ApolloServerPluginSchemaReporting(
}
}

if (schemaIsFederated(schema)) {
if (schemaIsSubgraph(schema)) {
throw Error(
[
'Schema reporting is not yet compatible with federated services.',
"If you're interested in using schema reporting with federated",
'services, please contact Apollo support. To set up managed federation, see',
'Schema reporting is not yet compatible with Apollo Federation subgraphs.',
"If you're interested in using schema reporting with subgraphs,",
'please contact Apollo support. To set up managed federation, see',
'https://go.apollo.dev/s/managed-federation',
].join(' '),
);
Expand Down
11 changes: 11 additions & 0 deletions packages/server/src/plugin/usageReporting/options.ts
Expand Up @@ -367,6 +367,17 @@ export interface ApolloServerPluginUsageReportingOptions<
* about how the signature relates to the operation you executed.
*/
calculateSignature?: (ast: DocumentNode, operationName: string) => string;
/**
* This option is for internal use by `@apollo/server` only.
*
* By default we want to enable this plugin for non-subgraph schemas only, but
* we need to come up with our list of plugins before we have necessarily
* loaded the schema. So (unless the user installs this plugin or
* ApolloServerPluginUsageReportingDisabled themselves), `@apollo/server`
* always installs this plugin (if API key and graph ref are provided) and
* uses this option to disable usage reporting if the schema is a subgraph.
*/
__onlyIfSchemaIsNotSubgraph?: boolean;
//#endregion
}

Expand Down
47 changes: 37 additions & 10 deletions packages/server/src/plugin/usageReporting/plugin.ts
Expand Up @@ -39,6 +39,7 @@ import { makeTraceDetails } from './traceDetails.js';
import { packageVersion } from '../../generated/packageVersion.js';
import { computeCoreSchemaHash } from '../../utils/computeCoreSchemaHash.js';
import type { HeaderMap } from '../../utils/HeaderMap.js';
import { schemaIsSubgraph } from '../schemaIsSubgraph.js';

const gzipPromise = promisify(gzip);

Expand Down Expand Up @@ -70,9 +71,11 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
? fieldLevelInstrumentationOption
: async () => true;

let requestDidStartHandler: (
requestContext: GraphQLRequestContext<TContext>,
) => GraphQLRequestListener<TContext>;
let requestDidStartHandler:
| ((
requestContext: GraphQLRequestContext<TContext>,
) => GraphQLRequestListener<TContext>)
| null = null;
return internalPlugin({
__internal_plugin_id__: 'UsageReporting',
__is_disabled_plugin__: false,
Expand All @@ -81,20 +84,19 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
// this little hack. (Perhaps we should also allow GraphQLServerListener to contain
// a requestDidStart?)
async requestDidStart(requestContext: GraphQLRequestContext<TContext>) {
if (!requestDidStartHandler) {
throw Error(
'The usage reporting plugin has been asked to handle a request before the ' +
'server has started. See https://github.com/apollographql/apollo-server/issues/4588 ' +
'for more details.',
);
if (requestDidStartHandler) {
return requestDidStartHandler(requestContext);
}
return requestDidStartHandler(requestContext);
// This happens if usage reporting is disabled (eg because this is a
// subgraph).
return {};
},

async serverWillStart({
logger: serverLogger,
apollo,
startedInBackground,
schema,
}): Promise<GraphQLServerListener> {
// Use the plugin-specific logger if one is provided; otherwise the general server one.
const logger = options.logger ?? serverLogger;
Expand All @@ -108,6 +110,31 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
);
}

if (schemaIsSubgraph(schema)) {
if (options.__onlyIfSchemaIsNotSubgraph) {
logger.warn(
'You have specified an Apollo API key and graph ref but this server appears ' +
'to be a subgraph. Typically usage reports are sent to Apollo by your Router ' +
'or Gateway, not directly from your subgraph; usage reporting is disabled. To ' +
'enable usage reporting anyway, explicitly install `ApolloServerPluginUsageReporting`. ' +
'To disable this warning, install `ApolloServerPluginUsageReportingDisabled`.',
);
// This early return means we don't start background timers, don't
// register serverDidStart, don't assign requestDidStartHandler, etc.
return {};
} else {
// This is just a warning; usage reporting is still enabled. If it
// turns out there are lots of people who really need to have this odd
// setup and they don't like the warning, we can provide a new option
// to disable the warning (or they can filter in their `logger`).
logger.warn(
'You have installed `ApolloServerPluginUsageReporting` but this server appears to ' +
'be a subgraph. Typically usage reports are sent to Apollo by your Router ' +
'or Gateway, not directly from your subgraph.',
);
}
}

logger.info(
'Apollo usage reporting starting! See your graph at ' +
`https://studio.apollographql.com/graph/${encodeURI(graphRef)}/`,
Expand Down

0 comments on commit b1548c1

Please sign in to comment.