feat: Automatic schema reporting to Apollo Graph Manager#4084
Conversation
Enrico2
left a comment
There was a problem hiding this comment.
The description here needs to be more detailed; this PR is doing 2 things, using the new schemaId field in metrics and adding support for schema reporting.
Had to stop mid-review, will come back after iteration..
94eaeb5 to
b3ca921
Compare
7a4e6ef to
30c11bc
Compare
47da111 to
24bca7a
Compare
f7d857d to
977a89e
Compare
abernix
left a comment
There was a problem hiding this comment.
I've left a few more asks within, but this is looking sharp.
abernix
left a comment
There was a problem hiding this comment.
I'll do some follow-up commits directly to this PR, but I think this mostly LGTM and is ready for an alpha.
| schema: string | GraphQLSchema, | ||
| ): string { | ||
| // Can't call digest on this object twice. Creating new object each function call | ||
| const sha256 = module.require('crypto').createHash('sha256'); |
There was a problem hiding this comment.
Note to self, I'll tweak this a bit before merging.
| }, | ||
| }); | ||
|
|
||
| await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); |
There was a problem hiding this comment.
I will add a change that checks the error message contents here for specific errors.
Having a code editor is nice.
The specificity of the `try`/`catch` around JSON parsing will ensure that HTML parsed as errors is insinuated clearly, whereas any general `fetch` error will propagate up to the error boundary provided by the `catch` that lives within the `reportingLoop` function after `schemaReporter.reportServerInfo` is invoked.
| }, | ||
| }); | ||
|
|
||
| await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); |
In the future, this might change back, but this can just be a top-level `import` right now. Ref: https://github.com/apollographql/apollo-server/pull/4084/files#r427103593
| @@ -0,0 +1,64 @@ | |||
| /* tslint:disable */ | |||
There was a problem hiding this comment.
Can you add an npm script to generate this file to package.json, or at least an explanation somewhere?
| endpointUrl?: string; | ||
| /** | ||
| * The URL to the Apollo Graph Manager ingress endpoint. | ||
| * (Previously, this was `endpointUrl`, which will be removed in AS3). |
| * **(Experimental)** Enable schema reporting from this server with | ||
| * Apollo Graph Manager. | ||
| * | ||
| * The use of this option avoids the need to rgister schemas manually within |
|
|
||
| /** | ||
| * The schema reporter waits before starting reporting. | ||
| * By default, the report waits some random amount of time between 0 and 10 seconds. |
There was a problem hiding this comment.
For what it's worth I find this approach to documenting the option a bit confusing.
The point is that there is a randomized delay, from 0 to 10 seconds. It always does that.
This lets you override the 10 second value, not the fact that it's a random amount of time.
This line
By default, the report waits some random amount of time between 0 and 10 seconds.
really should be more like
The report waits a random amount of time between 0 and (by default) 10 seconds.
It's also a bit odd for the first line of this whole comment to be a fact about how SR works rather than directly describing what the option does.
So something more like:
The maximum amount of time in milliseconds that the schema reporter waits before starting to report; defaults to 10 seconds.
The schema reporter waits a random amount of time up to this amount before reporting.
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 AWS Lambda or in other environments where processes are short-lived,
consider setting this value to a smaller number.
| private readonly logger: Logger = console; | ||
| private readonly graphVariant: string; | ||
|
|
||
| private reports: { [executableSchemaId: string]: Report } = Object.create( |
There was a problem hiding this comment.
(note: i wrote all the below before I realized that the only thing you did to these objects is prettify them. oops. well, it's still true. and i still want to fix it but i don't want to ask you to, so, um, have #4139)
If I understand correctly, there's a strong requirement here that reports, reportSizes, and reportHeaders all have the same keys all the time, and there's a bunch of implicit dependencies on that fact.
Can you instead combine them into a single map whose value type is a tiny class with three fields?
Additionally, the type {[x: string]: Foo} is actually problematic. It tells TypeScript "no matter what string I index by, I'll get a Foo out". But that's not how this data structure actually works! All of these should be changed into {[x: string]: Foo | undefined}, which in fact does reveal a bunch of places where we fetch an object from them and don't check to see if it was actually there...
| export class SchemaReporter { | ||
| // These mirror the gql variables | ||
| private readonly serverInfo: EdgeServerInfo; | ||
| private readonly executableSchemaDocument: any; |
| this.currentSchemaReporter = schemaReporter; | ||
| const logger = this.logger; | ||
|
|
||
| setTimeout(function() { |
There was a problem hiding this comment.
It's a little awkward that stop doesn't clearTimeout this timeout or any of the ones in reportingLoop; even if they won't do anything, you can imagine a context where this will require a program (like a test?) to stay running for up to 10 seconds before doing nothing! Leaking setTimeout return values isn't a great habit to get into anyway. Why not make inner from reportingLoop be a method on SchemaReporter, and save the return value from the active timeout on SchemaReporter, and clearTimeout on stop?
| ): Promise<SchemaReportingServerInfoResult> { | ||
| const request: GraphQLRequest = { | ||
| query: reportServerInfoGql, | ||
| operationName: 'ReportServerInfo', |
There was a problem hiding this comment.
There's no need to include this unless you have more than one (named) operation in query.
| const request: GraphQLRequest = { | ||
| query: reportServerInfoGql, | ||
| operationName: 'ReportServerInfo', | ||
| variables: variables, |
| [ | ||
| "Couldn't report server info to Apollo Graph Manager.", | ||
| 'Parsing response as JSON failed.', | ||
| 'If this continues please reach out to support@apollographql.com', |
There was a problem hiding this comment.
Putting just a space between our email address and the error will read strangely. Newline? Period?
Note: Schema reporting is not currently supported for managed federation
The Apollo Graph Manager schema registry is a free service of Apollo Graph Manager that allows tracking a graph (or variants of it) over time.
Previously, it was necessary to use the
apollo schema:pushcommand (usually in a CI pipeline) in order to push the latest state of the graph to Apollo Graph Manager and provide this visibility.Apollo schema reporting protocol, is a new specification for GraphQL servers to automatically report schemas to the Apollo Graph Manager schema registry.
This PR implements this 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_KEYenvironment variable and setting theexperimental_schemaReportingoption totruein the Apollo Server constructor options, like so: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-reportingagent. It will loop until theApolloServerinstance 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 PR deprecates
schemaHashin metrics reporting in favor of using the sameexecutableSchemaIdused within this new schema reporting protocol.