Skip to content

Commit

Permalink
Move basic configuration from engine to apollo
Browse files Browse the repository at this point in the history
Currently, the `engine` option to `new ApolloServer` contains a mishmash of data
including the Apollo API key, the graph variant, configuration options for usage
reporting, configuration options for schema reporting, and more. Also, Engine is
no longer the name of the Apollo commercial product (the web app is now called
Studio).

This is the first PR in a series of PRs which will replace this `engine`
option. Future PRs will refactor `EngineReportingAgent` into a set of smaller
`ApolloServerPlugin`s.

This PR addresses the basic configuration: API key, graph ID, and graph
variant. It adds a new option called `apollo` to `new ApolloServer` of type
`ApolloConfigInput`, where you can specify the API key, graph ID, and graph
variant. It adds a second type `ApolloConfig`, which is similar except it also
contains the API key hash, and the graph variant here is always
defined ('current' by default).  A single new internal determineApolloConfig
function is used to produce an `ApolloConfig` object at the top of the
ApolloServer constructor, consolidating a bunch of previously scattered code.

(This also consolidates some warnings. For example, we now consistently warn
when you create an ApolloServer and an API key is available but a variant is not
specified, instead of doing that in just ApolloGateway.)

It is an error to pass both `apollo` and `engine` to `new ApolloServer`. This
does mean that if you want to configure any of the other aspects of reporting,
you can not yet use the `apollo` option. I do not expect this PR to be released
until all of that configuration can be done via separate plugins as mentioned
above.

This `ApolloConfig` object is now passed to `ApolloServerPlugin`s in
`serverWillStart`, to `ApolloGateway` in `load`, and to the
`EngineReportingAgent` constructor.  The former two still also receive `engine`
options for backwards compatibility.  `ApolloGateway` will be updated to be able
to read both `apollo` and `engine` in
apollographql/federation#148

(Note that I've intentionally applied `prettier` to apollo-engine-reporting's
agent.ts, as that package's files have generally been kept prettier-clean. That
file will be deleted soon anyway.)
  • Loading branch information
glasser committed Sep 11, 2020
1 parent 822d786 commit 52ac486
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 212 deletions.
160 changes: 45 additions & 115 deletions packages/apollo-engine-reporting/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
GraphQLRequestContextDidEncounterErrors,
GraphQLRequestContextDidResolveOperation,
Logger,
ApolloConfig,
WithRequired,
} from 'apollo-server-types';
import { InMemoryLRUCache } from 'apollo-server-caching';
import { defaultEngineReportingSignature } from 'apollo-graphql';
Expand All @@ -30,8 +32,6 @@ import { reportingLoop, SchemaReporter } from './schemaReporter';
import { v4 as uuidv4 } from 'uuid';
import { createHash } from 'crypto';

let warnedOnDeprecatedApiKey = false;

export interface ClientInfo {
clientName?: string;
clientVersion?: string;
Expand Down Expand Up @@ -69,75 +69,6 @@ export type GenerateClientInfo<TContext> = (
requestContext: GraphQLRequestContext<TContext>,
) => ClientInfo;

// AS3: Drop support for deprecated `ENGINE_API_KEY`.
export function getEngineApiKey({
engine,
skipWarn = false,
logger = console,
}: {
engine: EngineReportingOptions<any> | boolean | undefined;
skipWarn?: boolean;
logger?: Logger;
}) {
if (typeof engine === 'object') {
if (engine.apiKey) {
return engine.apiKey;
}
}
const legacyApiKeyFromEnv = process.env.ENGINE_API_KEY;
const apiKeyFromEnv = process.env.APOLLO_KEY;

if (legacyApiKeyFromEnv && apiKeyFromEnv && !skipWarn) {
logger.warn(
'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.',
);
}
if (legacyApiKeyFromEnv && !warnedOnDeprecatedApiKey && !skipWarn) {
logger.warn(
'[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.',
);
warnedOnDeprecatedApiKey = true;
}
return apiKeyFromEnv || legacyApiKeyFromEnv || '';
}

// AS3: Drop support for deprecated `ENGINE_SCHEMA_TAG`.
export function getEngineGraphVariant(
engine: EngineReportingOptions<any> | boolean | undefined,
logger: Logger = console,
): string | undefined {
if (engine === false) {
return;
} else if (
typeof engine === 'object' &&
(engine.graphVariant || engine.schemaTag)
) {
if (engine.graphVariant && engine.schemaTag) {
throw new Error(
'Cannot set both engine.graphVariant and engine.schemaTag. Please use engine.graphVariant.',
);
}
if (engine.schemaTag) {
logger.warn(
'[deprecated] The `schemaTag` property within `engine` configuration has been renamed to `graphVariant`.',
);
}
return engine.graphVariant || engine.schemaTag;
} else {
if (process.env.ENGINE_SCHEMA_TAG) {
logger.warn(
'[deprecated] The `ENGINE_SCHEMA_TAG` environment variable has been renamed to `APOLLO_GRAPH_VARIANT`.',
);
}
if (process.env.ENGINE_SCHEMA_TAG && process.env.APOLLO_GRAPH_VARIANT) {
throw new Error(
'`APOLLO_GRAPH_VARIANT` and `ENGINE_SCHEMA_TAG` (deprecated) environment variables must not both be set.',
);
}
return process.env.APOLLO_GRAPH_VARIANT || process.env.ENGINE_SCHEMA_TAG;
}
}

export interface EngineReportingOptions<TContext> {
/**
* API key for the service. Get this from
Expand Down Expand Up @@ -429,9 +360,8 @@ class ReportData {
// to the Engine server.
export class EngineReportingAgent<TContext = any> {
private readonly options: EngineReportingOptions<TContext>;
private readonly apiKey: string;
private readonly apolloConfig: WithRequired<ApolloConfig, 'key'>;
private readonly logger: Logger = console;
private readonly graphVariant: string;

private readonly reportDataByExecutableSchemaId: {
[executableSchemaId: string]: ReportData | undefined;
Expand All @@ -452,29 +382,25 @@ export class EngineReportingAgent<TContext = any> {
private readonly tracesEndpointUrl: string;
readonly schemaReport: boolean;

public constructor(options: EngineReportingOptions<TContext> = {}) {
public constructor(
options: EngineReportingOptions<TContext> = {},
apolloConfig: WithRequired<ApolloConfig, 'key'>,
) {
this.options = options;
this.apiKey = getEngineApiKey({
engine: this.options,
skipWarn: false,
logger: this.logger,
});
if (options.logger) this.logger = options.logger;
this.bootId = uuidv4();
this.graphVariant = getEngineGraphVariant(options, this.logger) || '';

if (!this.apiKey) {
throw new Error(
`To use EngineReportingAgent, you must specify an API key via the apiKey option or the APOLLO_KEY environment variable.`,
);
if (!apolloConfig.key) {
throw new Error('Missing API key.');
}
this.apolloConfig = apolloConfig;

if (options.experimental_schemaReporting !== undefined) {
this.logger.warn(
[
'[deprecated] The "experimental_schemaReporting" option has been',
'renamed to "reportSchema"'
].join(' ')
'renamed to "reportSchema"',
].join(' '),
);
if (options.reportSchema === undefined) {
options.reportSchema = options.experimental_schemaReporting;
Expand All @@ -485,30 +411,32 @@ export class EngineReportingAgent<TContext = any> {
this.logger.warn(
[
'[deprecated] The "experimental_overrideReportedSchema" option has',
'been renamed to "overrideReportedSchema"'
].join(' ')
'been renamed to "overrideReportedSchema"',
].join(' '),
);
if (options.overrideReportedSchema === undefined) {
options.overrideReportedSchema = options.experimental_overrideReportedSchema;
options.overrideReportedSchema =
options.experimental_overrideReportedSchema;
}
}

if (options.experimental_schemaReportingInitialDelayMaxMs !== undefined) {
this.logger.warn(
[
'[deprecated] The "experimental_schemaReportingInitialDelayMaxMs"',
'option has been renamed to "schemaReportingInitialDelayMaxMs"'
].join(' ')
'option has been renamed to "schemaReportingInitialDelayMaxMs"',
].join(' '),
);
if (options.schemaReportingInitialDelayMaxMs === undefined) {
options.schemaReportingInitialDelayMaxMs = options.experimental_schemaReportingInitialDelayMaxMs;
options.schemaReportingInitialDelayMaxMs =
options.experimental_schemaReportingInitialDelayMaxMs;
}
}

if (options.reportSchema !== undefined) {
this.schemaReport = options.reportSchema;
} else {
this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === "true"
this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === 'true';
}

// Since calculating the signature for Engine reporting is potentially an
Expand Down Expand Up @@ -567,7 +495,7 @@ export class EngineReportingAgent<TContext = any> {
if (existing) {
return existing;
}
const reportData = new ReportData(executableSchemaId, this.graphVariant);
const reportData = new ReportData(executableSchemaId, this.apolloConfig.graphVariant);
this.reportDataByExecutableSchemaId[executableSchemaId] = reportData;
return reportData;
}
Expand Down Expand Up @@ -636,7 +564,7 @@ export class EngineReportingAgent<TContext = any> {

public async sendAllReports(): Promise<void> {
await Promise.all(
Object.keys(this.reportDataByExecutableSchemaId).map(id =>
Object.keys(this.reportDataByExecutableSchemaId).map((id) =>
this.sendReport(id),
),
);
Expand Down Expand Up @@ -702,7 +630,7 @@ export class EngineReportingAgent<TContext = any> {
method: 'POST',
headers: {
'user-agent': 'apollo-engine-reporting',
'x-api-key': this.apiKey,
'x-api-key': this.apolloConfig.key!,
'content-encoding': 'gzip',
},
body: compressed,
Expand All @@ -711,8 +639,9 @@ export class EngineReportingAgent<TContext = any> {

if (curResponse.status >= 500 && curResponse.status < 600) {
throw new Error(
`HTTP status ${curResponse.status}, ${(await curResponse.text()) ||
'(no body)'}`,
`HTTP status ${curResponse.status}, ${
(await curResponse.text()) || '(no body)'
}`,
);
} else {
return curResponse;
Expand Down Expand Up @@ -763,22 +692,22 @@ export class EngineReportingAgent<TContext = any> {
this.logger.info('Schema to report has been overridden');
}
if (this.options.schemaReportingInitialDelayMaxMs !== undefined) {
this.logger.info(`Schema reporting max initial delay override: ${
this.options.schemaReportingInitialDelayMaxMs
} ms`);
this.logger.info(
`Schema reporting max initial delay override: ${this.options.schemaReportingInitialDelayMaxMs} ms`,
);
}
if (this.options.schemaReportingUrl !== undefined) {
this.logger.info(`Schema reporting URL override: ${
this.options.schemaReportingUrl
}`);
this.logger.info(
`Schema reporting URL override: ${this.options.schemaReportingUrl}`,
);
}
if (this.currentSchemaReporter) {
this.currentSchemaReporter.stop();
}

const serverInfo = {
bootId: this.bootId,
graphVariant: this.graphVariant,
graphVariant: this.apolloConfig.graphVariant,
// 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',
Expand All @@ -796,29 +725,28 @@ export class EngineReportingAgent<TContext = any> {
};

this.logger.info(
`Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}`
)
`Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}`,
);

// Jitter the startup between 0 and 10 seconds
const delay = Math.floor(
Math.random() *
(this.options.schemaReportingInitialDelayMaxMs || 10_000),
Math.random() * (this.options.schemaReportingInitialDelayMaxMs || 10_000),
);

const schemaReporter = new SchemaReporter(
serverInfo,
executableSchema,
this.apiKey,
this.apolloConfig.key!,
this.options.schemaReportingUrl,
this.logger
this.logger,
);

const fallbackReportingDelayInMs = 20_000;

this.currentSchemaReporter = schemaReporter;
const logger = this.logger;

setTimeout(function() {
setTimeout(function () {
reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs);
}, delay);
}
Expand Down Expand Up @@ -896,7 +824,7 @@ export class EngineReportingAgent<TContext = any> {
// either the request-specific logger on the request context (if available)
// or to the `logger` that was passed into `EngineReportingOptions` which
// is provided in the `EngineReportingAgent` constructor options.
this.signatureCache.set(cacheKey, generatedSignature).catch(err => {
this.signatureCache.set(cacheKey, generatedSignature).catch((err) => {
logger.warn(
'Could not store signature cache. ' + (err && err.message) || err,
);
Expand All @@ -907,14 +835,16 @@ export class EngineReportingAgent<TContext = any> {

public async sendAllReportsAndReportErrors(): Promise<void> {
await Promise.all(
Object.keys(this.reportDataByExecutableSchemaId).map(executableSchemaId =>
Object.keys(
this.reportDataByExecutableSchemaId,
).map((executableSchemaId) =>
this.sendReportAndReportErrors(executableSchemaId),
),
);
}

private sendReportAndReportErrors(executableSchemaId: string): Promise<void> {
return this.sendReport(executableSchemaId).catch(err => {
return this.sendReport(executableSchemaId).catch((err) => {
// This catch block is primarily intended to catch network errors from
// the retried request itself, which include network errors and non-2xx
// HTTP errors.
Expand Down
Loading

0 comments on commit 52ac486

Please sign in to comment.