Skip to content

Add diagnostic channels #4429

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
101 changes: 101 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -5030,3 +5030,104 @@ const plugin = {
}
};
```

## Diagnostic Channels

Diagnostic Channels allows Hapi to report events to other modules. This is useful for APM vendors to collect data from Hapi for diagnostics purposes.
This not intended to be used in your own server or plugin, use [extension methods](#server.ext()) or [events](#server.events) instead.

See [the official documentation](https://nodejs.org/docs/latest/api/diagnostics_channel.html) for more information.

### `hapi.onServer`

This event is sent when a new server is created. The [`server`](#server) object is passed as the message.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onServer');

channel.subscribe((server) => {

// Do something with the server
console.log(server.version);
});
```

### `hapi.onRoute`

This event is sent when a new route is added to the server. The [`route`](#request.route) object is passed as the message.
Similar to `server.events.on('route', (route) => {})`.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onRoute');

channel.subscribe((route) => {

// Do something with the route
console.log(route.path);
});
```

### `hapi.onRequest`

This event is sent when a request is generated by the framework. The [`request`](#request) object is passed as the message.
Similar to `server.events.on('request', (request) => {})`.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onRequest');

channel.subscribe((request) => {

// Do something with the request
console.log(request.info.id);
});
```

### `hapi.onResponse`

This event is sent after the response is sent back to the client. The [`response`](#request.response) object is passed as the message.
Similar to `server.events.on('response', ({ response }) => {})`.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onResponse');

channel.subscribe((response) => {

// Do something with the response
console.log(response.statusCode);
});
```

### `hapi.onRequestLifecycle`

This event is sent after the response is matched to a route. The [`request`](#request) object is passed as the message.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onRequestLifecycle');

channel.subscribe((request) => {

// Do something with the request
console.log(request.route);
});
```

### `hapi.onError`

This event is sent when a request responded with a 500 status code. An object containing the [`request`](#request) object and the error is passed as the message.
Similar to `server.events.on({ name: 'request', channels: 'error' }, (request, { error }) => {})`.

```js
const DC = require('diagnostics_channel');
const channel = DC.channel('hapi.onError');

channel.subscribe(({ request, error }) => {

// Do something with the request or the error
console.error(error);
});
```
15 changes: 15 additions & 0 deletions lib/channels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const DC = require('diagnostics_channel');

exports.server = DC.channel('hapi.onServer');

exports.route = DC.channel('hapi.onRoute');

exports.response = DC.channel('hapi.onResponse');

exports.request = DC.channel('hapi.onRequest');

exports.requestLifecycle = DC.channel('hapi.onRequestLifecycle');

exports.error = DC.channel('hapi.onError');
3 changes: 3 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Podium = require('@hapi/podium');
const Statehood = require('@hapi/statehood');

const Auth = require('./auth');
const Channels = require('./channels');
const Compression = require('./compression');
const Config = require('./config');
const Cors = require('./cors');
Expand Down Expand Up @@ -510,6 +511,8 @@ exports = module.exports = internals.Core = class {

const request = Request.generate(this.root, req, res, options);

Channels.request.publish(request);

// Track socket request processing state

if (this.settings.operations.cleanStop &&
Expand Down
7 changes: 7 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Bounce = require('@hapi/bounce');
const Hoek = require('@hapi/hoek');
const Podium = require('@hapi/podium');

const Channels = require('./channels');
const Cors = require('./cors');
const Toolkit = require('./toolkit');
const Transmit = require('./transmit');
Expand Down Expand Up @@ -361,6 +362,8 @@ exports = module.exports = internals.Request = class {

async _lifecycle() {

Channels.requestLifecycle.publish(this);

for (const func of this._route._cycle) {
if (this._isReplied) {
return;
Expand Down Expand Up @@ -497,6 +500,8 @@ exports = module.exports = internals.Request = class {
if (this.response.statusCode === 500 &&
this.response._error) {

Channels.error.publish({ request: this, error: this.response._error });

const tags = this.response._error.isDeveloperError ? ['internal', 'implementation', 'error'] : ['internal', 'error'];
this._log(tags, this.response._error, 'error');
}
Expand All @@ -508,6 +513,8 @@ exports = module.exports = internals.Request = class {

this.info.completed = Date.now();

Channels.response.publish(this.response);

this._core.events.emit('response', this);

if (this._route._extensions.onPostResponse.nodes) {
Expand Down
6 changes: 6 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Hoek = require('@hapi/hoek');
const Shot = require('@hapi/shot');
const Teamwork = require('@hapi/teamwork');

const Channels = require('./channels');
const Config = require('./config');
const Core = require('./core');
const Cors = require('./cors');
Expand Down Expand Up @@ -83,6 +84,8 @@ internals.Server = class {
}

core.registerServer(this);

Channels.server.publish(this);
}

_clone(name) {
Expand Down Expand Up @@ -531,7 +534,10 @@ internals.Server = class {
route.params = record.params;
}

Channels.route.publish(route.public);

this.events.emit('route', route.public);

Cors.options(route.public, server);
}

Expand Down
197 changes: 197 additions & 0 deletions test/channels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
'use strict';

const DC = require('diagnostics_channel');

const Code = require('@hapi/code');
const Lab = require('@hapi/lab');
const Hoek = require('@hapi/hoek');

const Hapi = require('..');

const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;

describe('DiagnosticChannel', () => {

describe('hapi.onServer', () => {

const channel = DC.channel('hapi.onServer');

it('server should be exposed on creation through the channel hapi.onServer', async () => {

let server;

const exposedServer = await new Promise((resolve) => {

channel.subscribe(resolve);

server = Hapi.server();
});

expect(exposedServer).to.shallow.equal(server);
});
});

describe('hapi.onRoute', () => {

const channel = DC.channel('hapi.onRoute');

it('route should be exposed on creation through the channel hapi.onRoute', async () => {

const server = Hapi.server();

const exposedRoute = await new Promise((resolve) => {

channel.subscribe(resolve);

server.route({
method: 'GET',
path: '/',
options: { app: { x: 'o' } },
handler: () => 'ok'
});
});

expect(exposedRoute).to.be.an.object();
expect(exposedRoute.settings.app.x).to.equal('o');
});
});

describe('hapi.onResponse', () => {

const channel = DC.channel('hapi.onResponse');

it('response should be exposed on creation through the channel hapi.onResponse', async () => {

const server = Hapi.server();

server.route({ method: 'GET', path: '/', handler: () => 'ok' });

const event = new Promise((resolve) => {

channel.subscribe(resolve);
});

const response = await server.inject('/');
const responseExposed = await event;
expect(response.request.response).to.shallow.equal(responseExposed);
});
});

describe('hapi.onRequest', () => {

const channel = DC.channel('hapi.onRequest');

it('request should be exposed on creation through the channel hapi.onRequest', async () => {

const server = Hapi.server();

server.route({ method: 'GET', path: '/', handler: () => 'ok' });

const event = new Promise((resolve) => {

channel.subscribe(resolve);
});

const response = await server.inject('/');
const requestExposed = await event;
expect(response.request).to.shallow.equal(requestExposed);
});

it('request should not have been routed when hapi.onRequest is triggered', async () => {

const server = Hapi.server();

server.route({ method: 'GET', path: '/test/{p}', handler: () => 'ok' });

server.ext('onRequest', async (request, h) => {

await Hoek.wait(10);
return h.continue;
});

const event = new Promise((resolve) => {

channel.subscribe(resolve);
});

const request = server.inject('/test/foo');
const requestExposed = await event;
expect(requestExposed.params).to.be.null();

const response = await request;
expect(response.request).to.shallow.equal(requestExposed);
});
});

describe('hapi.onRequestLifecycle', () => {

const channel = DC.channel('hapi.onRequestLifecycle');

it('request should be exposed after routing through the channel hapi.onRequestLifecycle', async () => {

const server = Hapi.server();

server.route({
method: 'POST',
path: '/test/{p}',
options: { app: { x: 'o' } },
handler: () => 'ok'
});

server.ext('onPreAuth', async (request, h) => {

await Hoek.wait(10);
return h.continue;
});

const event = new Promise((resolve) => {

channel.subscribe(resolve);
});

const request = server.inject({ method: 'POST', url: '/test/foo', payload: '{"a":"b"}' });
const requestExposed = await event;
expect(requestExposed.params).to.be.an.object();
expect(requestExposed.params.p).to.equal('foo');
expect(requestExposed.route).to.be.an.object();
expect(requestExposed.route.settings.app.x).to.equal('o');
expect(requestExposed.payload).to.be.undefined();

const response = await request;
expect(response.request).to.shallow.equal(requestExposed);
expect(response.request.payload).to.be.an.object();
expect(response.request.payload.a).to.equal('b');
});
});

describe('hapi.onError', () => {

const channel = DC.channel('hapi.onError');

it('should expose the request as well as the error object when an error happens during the request lifetime', async () => {

const server = Hapi.server();

server.route({
method: 'GET',
path: '/',
handler: () => {

throw new Error('some error message');
}
});

const event = new Promise((resolve) => {

channel.subscribe(resolve);
});

const response = await server.inject('/');
const { request: requestExposed, error: errorExposed } = await event;
expect(response.request).to.shallow.equal(requestExposed);
expect(errorExposed).to.be.an.instanceof(Error);
expect(errorExposed.message).to.equal('some error message');
});
});
});