diff --git a/.changeset/rude-steaks-smell.md b/.changeset/rude-steaks-smell.md new file mode 100644 index 00000000000..09cd5ddbc00 --- /dev/null +++ b/.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. diff --git a/docs/source/api/plugin/usage-reporting.mdx b/docs/source/api/plugin/usage-reporting.mdx index 45f70379793..fd9ef6b0b7d 100644 --- a/docs/source/api/plugin/usage-reporting.mdx +++ b/docs/source/api/plugin/usage-reporting.mdx @@ -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: @@ -38,6 +42,8 @@ const server = new ApolloServer({ +> 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 @@ -529,4 +535,4 @@ const server = new ApolloServer({ -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. diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx index b7165952a7b..7a96d06bf43 100644 --- a/docs/source/migration.mdx +++ b/docs/source/migration.mdx @@ -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: diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 123de66aafb..13f88e5f5ee 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -869,7 +869,11 @@ export class ApolloServer { 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 ' + @@ -917,7 +921,7 @@ export class ApolloServer { './plugin/inlineTrace/index.js' ); plugins.push( - ApolloServerPluginInlineTrace({ __onlyIfSchemaIsFederated: true }), + ApolloServerPluginInlineTrace({ __onlyIfSchemaIsSubgraph: true }), ); } } diff --git a/packages/server/src/plugin/inlineTrace/index.ts b/packages/server/src/plugin/inlineTrace/index.ts index fa2de0215b8..0c8ba0ac587 100644 --- a/packages/server/src/plugin/inlineTrace/index.ts +++ b/packages/server/src/plugin/inlineTrace/index.ts @@ -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 { @@ -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 @@ -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, @@ -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.', ); } diff --git a/packages/server/src/plugin/schemaIsFederated.ts b/packages/server/src/plugin/schemaIsFederated.ts deleted file mode 100644 index 61b3dda7d1e..00000000000 --- a/packages/server/src/plugin/schemaIsFederated.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GraphQLSchema, isObjectType, isScalarType } from 'graphql'; - -// Returns true if it appears that the schema was returned from -// @apollo/federation's buildFederatedSchema. This strategy avoids depending -// explicitly on @apollo/federation 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-federated schemas (when requested via header) you can use -// ApolloServerPluginInlineTrace yourself; if you want to never -// include these traces even for federated schemas you can use -// ApolloServerPluginInlineTraceDisabled. -// 2) Determining whether schema-reporting should be allowed; federated -// services shouldn't be reporting schemas, and we accordingly throw if -// it's attempted. -export function schemaIsFederated(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'; -} diff --git a/packages/server/src/plugin/schemaIsSubgraph.ts b/packages/server/src/plugin/schemaIsSubgraph.ts new file mode 100644 index 00000000000..88e75aecbc5 --- /dev/null +++ b/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'; +} diff --git a/packages/server/src/plugin/schemaReporting/index.ts b/packages/server/src/plugin/schemaReporting/index.ts index 3ff24e86ccd..780e32ba80d 100644 --- a/packages/server/src/plugin/schemaReporting/index.ts +++ b/packages/server/src/plugin/schemaReporting/index.ts @@ -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'; @@ -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(' '), ); diff --git a/packages/server/src/plugin/usageReporting/options.ts b/packages/server/src/plugin/usageReporting/options.ts index 6936bc505e3..62b7ff7d62a 100644 --- a/packages/server/src/plugin/usageReporting/options.ts +++ b/packages/server/src/plugin/usageReporting/options.ts @@ -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 } diff --git a/packages/server/src/plugin/usageReporting/plugin.ts b/packages/server/src/plugin/usageReporting/plugin.ts index 1461b16d0a0..6f4e256997d 100644 --- a/packages/server/src/plugin/usageReporting/plugin.ts +++ b/packages/server/src/plugin/usageReporting/plugin.ts @@ -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); @@ -70,9 +71,11 @@ export function ApolloServerPluginUsageReporting( ? fieldLevelInstrumentationOption : async () => true; - let requestDidStartHandler: ( - requestContext: GraphQLRequestContext, - ) => GraphQLRequestListener; + let requestDidStartHandler: + | (( + requestContext: GraphQLRequestContext, + ) => GraphQLRequestListener) + | null = null; return internalPlugin({ __internal_plugin_id__: 'UsageReporting', __is_disabled_plugin__: false, @@ -81,20 +84,19 @@ export function ApolloServerPluginUsageReporting( // this little hack. (Perhaps we should also allow GraphQLServerListener to contain // a requestDidStart?) async requestDidStart(requestContext: GraphQLRequestContext) { - 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 { // Use the plugin-specific logger if one is provided; otherwise the general server one. const logger = options.logger ?? serverLogger; @@ -108,6 +110,31 @@ export function ApolloServerPluginUsageReporting( ); } + 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)}/`,