diff --git a/CHANGELOG.md b/CHANGELOG.md index b1aee8a1900..b7e9cc29be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,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 appropriate changes within that release will be moved into the new section. -- _Nothing yet! Stay tuned!_ +- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) ### v2.13.0 diff --git a/package-lock.json b/package-lock.json index b69aa5ee382..0eee2493ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6025,7 +6025,7 @@ "version": "file:packages/apollo-tracing", "requires": { "apollo-server-env": "file:packages/apollo-server-env", - "graphql-extensions": "file:packages/graphql-extensions" + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base" } }, "apollo-utilities": { diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 724dd382c35..44835b757fa 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -71,6 +71,7 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; +import { plugin as pluginTracing } from "apollo-tracing"; import { Logger, SchemaHash } from "apollo-server-types"; import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent"; @@ -765,7 +766,11 @@ export class ApolloServerBase { // Internal plugins should be added to `pluginsToInit` here. // User's plugins, provided as an argument to this method, will be added // at the end of that list so they take precedence. - // A follow-up commit will actually introduce this. + + // If the user has enabled it explicitly, add our tracing lugin. + if (this.config.tracing) { + pluginsToInit.push(pluginTracing()) + } pluginsToInit.push(...plugins); this.plugins = pluginsToInit.map(plugin => { diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index eb287cb9516..88655a85a1d 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -27,7 +27,6 @@ import { CacheControlExtension, CacheControlExtensionOptions, } from 'apollo-cache-control'; -import { TracingExtension } from 'apollo-tracing'; import { ApolloError, fromGraphQLError, @@ -98,7 +97,6 @@ export interface GraphQLRequestPipelineConfig { dataSources?: () => DataSources; extensions?: Array<() => GraphQLExtension>; - tracing?: boolean; persistedQueries?: PersistedQueryOptions; cacheControl?: CacheControlExtensionOptions; @@ -651,10 +649,6 @@ export async function processGraphQLRequest( // objects. const extensions = config.extensions ? config.extensions.map(f => f()) : []; - if (config.tracing) { - extensions.push(new TracingExtension()); - } - if (config.cacheControl) { cacheControlExtension = new CacheControlExtension(config.cacheControl); extensions.push(cacheControlExtension); diff --git a/packages/apollo-tracing/README.md b/packages/apollo-tracing/README.md index ffaab016414..f23149a579a 100644 --- a/packages/apollo-tracing/README.md +++ b/packages/apollo-tracing/README.md @@ -12,14 +12,13 @@ This data can be consumed by [Apollo Graph Manager](https://www.apollographql.co Apollo Server includes built-in support for tracing from version 1.1.0 onwards. -The only code change required is to add `tracing: true` to the options passed to the Apollo Server middleware function for your framework of choice. For example, for Express: +The only code change required is to add `tracing: true` to the options passed to the `ApolloServer` constructor options for your integration of choice. For example, for [`apollo-server-express`](https://npm.im/apollo-server-express): ```javascript -app.use('/graphql', bodyParser.json(), graphqlExpress({ +const { ApolloServer } = require('apollo-server-express'); + +const server = new ApolloServer({ schema, - context: {}, tracing: true, -})); +}); ``` - -> If you are using `express-graphql`, we recommend you switch to Apollo Server. Both `express-graphql` and Apollo Server are based on the [`graphql-js`](https://github.com/graphql/graphql-js) reference implementation, and switching should only require changing a few lines of code. diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index 18dd20f770c..a27bd152107 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", - "graphql-extensions": "file:../graphql-extensions" + "apollo-server-plugin-base": "file:../apollo-server-plugin-base" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-tracing/src/index.ts b/packages/apollo-tracing/src/index.ts index 9c31c22ed54..f6ec8a06765 100644 --- a/packages/apollo-tracing/src/index.ts +++ b/packages/apollo-tracing/src/index.ts @@ -1,11 +1,11 @@ import { ResponsePath, responsePathAsArray, - GraphQLResolveInfo, GraphQLType, } from 'graphql'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; -import { GraphQLExtension } from 'graphql-extensions'; +const { PACKAGE_NAME } = require("../package.json").name; export interface TracingFormat { version: 1; @@ -33,94 +33,104 @@ interface ResolverCall { endOffset?: HighResolutionTime; } -export class TracingExtension - implements GraphQLExtension { - private startWallTime?: Date; - private endWallTime?: Date; - private startHrTime?: HighResolutionTime; - private duration?: HighResolutionTime; - - private resolverCalls: ResolverCall[] = []; - - public requestDidStart() { - this.startWallTime = new Date(); - this.startHrTime = process.hrtime(); - } - - public executionDidStart() { - // 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.duration = process.hrtime(this.startHrTime); - this.endWallTime = new Date(); - }; - } - - public willResolveField( - _source: any, - _args: { [argName: string]: any }, - _context: TContext, - info: GraphQLResolveInfo, - ) { - const resolverCall: ResolverCall = { - path: info.path, - fieldName: info.fieldName, - parentType: info.parentType, - returnType: info.returnType, - startOffset: process.hrtime(this.startHrTime), - }; +export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({ + requestDidStart() { + let startWallTime: Date | undefined; + let endWallTime: Date | undefined; + let startHrTime: HighResolutionTime | undefined; + let duration: HighResolutionTime | undefined; + const resolverCalls: ResolverCall[] = []; - this.resolverCalls.push(resolverCall); + startWallTime = new Date(); + startHrTime = process.hrtime(); - return () => { - resolverCall.endOffset = process.hrtime(this.startHrTime); - }; - } - - public format(): [string, TracingFormat] | undefined { - // In the event that we are called prior to the initialization of critical - // date metrics, we'll return undefined to signal that the extension did not - // format properly. Any undefined extension results are simply purged by - // the graphql-extensions module. - if ( - typeof this.startWallTime === 'undefined' || - typeof this.endWallTime === 'undefined' || - typeof this.duration === 'undefined' - ) { - return; - } - - return [ - 'tracing', - { - version: 1, - startTime: this.startWallTime.toISOString(), - endTime: this.endWallTime.toISOString(), - duration: durationHrTimeToNanos(this.duration), - execution: { - resolvers: this.resolverCalls.map(resolverCall => { - const startOffset = durationHrTimeToNanos(resolverCall.startOffset); - const duration = resolverCall.endOffset - ? durationHrTimeToNanos(resolverCall.endOffset) - startOffset - : 0; - return { - path: [...responsePathAsArray(resolverCall.path)], - parentType: resolverCall.parentType.toString(), - fieldName: resolverCall.fieldName, - returnType: resolverCall.returnType.toString(), - startOffset, - duration, - }; - }), + return { + executionDidStart: () => ({ + // 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! + + // Historically speaking: It's WAS odd that we don't do traces for parse + // or validation errors. Reason being: at the time that this was written + // (now a plugin but originally an extension)). That was the case + // because runQuery DIDN'T (again, at the time, when it was an + // extension) support that since format() was only invoked after + // execution. + executionDidEnd: () => { + duration = process.hrtime(startHrTime); + endWallTime = new Date(); + }, + + willResolveField({ info }) { + const resolverCall: ResolverCall = { + path: info.path, + fieldName: info.fieldName, + parentType: info.parentType, + returnType: info.returnType, + startOffset: process.hrtime(startHrTime), + }; + + resolverCalls.push(resolverCall); + + return () => { + resolverCall.endOffset = process.hrtime(startHrTime); + }; }, + }), + + willSendResponse({ response }) { + // In the event that we are called prior to the initialization of + // critical date metrics, we'll return undefined to signal that the + // extension did not format properly. Any undefined extension + // results are simply purged by the graphql-extensions module. + if ( + typeof startWallTime === 'undefined' || + typeof endWallTime === 'undefined' || + typeof duration === 'undefined' + ) { + return; + } + + const extensions = + response.extensions || (response.extensions = Object.create(null)); + + // Be defensive and make sure nothing else (other plugin, etc.) has + // already used the `tracing` property on `extensions`. + if (typeof extensions.tracing !== 'undefined') { + throw new Error(PACKAGE_NAME + ": Could not add `tracing` to " + + "`extensions` since `tracing` was unexpectedly already present."); + } + + // Set the extensions. + extensions.tracing = { + version: 1, + startTime: startWallTime.toISOString(), + endTime: endWallTime.toISOString(), + duration: durationHrTimeToNanos(duration), + execution: { + resolvers: resolverCalls.map(resolverCall => { + const startOffset = durationHrTimeToNanos( + resolverCall.startOffset, + ); + const duration = resolverCall.endOffset + ? durationHrTimeToNanos(resolverCall.endOffset) - startOffset + : 0; + return { + path: [...responsePathAsArray(resolverCall.path)], + parentType: resolverCall.parentType.toString(), + fieldName: resolverCall.fieldName, + returnType: resolverCall.returnType.toString(), + startOffset, + duration, + }; + }), + }, + }; }, - ]; - } -} + }; + }, +}) type HighResolutionTime = [number, number]; diff --git a/packages/apollo-tracing/tsconfig.json b/packages/apollo-tracing/tsconfig.json index 0de28001c29..29dff935854 100644 --- a/packages/apollo-tracing/tsconfig.json +++ b/packages/apollo-tracing/tsconfig.json @@ -7,6 +7,6 @@ "include": ["src/**/*"], "exclude": ["**/__tests__", "**/__mocks__"], "references": [ - { "path": "../graphql-extensions" }, + { "path": "../apollo-server-plugin-base" }, ] }