Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: opentelemetry integration #346

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions docs/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

- [@nexus/schema](/docs/integrations/nexus-schema.md) - Declarative, code-first and strongly typed GraphQL schema construction for TypeScript & JavaScript
- [mercurius-integration-testing](/docs/integrations/mercurius-integration-testing.md) - Utility library for writing mercurius integration tests.
- [@opentelemetry](/docs/integrations/open-telemetry) - A framework for collecting traces and metrics from applications.
141 changes: 141 additions & 0 deletions docs/integrations/open-telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# mercurius

## OpenTelemetry (Tracing)

Mercurius is compatible with open-telemetry


## Example

Here is a simple exemple on how to enable tracing on Mercurius with OpenTelemetry:

tracer.js
```js
'use strict'

const api = require('@opentelemetry/api')
const { NodeTracerProvider } = require('@opentelemetry/node')
const { SimpleSpanProcessor } = require('@opentelemetry/tracing')
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger')
const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql')
const { HttpTraceContext } = require('@opentelemetry/core')
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
// or
// const { JaegerExporter } = require('@opentelemetry/exporter-jaeger')

module.exports = serviceName => {
const provider = new NodeTracerProvider()
const graphQLInstrumentation = new GraphQLInstrumentation()
graphQLInstrumentation.setTracerProvider(provider)
graphQLInstrumentation.enable()

api.propagation.setGlobalPropagator(new HttpTraceContext())
api.trace.setGlobalTracerProvider(provider)

provider.addSpanProcessor(
new SimpleSpanProcessor(
new ZipkinExporter({
serviceName
})
// or
// new JaegerExporter({
// serviceName,
// })
)
)
provider.register()
return provider
}
```

serviceAdd.js
```js
'use strict'
// Register tracer
const serviceName = 'service-add'
const tracer = require('./tracer')
tracer(serviceName)

const service = require('fastify')({ logger: { level: 'debug' } })
const mercurius = require('mercurius')
const opentelemetry = require('@autotelic/fastify-opentelemetry')

service.register(opentelemetry, { serviceName })
service.register(mercurius, {
schema: `
extend type Query {
add(x: Float, y: Float): Float
}
`,
resolvers: {
Query: {
add: (_, { x, y }, { reply }) => {
const { activeSpan, tracer } = reply.request.openTelemetry()

activeSpan.setAttribute('arg.x', x)
activeSpan.setAttribute('arg.y', y)

const span = tracer.startSpan('compute-add', { parent: tracer.getCurrentSpan() })
const result = x + y
span.end()

return result
}
}
},
federationMetadata: true
})

service.listen(4001, 'localhost', err => {
if (err) {
console.error(err)
process.exit(1)
}
})
```

gateway.js
```js
'use strict'
const serviceName = 'gateway'
const tracer = require('./tracer')
// Register tracer
tracer(serviceName)

const gateway = require('fastify')({ logger: { level: 'debug' } })
const mercurius = require('mercurius')
const opentelemetry = require('@autotelic/fastify-opentelemetry')

// Register fastify opentelemetry
gateway.register(opentelemetry, { serviceName })
gateway.register(mercurius, {
gateway: {
services: [
{
name: 'add',
url: 'http://localhost:4001/graphql'
}
]
}
})

gateway.listen(3000, 'localhost', err => {
if (err) {
process.exit(1)
}
})
```

Start a zipkin service:

```
$ docker run -d -p 9411:9411 openzipkin/zipkin
```

Send some request to the gateway:

```bash
$ curl localhost:3000/graphql -H 'Content-Type: application/json' --data '{"query":"{ add(x: 1, y: 2) }"}'
```

You can now browse through mercurius tracing at `http://localhost:9411`
1 change: 1 addition & 0 deletions docsify/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [Integrations](/docs/integrations/)
* [@nexus/schema](/docs/integrations/nexus-schema)
* [Testing](/docs/integrations/mercurius-integration-testing)
* [Tracing - OpenTelemetry](/docs/integrations/open-telemetry)
* [Related plugins](/docs/plugins)
* [mercurius-upload](/docs/plugins#mercurius-upload)
* [altair-fastify-plugin](/docs/plugins#altair-fastify-plugin)
Expand Down
19 changes: 16 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const {
MER_ERR_INVALID_OPTS,
MER_ERR_METHOD_NOT_ALLOWED
} = require('./lib/errors')
const tracer = require('./lib/tracer')

const kLoaders = Symbol('mercurius.loaders')
const kFactory = Symbol('mercurius.loadersFactory')
Expand Down Expand Up @@ -373,12 +374,15 @@ 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
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
Expand All @@ -388,6 +392,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
}

Expand All @@ -396,6 +401,7 @@ const plugin = fp(async function (app, opts) {
} catch (syntaxError) {
const err = new MER_ERR_GQL_VALIDATION()
err.errors = [syntaxError]
span.end()
throw err
}

Expand All @@ -416,6 +422,7 @@ const plugin = fp(async function (app, opts) {
}
const err = new MER_ERR_GQL_VALIDATION()
err.errors = validationErrors
span.end()
throw err
}

Expand All @@ -425,6 +432,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
}
}
Expand All @@ -442,6 +450,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
}
}
Expand All @@ -453,8 +462,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
Expand All @@ -463,6 +473,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
}
}
Expand All @@ -476,7 +487,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) {
Expand Down
5 changes: 5 additions & 0 deletions lib/tracer.js
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.11.0",
"@sinonjs/fake-timers": "^6.0.1",
"@types/node": "^14.0.23",
"@types/ws": "^7.2.6",
Expand Down