Skip to content

Commit 1a5eae4

Browse files
authored
feat(apollo): add hooks to Apollo Gateway (#7704)
* Introduce hooks for Apollo Gateway.
1 parent d698403 commit 1a5eae4

File tree

10 files changed

+241
-8
lines changed

10 files changed

+241
-8
lines changed

index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,22 @@ declare namespace tracer {
20342034
* @default true
20352035
*/
20362036
signature?: boolean;
2037+
2038+
/**
2039+
* An object of optional callbacks to be executed during the respective
2040+
* phase of an Apollo Gateway operation. Undefined callbacks default to a
2041+
* noop function.
2042+
*
2043+
* @default {}
2044+
*/
2045+
hooks?: {
2046+
request?: (span?: Span, ctx?: any) => void;
2047+
validate?: (span?: Span, ctx?: any) => void;
2048+
plan?: (span?: Span, ctx?: any) => void;
2049+
execute?: (span?: Span, ctx?: any) => void;
2050+
fetch?: (span?: Span, ctx?: any) => void;
2051+
postprocessing?: (span?: Span, ctx?: any) => void;
2052+
};
20372053
}
20382054

20392055
/**

packages/datadog-plugin-apollo/src/gateway/execute.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
55
class ApolloGatewayExecutePlugin extends ApolloBasePlugin {
66
static operation = 'execute'
77
static prefix = 'tracing:apm:apollo:gateway:execute'
8+
9+
onAsyncStart (ctx) {
10+
const span = ctx?.currentStore?.span
11+
12+
if (!span) return
13+
14+
this.config.hooks.execute(span, ctx)
15+
}
816
}
917

1018
module.exports = ApolloGatewayExecutePlugin

packages/datadog-plugin-apollo/src/gateway/fetch.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class ApolloGatewayFetchPlugin extends ApolloBasePlugin {
2929

3030
return ctx.currentStore
3131
}
32+
33+
onAsyncStart (ctx) {
34+
const span = ctx?.currentStore?.span
35+
this.config.hooks.fetch(span, ctx)
36+
}
3237
}
3338

3439
module.exports = ApolloGatewayFetchPlugin

packages/datadog-plugin-apollo/src/gateway/plan.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
55
class ApolloGatewayPlanPlugin extends ApolloBasePlugin {
66
static operation = 'plan'
77
static prefix = 'tracing:apm:apollo:gateway:plan'
8+
9+
onEnd (ctx) {
10+
const span = ctx?.currentStore?.span
11+
12+
if (!span) return
13+
14+
this.config.hooks.plan(span, ctx)
15+
}
816
}
917

1018
module.exports = ApolloGatewayPlanPlugin

packages/datadog-plugin-apollo/src/gateway/postprocessing.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo')
55
class ApolloGatewayPostProcessingPlugin extends ApolloBasePlugin {
66
static operation = 'postprocessing'
77
static prefix = 'tracing:apm:apollo:gateway:postprocessing'
8+
9+
onAsyncStart (ctx) {
10+
const span = ctx?.currentStore?.span
11+
this.config.hooks.postprocessing(span, ctx)
12+
}
813
}
914

1015
module.exports = ApolloGatewayPostProcessingPlugin

packages/datadog-plugin-apollo/src/gateway/request.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,16 @@ class ApolloGatewayRequestPlugin extends ApolloBasePlugin {
5252
return ctx.currentStore
5353
}
5454

55-
asyncStart (ctx) {
55+
onAsyncStart (ctx) {
5656
const errors = ctx?.result?.errors
5757
// apollo gateway catches certain errors and returns them in the result object
5858
// we want to capture these errors as spans
5959
if (Array.isArray(errors) && errors.at(-1)?.stack && errors.at(-1).message) {
6060
ctx.currentStore.span.setTag('error', errors.at(-1))
6161
}
62-
ctx.currentStore.span.finish()
63-
return ctx.parentStore
62+
63+
const span = ctx?.currentStore?.span
64+
this.config.hooks.request(span, ctx)
6465
}
6566
}
6667

packages/datadog-plugin-apollo/src/gateway/validate.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ class ApolloGatewayValidatePlugin extends ApolloBasePlugin {
66
static operation = 'validate'
77
static prefix = 'tracing:apm:apollo:gateway:validate'
88

9-
end (ctx) {
9+
onEnd (ctx) {
1010
const result = ctx.result
11-
const span = ctx.currentStore?.span
11+
const span = ctx?.currentStore?.span
1212

1313
if (!span) return
1414

1515
if (Array.isArray(result) && result.at(-1)?.stack && result.at(-1).message) {
1616
span.setTag('error', result.at(-1))
1717
}
18-
span.finish()
18+
19+
this.config.hooks.validate(span, ctx)
1920
}
2021
}
2122

packages/datadog-plugin-apollo/src/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@ class ApolloPlugin extends CompositePlugin {
1010
gateway: ApolloGatewayPlugin,
1111
}
1212
}
13+
14+
/**
15+
* @override
16+
*/
17+
configure (config) {
18+
return super.configure(validateConfig(config))
19+
}
20+
}
21+
22+
const noop = () => {}
23+
24+
function validateConfig (config) {
25+
return {
26+
...config,
27+
hooks: getHooks(config),
28+
}
29+
}
30+
31+
function getHooks (config) {
32+
const hooks = config?.hooks
33+
const request = hooks?.request ?? noop
34+
const validate = hooks?.validate ?? noop
35+
const plan = hooks?.plan ?? noop
36+
const execute = hooks?.execute ?? noop
37+
const fetch = hooks?.fetch ?? noop
38+
const postprocessing = hooks?.postprocessing ?? noop
39+
40+
return { request, validate, plan, execute, fetch, postprocessing }
1341
}
1442

1543
module.exports = ApolloPlugin

packages/datadog-plugin-apollo/test/index.spec.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const assert = require('node:assert/strict')
44

55
const axios = require('axios')
6+
const sinon = require('sinon')
67

78
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants.js')
89
const agent = require('../../dd-trace/test/plugins/agent.js')
@@ -552,6 +553,161 @@ describe('Plugin', () => {
552553
})
553554
})
554555
})
556+
557+
describe('with hooks configuration', () => {
558+
const config = {
559+
hooks: {
560+
request: sinon.spy((span, ctx) => {}),
561+
validate: sinon.spy((span, ctx) => {}),
562+
plan: sinon.spy((span, ctx) => {}),
563+
execute: sinon.spy((span, ctx) => {}),
564+
fetch: sinon.spy((span, ctx) => {}),
565+
postprocessing: sinon.spy((span, ctx) => {}),
566+
},
567+
}
568+
569+
before(() => agent.load('apollo', config))
570+
before(() => setupFixtures())
571+
before(() => setupApollo(version))
572+
573+
afterEach(() => Object.keys(config.hooks).forEach(
574+
key => config.hooks[key].resetHistory()
575+
))
576+
577+
it('should run request, plan, execute and postprocessing hooks before spans are finished', done => {
578+
const operationName = 'MyQuery'
579+
const source = `query ${operationName} { hello(name: "world") }`
580+
const variableValues = { who: 'world' }
581+
582+
agent
583+
.assertSomeTraces((traces) => {
584+
sinon.assert.calledOnce(config.hooks.request)
585+
sinon.assert.calledOnce(config.hooks.plan)
586+
sinon.assert.calledOnce(config.hooks.execute)
587+
sinon.assert.calledOnce(config.hooks.postprocessing)
588+
589+
const requestSpan = config.hooks.request.firstCall.args[0]
590+
const requestCtx = config.hooks.request.firstCall.args[1]
591+
const planSpan = config.hooks.plan.firstCall.args[0]
592+
const executeSpan = config.hooks.execute.firstCall.args[0]
593+
const postprocessingSpan = config.hooks.postprocessing.firstCall.args[0]
594+
595+
assert.strictEqual(requestSpan.context()._name, expectedSchema.server.opName)
596+
assert.strictEqual(planSpan.context()._name, 'apollo.gateway.plan')
597+
assert.strictEqual(executeSpan.context()._name, 'apollo.gateway.execute')
598+
assert.strictEqual(postprocessingSpan.context()._name, 'apollo.gateway.postprocessing')
599+
assert.strictEqual(requestCtx.requestContext.operationName, operationName)
600+
})
601+
.then(done)
602+
.catch(done)
603+
604+
gateway()
605+
.then(({ executor }) => {
606+
return execute(executor, source, variableValues, operationName).then(() => {})
607+
})
608+
})
609+
610+
it('should run validate hook on validation failure and keep error tagging', done => {
611+
let error
612+
const source = `#graphql
613+
query InvalidVariables($first: Int!, $second: Int!) {
614+
topReviews(first: $first) {
615+
body
616+
}
617+
}`
618+
const variableValues = { who: 'world' }
619+
620+
agent
621+
.assertSomeTraces((traces) => {
622+
sinon.assert.calledOnce(config.hooks.request)
623+
sinon.assert.calledOnce(config.hooks.validate)
624+
sinon.assert.notCalled(config.hooks.plan)
625+
sinon.assert.notCalled(config.hooks.execute)
626+
sinon.assert.notCalled(config.hooks.fetch)
627+
sinon.assert.notCalled(config.hooks.postprocessing)
628+
629+
const validateSpan = config.hooks.validate.firstCall.args[0]
630+
const validateCtx = config.hooks.validate.firstCall.args[1]
631+
632+
assert.strictEqual(validateSpan.context()._name, 'apollo.gateway.validate')
633+
assert.ok(Array.isArray(validateCtx.result))
634+
assert.strictEqual(validateCtx.result.at(-1).message, error.message)
635+
636+
assertObjectContains(traces[0][1], {
637+
name: 'apollo.gateway.validate',
638+
error: 1,
639+
meta: {
640+
[ERROR_TYPE]: error.name,
641+
[ERROR_MESSAGE]: error.message,
642+
[ERROR_STACK]: error.stack,
643+
},
644+
})
645+
})
646+
.then(done)
647+
.catch(done)
648+
649+
gateway()
650+
.then(({ executor }) => {
651+
return execute(executor, source, variableValues, 'InvalidVariables').then((result) => {
652+
error = result.errors[1]
653+
})
654+
})
655+
})
656+
657+
it('should run request and fetch hooks on fetch failure', done => {
658+
let error
659+
const operationName = 'MyQuery'
660+
const source = `query ${operationName} { hello(name: "world") }`
661+
const variableValues = { who: 'world' }
662+
663+
agent
664+
.assertSomeTraces((traces) => {
665+
sinon.assert.calledOnce(config.hooks.request)
666+
sinon.assert.calledOnce(config.hooks.fetch)
667+
668+
const requestSpan = config.hooks.request.firstCall.args[0]
669+
const requestCtx = config.hooks.request.firstCall.args[1]
670+
const fetchSpan = config.hooks.fetch.firstCall.args[0]
671+
const fetchCtx = config.hooks.fetch.firstCall.args[1]
672+
673+
assert.strictEqual(requestSpan.context()._name, expectedSchema.server.opName)
674+
assert.strictEqual(fetchSpan.context()._name, 'apollo.gateway.fetch')
675+
assert.ok(requestCtx?.result?.errors?.length)
676+
assert.ok(fetchCtx?.error)
677+
678+
assertObjectContains(traces[0][0], {
679+
name: expectedSchema.server.opName,
680+
error: 1,
681+
})
682+
683+
assertObjectContains(traces[0][4], {
684+
name: 'apollo.gateway.fetch',
685+
error: 1,
686+
meta: {
687+
[ERROR_TYPE]: error.name,
688+
[ERROR_MESSAGE]: error.message,
689+
[ERROR_STACK]: error.stack,
690+
},
691+
})
692+
})
693+
.then(done)
694+
.catch(done)
695+
696+
const gateway = new ApolloGateway({
697+
localServiceList: fixtures,
698+
fetcher: () => {
699+
throw Error('Nooo')
700+
},
701+
})
702+
gateway.load().then(resp => {
703+
return execute(resp.executor, source, variableValues, operationName)
704+
.then((result) => {
705+
const errors = result.errors
706+
error = errors[errors.length - 1]
707+
})
708+
})
709+
})
710+
})
555711
})
556712
})
557713
})

packages/dd-trace/src/plugins/apollo.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@ class ApolloBasePlugin extends TracingPlugin {
2727
}
2828

2929
end (ctx) {
30-
// Only synchronous operations would have `result` or `error` on `end`.
3130
if (!ctx.hasOwnProperty('result') && !ctx.hasOwnProperty('error')) return
31+
this.onEnd(ctx)
3232
ctx?.currentStore?.span?.finish()
3333
}
3434

3535
asyncStart (ctx) {
36-
ctx?.currentStore?.span.finish()
36+
this.onAsyncStart(ctx)
37+
ctx?.currentStore?.span?.finish()
3738
return ctx.parentStore
3839
}
3940

41+
onEnd (ctx) {}
42+
43+
onAsyncStart (ctx) {}
44+
4045
getServiceName () {
4146
return this.serviceName({
4247
id: `${this.constructor.id}.${this.constructor.operation}`,

0 commit comments

Comments
 (0)