diff --git a/docs/Hooks.md b/docs/Hooks.md index 38227bdbfe..58359d5b9e 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -564,3 +564,45 @@ fastify.route({ ``` **Note**: both options also accept an array of functions. + +## Diagnostics Channel Hooks + +> **Note:** The `diagnostics_channel` is currently experimental on Node.js, so +> its API is subject to change even in semver-patch releases of Node.js. For +> versions of Node.js supported by Fastify where `diagnostics_channel` is +> unavailable, the hook will use the +> [polyfill](https://www.npmjs.com/package/diagnostics_channel) if it is +> available. Otherwise this feature will not be present. + +Currently, one +[`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) publish +event, `'fastify.initialization'`, happens at initialization time. The Fastify +instance is passed into the hook as a property of the object passed in. At this +point, the instance can be interacted with to add hooks, plugins, routes or any +other sort of modification. + +For example, a tracing package might do something like the following (which is, +of course, a simplification). This would be in a file loaded in the +initialization of the tracking package, in the typical "require instrumentation +tools first" fashion. + +```js +const tracer = /* retrieved from elsehwere in the package */ +const dc = require('diagnostics_channel') +const channel = dc.channel('fastify.initialization') +const spans = new WeakMap() + +channel.subscribe(function ({ fastify }) { + fastify.addHook('onRequest', (request, reply, done) => { + const span = tracer.startSpan('fastify.request') + spans.set(request, span) + done() + }) + + fastify.addHook('onResponse', (request, reply, done) => { + const span = spans.get(request) + span.finish() + done() + }) +}) +``` diff --git a/fastify.js b/fastify.js index f1fae99cac..22a9dadaa5 100644 --- a/fastify.js +++ b/fastify.js @@ -399,6 +399,17 @@ function fastify (options) { // Delay configuring clientError handler so that it can access fastify state. server.on('clientError', options.clientErrorHandler.bind(fastify)) + try { + const dc = require('diagnostics_channel') + const initChannel = dc.channel('fastify.initialization') + if (initChannel.hasSubscribers) { + initChannel.publish({ fastify }) + } + } catch (e) { + // This only happens if `diagnostics_channel` isn't available, i.e. earlier + // versions of Node.js. In that event, we don't care, so ignore the error. + } + return fastify function throwIfAlreadyStarted (msg) { diff --git a/test/diagnostics-channel.test.js b/test/diagnostics-channel.test.js new file mode 100644 index 0000000000..9b8e301cea --- /dev/null +++ b/test/diagnostics-channel.test.js @@ -0,0 +1,61 @@ +'use strict' + +const t = require('tap') +const test = t.test +const proxyquire = require('proxyquire') + +test('diagnostics_channel when present and subscribers', t => { + t.plan(3) + + let fastifyInHook + + const dc = { + channel (name) { + t.equal(name, 'fastify.initialization') + return { + hasSubscribers: true, + publish (event) { + t.ok(event.fastify) + fastifyInHook = event.fastify + } + } + }, + '@noCallThru': true + } + + const fastify = proxyquire('../fastify', { + diagnostics_channel: dc + })() + t.equal(fastifyInHook, fastify) +}) + +test('diagnostics_channel when present and no subscribers', t => { + t.plan(1) + + const dc = { + channel (name) { + t.equal(name, 'fastify.initialization') + return { + hasSubscribers: false, + publish () { + t.fail('publish should not be called') + } + } + }, + '@noCallThru': true + } + + proxyquire('../fastify', { + diagnostics_channel: dc + })() +}) + +test('diagnostics_channel when not present', t => { + t.plan(1) + + t.doesNotThrow(() => { + proxyquire('../fastify', { + diagnostics_channel: null + })() + }) +})