From 9cb48c74eea5d428de82deda24cbe5252d853604 Mon Sep 17 00:00:00 2001 From: Paco Dupont Date: Mon, 14 Dec 2020 10:53:56 +0100 Subject: [PATCH 1/4] feat(opentelemetry): implement basic tracer, span and cached attribute --- index.js | 21 +++++++++++++++++++-- lib/tracer.js | 5 +++++ package.json | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 lib/tracer.js diff --git a/index.js b/index.js index e646a1c5..d1553e51 100644 --- a/index.js +++ b/index.js @@ -38,6 +38,7 @@ const { MER_ERR_METHOD_NOT_ALLOWED, MER_ERR_INVALID_METHOD } = require('./lib/errors') +const tracer = require('./lib/tracer') const kLoaders = Symbol('mercurius.loaders') const kFactory = Symbol('mercurius.loadersFactory') @@ -382,12 +383,18 @@ const plugin = fp(async function (app, opts) { } async function fastifyGraphQl (source, context, variables, operationName) { + const span = tracer.startSpan('mercurius - graphql', { parent: tracer.getCurrentSpan() }) + context = Object.assign({ app: this, lruGatewayResolvers }, context) const reply = context.reply // Parse, with a little lru const cached = lru !== null && lru.get(source) let document = null + + // Add opentelemetry attributes + span.setAttribute('mercurius.cached', cached) + if (!cached) { // We use two caches to avoid errors bust the good // cache. This is a protection against DoS attacks @@ -397,6 +404,7 @@ const plugin = fp(async function (app, opts) { // this query errored const err = new MER_ERR_GQL_VALIDATION() err.errors = cachedError.validationErrors + span.end() throw err } @@ -405,6 +413,7 @@ const plugin = fp(async function (app, opts) { } catch (syntaxError) { const err = new MER_ERR_GQL_VALIDATION() err.errors = [syntaxError] + span.end() throw err } @@ -425,6 +434,7 @@ const plugin = fp(async function (app, opts) { } const err = new MER_ERR_GQL_VALIDATION() err.errors = validationErrors + span.end() throw err } @@ -434,6 +444,7 @@ const plugin = fp(async function (app, opts) { if (queryDepthErrors.length > 0) { const err = new MER_ERR_GQL_VALIDATION() err.errors = queryDepthErrors + span.end() throw err } } @@ -451,6 +462,7 @@ const plugin = fp(async function (app, opts) { if (operationAST.operation !== 'query') { const err = new MER_ERR_METHOD_NOT_ALLOWED() err.errors = [new Error('Operation cannot be performed via a GET request')] + span.end() throw err } } @@ -463,7 +475,9 @@ const plugin = fp(async function (app, opts) { if (cached && cached.jit !== null) { const execution = await cached.jit.query(root, context, variables || {}) - return maybeFormatErrors(execution, context) + const resWithFormatedErrors = maybeFormatErrors(execution, context) + span.end() + return resWithFormatedErrors } // Validate variables @@ -472,6 +486,7 @@ const plugin = fp(async function (app, opts) { if (Array.isArray(executionContext)) { const err = new MER_ERR_GQL_VALIDATION() err.errors = executionContext + span.end() throw err } } @@ -485,7 +500,9 @@ const plugin = fp(async function (app, opts) { operationName ) - return maybeFormatErrors(execution, context) + const resWithFormatedErrors = maybeFormatErrors(execution, context) + span.end() + return resWithFormatedErrors } function maybeFormatErrors (execution, context) { diff --git a/lib/tracer.js b/lib/tracer.js new file mode 100644 index 00000000..d7b7645e --- /dev/null +++ b/lib/tracer.js @@ -0,0 +1,5 @@ +'use strict' +const api = require('@opentelemetry/api') +const meta = require('../package.json') + +module.exports = api.trace.getTracer(meta.name, meta.version) diff --git a/package.json b/package.json index 82fb1e10..afd0c984 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@graphql-tools/merge": "^6.2.4", "@graphql-tools/schema": "^6.2.4", "@graphql-tools/utils": "^6.2.4", + "@opentelemetry/api": "^0.13.0", "@sinonjs/fake-timers": "^6.0.1", "@types/node": "^14.0.23", "@types/ws": "^7.2.6", From 238b680f46d03256aa66796cd7eb76a8cd23412b Mon Sep 17 00:00:00 2001 From: Paco Dupont Date: Mon, 14 Dec 2020 11:28:59 +0100 Subject: [PATCH 2/4] test(opentelemetry): should add span and cached attribute --- index.js | 2 +- test/opentelemetry.js | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 test/opentelemetry.js diff --git a/index.js b/index.js index d1553e51..e5e1ce83 100644 --- a/index.js +++ b/index.js @@ -393,7 +393,7 @@ const plugin = fp(async function (app, opts) { let document = null // Add opentelemetry attributes - span.setAttribute('mercurius.cached', cached) + span.setAttribute('mercurius.cached', !!cached) if (!cached) { // We use two caches to avoid errors bust the good diff --git a/test/opentelemetry.js b/test/opentelemetry.js new file mode 100644 index 00000000..4ea7e203 --- /dev/null +++ b/test/opentelemetry.js @@ -0,0 +1,65 @@ +'use strict' +const { test } = require('tap') +const Fastify = require('fastify') +const api = require('@opentelemetry/api') +const GQL = require('..') + +test('Should add opentelemetry span and cached attribute', async (t) => { + t.plan(6) + + class TestSpan extends api.NoopSpan { + setAttribute (name, value) { + t.is(name, 'mercurius.cached') + t.is(value, false) + } + + end () { + t.pass() + } + } + class TestTracer extends api.NoopTracer { + startSpan (name, opts) { + t.is(name, 'mercurius - graphql') + t.deepEquals(opts, { parent: new api.NoopSpan() }) + return new TestSpan() + } + } + class TestTracerProvider extends api.NoopTracerProvider { + getTracer () { + return new TestTracer() + } + } + api.trace.setGlobalTracerProvider(new TestTracerProvider()) + + const app = Fastify() + const schema = ` + type Query { + add(x: Int, y: Int): Int + } + ` + + const resolvers = { + add: async ({ x, y }) => x + y + } + + app.register(GQL, { + schema, + resolvers + }) + + app.get('/', async function (req, reply) { + const query = '{ add(x: 2, y: 2) }' + return reply.graphql(query) + }) + + const res = await app.inject({ + method: 'GET', + url: '/' + }) + + t.deepEqual(JSON.parse(res.body), { + data: { + add: 4 + } + }) +}) From f52a850706840470a20c90e9d96290a8793c279b Mon Sep 17 00:00:00 2001 From: Paco Dupont Date: Mon, 14 Dec 2020 11:37:11 +0100 Subject: [PATCH 3/4] test(opentelemetry): refine span context testing --- test/opentelemetry.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/opentelemetry.js b/test/opentelemetry.js index 4ea7e203..9b349c53 100644 --- a/test/opentelemetry.js +++ b/test/opentelemetry.js @@ -7,6 +7,11 @@ const GQL = require('..') test('Should add opentelemetry span and cached attribute', async (t) => { t.plan(6) + const testSpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: api.TraceFlags.NONE + } class TestSpan extends api.NoopSpan { setAttribute (name, value) { t.is(name, 'mercurius.cached') @@ -17,11 +22,16 @@ test('Should add opentelemetry span and cached attribute', async (t) => { t.pass() } } + const dummySpan = new TestSpan(testSpanContext) class TestTracer extends api.NoopTracer { startSpan (name, opts) { t.is(name, 'mercurius - graphql') - t.deepEquals(opts, { parent: new api.NoopSpan() }) - return new TestSpan() + t.deepEquals(opts.parent.context(), dummySpan.context()) + return new TestSpan(testSpanContext) + } + + getCurrentSpan () { + return dummySpan } } class TestTracerProvider extends api.NoopTracerProvider { From ac6a9e938e7c557744fd9552e9cd6c1bbfc600d8 Mon Sep 17 00:00:00 2001 From: Paco Dupont Date: Mon, 14 Dec 2020 11:39:16 +0100 Subject: [PATCH 4/4] test(opentelemetry): remove unused span context from TestTracer --- test/opentelemetry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/opentelemetry.js b/test/opentelemetry.js index 9b349c53..0b76aef0 100644 --- a/test/opentelemetry.js +++ b/test/opentelemetry.js @@ -27,7 +27,7 @@ test('Should add opentelemetry span and cached attribute', async (t) => { startSpan (name, opts) { t.is(name, 'mercurius - graphql') t.deepEquals(opts.parent.context(), dummySpan.context()) - return new TestSpan(testSpanContext) + return new TestSpan() } getCurrentSpan () {