Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The version headers in this history reflect the versions of Apollo Server itself

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section.

- `apollo-engine-reporting`: fix reporting errors from backend. (The support for federated metrics introduced in v2.7.0 did not properly handle GraphQL errors from the backend; all users of federated metrics should upgrade to this version.) [PR #3056](https://github.com/apollographql/apollo-server/pull/3056) [Issue #3052](https://github.com/apollographql/apollo-server/issues/3052)
- `apollo-engine-reporting`: clean up `SIGINT` and `SIGTERM` handlers when `EngineReportingAgent` is stopped; fixes 'Possible EventEmitter memory leak detected' log. [PR #3090](https://github.com/apollographql/apollo-server/pull/3090)

### v2.7.1
Expand Down
60 changes: 20 additions & 40 deletions package-lock.json

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

33 changes: 15 additions & 18 deletions packages/apollo-engine-reporting/src/federatedExtension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphQLResolveInfo, GraphQLError } from 'graphql';
import { GraphQLExtension, EndHandler } from 'graphql-extensions';
import { GraphQLExtension } from 'graphql-extensions';
import { Trace } from 'apollo-engine-reporting-protobuf';
import { GraphQLRequestContext } from 'apollo-server-types';

Expand Down Expand Up @@ -53,30 +53,27 @@ export class EngineFederatedTracingExtension<TContext = any>
}
}

public executionDidStart(): EndHandler | void {
if (this.enabled) {
// It's a little odd that we record the end time after execution rather than
// at the end of the whole request, but because we need to include our
// formatted trace in the request itself, we have to record it before the
// request is over! It's also odd that we don't do traces for parse or
// validation errors, but runQuery doesn't currently support that, as
// format() is only invoked after execution.
return () => {
this.treeBuilder.stopTiming();
this.done = true;
};
}
}

// The ftv1 extension is a base64'd Trace protobuf containing only the
// durationNs, startTime, endTime, and root fields.
//
// Note: format() is only called after executing an operation, and
// specifically isn't called for parse or validation errors. Parse and validation
// errors in a federated backend will get reported to the end user as a downstream

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm... can we track this as an issue for followup at least? Seems like it would be useful to know if the query planner is sending sub-operations that fail validation :)

// error but will not get reported to Engine (because Engine filters out downstream
// errors)! See #3091.
public format(): [string, string] | undefined {
if (!this.enabled) {
return;
}
if (!this.done) {
throw Error('format called before end of execution?');
if (this.done) {
throw Error('format called twice?');
}

// We record the end time at the latest possible time: right before serializing the trace.
// If we wait any longer, the time we record won't actually be sent anywhere!
this.treeBuilder.stopTiming();
this.done = true;

const encodedUint8Array = Trace.encode(this.treeBuilder.trace).finish();
const encodedBuffer = Buffer.from(
encodedUint8Array,
Expand Down
24 changes: 16 additions & 8 deletions packages/apollo-engine-reporting/src/treeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
} from 'graphql';
import { Trace, google } from 'apollo-engine-reporting-protobuf';

function internalError(message: string) {
return new Error(`[internal apollo-server error] ${message}`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I do think apollo-engine-reporting is probably more appropriate, or internal apollo metrics reporting if we want to avoid the term engine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that's in the stack trace?

}

export class EngineReportingTreeBuilder {
private rootNode = new Trace.Node();
public trace = new Trace({ root: this.rootNode });
Expand All @@ -24,21 +28,21 @@ export class EngineReportingTreeBuilder {

public startTiming() {
if (this.startHrTime) {
throw Error('startTiming called twice!');
throw internalError('startTiming called twice!');
}
if (this.stopped) {
throw Error('startTiming called after stopTiming!');
throw internalError('startTiming called after stopTiming!');
}
this.trace.startTime = dateToProtoTimestamp(new Date());
this.startHrTime = process.hrtime();
}

public stopTiming() {
if (!this.startHrTime) {
throw Error('stopTiming called before startTiming!');
throw internalError('stopTiming called before startTiming!');
}
if (this.stopped) {
throw Error('stopTiming called twice!');
throw internalError('stopTiming called twice!');
}

this.trace.durationNs = durationHrTimeToNanos(
Expand All @@ -50,10 +54,10 @@ export class EngineReportingTreeBuilder {

public willResolveField(info: GraphQLResolveInfo): () => void {
if (!this.startHrTime) {
throw Error('willResolveField called before startTiming!');
throw internalError('willResolveField called before startTiming!');
}
if (this.stopped) {
throw Error('willResolveField called after stopTiming!');
throw internalError('willResolveField called after stopTiming!');
}

const path = info.path;
Expand All @@ -75,6 +79,10 @@ export class EngineReportingTreeBuilder {
errors.forEach(err => {
// This is an error from a federated service. We will already be reporting
// it in the nested Trace in the query plan.
//
// XXX This probably shouldn't skip query or validation errors, which are
// not in nested Traces because format() isn't called in this case! Or
// maybe format() should be called in that case?
if (err.extensions && err.extensions.serviceName) {
return;
}
Expand All @@ -101,10 +109,10 @@ export class EngineReportingTreeBuilder {
error: Trace.Error,
) {
if (!this.startHrTime) {
throw Error('addProtobufError called before startTiming!');
throw internalError('addProtobufError called before startTiming!');
}
if (this.stopped) {
throw Error('addProtobufError called after stopTiming!');
throw internalError('addProtobufError called after stopTiming!');
}

// By default, put errors on the root node.
Expand Down
Loading