@apollo/server
Version:
Core engine for Apollo GraphQL server
199 lines (187 loc) • 7.65 kB
text/typescript
import os from 'os';
import { internalPlugin } from '../../internalPlugin.js';
import { v4 as uuidv4 } from 'uuid';
import { printSchema, validateSchema, buildSchema } from 'graphql';
import { SchemaReporter } from './schemaReporter.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';
import { packageVersion } from '../../generated/packageVersion.js';
import { computeCoreSchemaHash } from '../../utils/computeCoreSchemaHash.js';
export interface ApolloServerPluginSchemaReportingOptions {
/**
* The schema reporter waits before starting reporting.
* By default, the report waits some random amount of time between 0 and 10 seconds.
* A longer interval leads to more staggered starts which means it is less likely
* multiple servers will get asked to upload the same schema.
*
* If this server runs in lambda or in other constrained environments it would be useful
* to decrease the schema reporting max wait time to be less than default.
*
* This number will be the max for the range in ms that the schema reporter will
* wait before starting to report.
*/
initialDelayMaxMs?: number;
/**
* Override the reported schema that is reported to the Apollo registry. This
* schema does not go through any normalizations and the string is directly
* sent to the Apollo registry. This can be useful for comments or other
* ordering and whitespace changes that get stripped when generating a
* `GraphQLSchema`.
*
* **If you pass this option to this plugin, you should explicitly configure
* `ApolloServerPluginUsageReporting` and pass the same value to its
* `overrideReportedSchema` option.** This ensures that the schema ID
* associated with requests reported by the usage reporting plugin matches the
* schema ID that this plugin reports. For example:
*
* ```js
* new ApolloServer({
* plugins: [
* ApolloServerPluginSchemaReporting({overrideReportedSchema: schema}),
* ApolloServerPluginUsageReporting({overrideReportedSchema: schema}),
* ],
* })
* ```
*/
overrideReportedSchema?: string;
/**
* The URL to use for reporting schemas. Primarily for testing and internal
* Apollo use.
*/
endpointUrl?: string;
/**
* Specifies which Fetch API implementation to use when reporting schemas.
*/
fetcher?: Fetcher;
}
export function ApolloServerPluginSchemaReporting(
{
initialDelayMaxMs,
overrideReportedSchema,
endpointUrl,
fetcher,
}: ApolloServerPluginSchemaReportingOptions = Object.create(null),
): ApolloServerPlugin {
const bootId = uuidv4();
return internalPlugin({
__internal_plugin_id__: 'SchemaReporting',
__is_disabled_plugin__: false,
async serverWillStart({ apollo, schema, logger }) {
const { key, graphRef } = apollo;
if (!key) {
throw Error(
'To use ApolloServerPluginSchemaReporting, you must provide an Apollo API ' +
'key, via the APOLLO_KEY environment variable or via `new ApolloServer({apollo: {key})`',
);
}
if (!graphRef) {
// This error is a bit imprecise as you can also specify ID and variant separately,
// or rely on API-key parsing (before AS3), but this is "best practices".
throw Error(
'To use ApolloServerPluginSchemaReporting, you must provide your graph ref (eg, ' +
"'my-graph-id@my-graph-variant'). Try setting the APOLLO_GRAPH_REF environment " +
'variable or passing `new ApolloServer({apollo: {graphRef}})`.',
);
}
// Ensure a provided override schema can be parsed and validated
if (overrideReportedSchema) {
try {
const validationErrors = validateSchema(
buildSchema(overrideReportedSchema, { noLocation: true }),
);
if (validationErrors.length) {
throw new Error(
validationErrors.map((error) => error.message).join('\n'),
);
}
} catch (err) {
throw new Error(
'The schema provided to overrideReportedSchema failed to parse or ' +
`validate: ${(err as Error).message}`,
);
}
}
if (schemaIsSubgraph(schema)) {
throw Error(
[
'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(' '),
);
}
if (endpointUrl !== undefined) {
logger.info(
`Apollo schema reporting: schema reporting URL override: ${endpointUrl}`,
);
}
const baseSchemaReport: Omit<SchemaReport, 'coreSchemaHash'> = {
bootId,
graphRef,
// The infra environment in which this edge server is running, e.g. localhost, Kubernetes
// Length must be <= 256 characters.
platform: process.env.APOLLO_SERVER_PLATFORM || 'local',
runtimeVersion: `node ${process.version}`,
// An identifier used to distinguish the version of the server code such as git or docker sha.
// Length must be <= 256 characters
userVersion: process.env.APOLLO_SERVER_USER_VERSION,
// "An identifier for the server instance. Length must be <= 256 characters.
serverId:
process.env.APOLLO_SERVER_ID || process.env.HOSTNAME || os.hostname(),
libraryVersion: `@apollo/server@${packageVersion}`,
};
let currentSchemaReporter: SchemaReporter | undefined;
return {
schemaDidLoadOrUpdate({ apiSchema, coreSupergraphSdl }): void {
if (overrideReportedSchema !== undefined) {
if (currentSchemaReporter) {
// When the schema to report has been overridden, there is no need
// to create a new schema reporter.
return;
} else {
logger.info(
'Apollo schema reporting: schema to report has been overridden',
);
}
}
const coreSchema =
overrideReportedSchema ??
coreSupergraphSdl ??
printSchema(apiSchema);
const coreSchemaHash = computeCoreSchemaHash(coreSchema);
const schemaReport: SchemaReport = {
...baseSchemaReport,
coreSchemaHash,
};
currentSchemaReporter?.stop();
currentSchemaReporter = new SchemaReporter({
schemaReport,
coreSchema,
apiKey: key,
endpointUrl,
logger,
// Jitter the startup between 0 and 10 seconds
initialReportingDelayInMs: Math.floor(
Math.random() * (initialDelayMaxMs ?? 10_000),
),
fallbackReportingDelayInMs: 20_000,
fetcher,
});
currentSchemaReporter.start();
logger.info(
'Apollo schema reporting: reporting a new schema to Studio! See your graph at ' +
`https://studio.apollographql.com/graph/${encodeURI(
graphRef,
)}/ with server info ${JSON.stringify(schemaReport)}`,
);
},
async serverWillStop() {
currentSchemaReporter?.stop();
},
};
},
});
}