Skip to content

Commit

Permalink
feat: Automatic schema reporting to Apollo Graph Manager (#4084)
Browse files Browse the repository at this point in the history
This commit implements the new schema reporting protocol for Apollo Server's
reporting facilities and can be enabled by providing a Graph Manager API key
available from Apollo Graph Manager in the `APOLLO_KEY` environment variable
*and* setting the `experimental_schemaReporting` option to `true` in the
Apollo Server constructor options, like so:

```js
const server = new ApolloServer({
  typeDefs,
  resolvers,
  engine: {
    experimental_schemaReporting: true,
    /* Other existing options can remain the same. */
  },
});
```

When schema-reporting is enabled, Apollo server will send the running schema
and server information to Apollo Graph Manager. The extra runtime information
will be used to power _auto-promotion_ which will set a schema as active for
a _variant_, based on the algorithm described in the [Preview documentation].

When enabled, a schema reporting interval is initiated by the
`apollo-engine-reporting` agent.  It will loop until the `ApolloServer`
instance is stopped, periodically calling back to Apollo Graph Manager to
send information.  The life-cycle of this loop is managed by the reporting
agent.

Additionally, this commit deprecates `schemaHash` in metrics reporting in
favor of using the same `executableSchemaId` used within this new schema
reporting protocol.

[Preview documentation]: https://github.com/apollographql/apollo-schema-reporting-preview-docs#automatic-promotion-in-apollo-graph-manager
  • Loading branch information
jsegaran authored May 19, 2020
1 parent 7dd68b3 commit 8b92145
Show file tree
Hide file tree
Showing 13 changed files with 920 additions and 66 deletions.
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@types/supertest": "^2.0.8",
"@types/test-listen": "1.1.0",
"@types/type-is": "1.6.3",
"@types/uuid": "^7.0.3",
"@types/ws": "7.2.4",
"apollo-fetch": "0.7.0",
"apollo-link": "1.2.14",
Expand Down
6 changes: 4 additions & 2 deletions packages/apollo-engine-reporting-protobuf/src/reports.proto
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ message ReportHeader {
string uname = 9;
// eg "current", "prod"
string schema_tag = 10;
// The hex representation of the sha512 of the introspection response
string schema_hash = 11;
// An id that is used to represent the schema to Apollo Graph Manager
// Using this in place of what used to be schema_hash, since that is no longer
// attached to a schema in the backend.
string executable_schema_id = 11;

reserved 3; // removed string service = 3;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/apollo-engine-reporting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"apollo-server-errors": "file:../apollo-server-errors",
"apollo-server-plugin-base": "file:../apollo-server-plugin-base",
"apollo-server-types": "file:../apollo-server-types",
"async-retry": "^1.2.1"
"async-retry": "^1.2.1",
"uuid": "^8.0.0"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
Expand Down
57 changes: 56 additions & 1 deletion packages/apollo-engine-reporting/src/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
signatureCacheKey,
handleLegacyOptions,
EngineReportingOptions,
} from '../agent';
computeExecutableSchemaId
} from "../agent";
import { buildSchema } from "graphql";

describe('signature cache key', () => {
it('generates without the operationName', () => {
Expand All @@ -16,6 +18,59 @@ describe('signature cache key', () => {
});
});

describe('Executable Schema Id', () => {
const unsortedGQLSchemaDocument = `
directive @example on FIELD
union AccountOrUser = Account | User
type Query {
userOrAccount(name: String, id: String): AccountOrUser
}
type User {
accounts: [Account!]
email: String
name: String!
}
type Account {
name: String!
id: ID!
}
`;

const sortedGQLSchemaDocument = `
directive @example on FIELD
union AccountOrUser = Account | User
type Account {
name: String!
id: ID!
}
type Query {
userOrAccount(id: String, name: String): AccountOrUser
}
type User {
accounts: [Account!]
email: String
name: String!
}
`;
it('does not normalize GraphQL schemas', () => {
expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).not.toEqual(
computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument))
);
});
it('does not normalize strings', () => {
expect(computeExecutableSchemaId(unsortedGQLSchemaDocument)).not.toEqual(
computeExecutableSchemaId(sortedGQLSchemaDocument)
);
});
});


describe("test handleLegacyOptions(), which converts the deprecated privateVariable and privateHeaders options to the new options' formats", () => {
it('Case 1: privateVariables/privateHeaders == False; same as all', () => {
const optionsPrivateFalse: EngineReportingOptions<any> = {
Expand Down
176 changes: 167 additions & 9 deletions packages/apollo-engine-reporting/src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { graphql, GraphQLError } from 'graphql';
import { graphql, GraphQLError, printSchema } from 'graphql';
import { Request } from 'node-fetch';
import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin';
import { Headers } from 'apollo-server-env';
import { AddTraceArgs } from '../agent';
import { AddTraceArgs, computeExecutableSchemaId } from '../agent';
import { Trace } from 'apollo-engine-reporting-protobuf';
import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness';

it('trace construction', async () => {
const typeDefs = `
const typeDefs = `
type User {
id: Int
name: String
Expand All @@ -31,7 +30,7 @@ it('trace construction', async () => {
}
`;

const query = `
const query = `
query q {
author(id: 5) {
name
Expand All @@ -43,17 +42,176 @@ it('trace construction', async () => {
}
`;

describe('schema reporting', () => {
const schema = makeExecutableSchema({ typeDefs });
addMockFunctionsToSchema({ schema });

const addTrace = jest.fn();
const startSchemaReporting = jest.fn();
const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId);

beforeEach(() => {
addTrace.mockClear();
startSchemaReporting.mockClear();
executableSchemaIdGenerator.mockClear();
});

it('starts reporting if enabled', async () => {
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
},
addTrace,
{
startSchemaReporting,
executableSchemaIdGenerator,
},
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: printSchema(schema),
executableSchemaId: executableSchemaIdGenerator(schema),
});
});

it('uses the override schema', async () => {
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
experimental_overrideReportedSchema: typeDefs,
},
addTrace,
{
startSchemaReporting,
executableSchemaIdGenerator,
},
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

const expectedExecutableSchemaId = executableSchemaIdGenerator(typeDefs);
expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: typeDefs,
executableSchemaId: expectedExecutableSchemaId,
});

// Get the first argument from the first time this is called.
// Not using called with because that has to be exhaustive and this isn't
// testing trace generation
expect(addTrace).toBeCalledWith(
expect.objectContaining({
executableSchemaId: expectedExecutableSchemaId,
}),
);
});

it('uses the same executable schema id for metric reporting', async () => {
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
},
addTrace,
{
startSchemaReporting,
executableSchemaIdGenerator,
},
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

const expectedExecutableSchemaId = executableSchemaIdGenerator(schema);
expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: printSchema(schema),
executableSchemaId: expectedExecutableSchemaId,
});
// Get the first argument from the first time this is called.
// Not using called with because that has to be exhaustive and this isn't
// testing trace generation
expect(addTrace.mock.calls[0][0].executableSchemaId).toBe(
expectedExecutableSchemaId,
);
});
});

it('trace construction', async () => {
const schema = makeExecutableSchema({ typeDefs });
addMockFunctionsToSchema({ schema });

const traces: Array<AddTraceArgs> = [];
async function addTrace(args: AddTraceArgs) {
traces.push(args);
}
const startSchemaReporting = jest.fn();
const executableSchemaIdGenerator = jest.fn();

const pluginInstance = plugin({ /* no options!*/ }, addTrace);
const pluginInstance = plugin(
{
/* no options!*/
},
addTrace,
{
startSchemaReporting,
executableSchemaIdGenerator,
},
);

pluginTestHarness({
await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
Expand All @@ -64,7 +222,7 @@ it('trace construction', async () => {
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source }}) => {
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
Expand Down Expand Up @@ -260,7 +418,7 @@ describe('variableJson output for sendVariableValues transform: custom function
).toEqual(JSON.stringify(null));
});

const errorThrowingModifier = (input: {
const errorThrowingModifier = (_input: {
variables: Record<string, any>;
}): Record<string, any> => {
throw new GraphQLError('testing error handling');
Expand Down
Loading

0 comments on commit 8b92145

Please sign in to comment.