Skip to content

Commit

Permalink
feat: add hapi 17 tracing support
Browse files Browse the repository at this point in the history
  • Loading branch information
kjin committed Apr 2, 2018
1 parent 7d46ab6 commit 092bb6b
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 105 deletions.
190 changes: 112 additions & 78 deletions src/plugins/plugin-hapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import {parse as urlParse} from 'url';

import {PluginTypes} from '..';

import {hapi_16} from './types';
import {hapi_16, hapi_17} from './types';

type Hapi16Module = typeof hapi_16;

const SUPPORTED_VERSIONS = '8 - 16';
type Hapi17Module = typeof hapi_17;

function getFirstHeader(req: IncomingMessage, key: string): string|null {
let headerValue = req.headers[key] || null;
Expand All @@ -34,87 +33,122 @@ function getFirstHeader(req: IncomingMessage, key: string): string|null {
return headerValue;
}

function createMiddleware(api: PluginTypes.TraceAgent):
hapi_16.ServerExtRequestHandler {
return function middleware(request, reply) {
const req = request.raw.req;
const res = request.raw.res;
const originalEnd = res.end;
const options: PluginTypes.RootSpanOptions = {
name: req.url ? (urlParse(req.url).pathname || '') : '',
url: req.url,
traceContext:
getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME),
skipFrames: 3
};
api.runInRootSpan(options, (root) => {
// Set response trace context.
const responseTraceContext = api.getResponseTraceContext(
options.traceContext || null, api.isRealSpan(root));
if (responseTraceContext) {
res.setHeader(
api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext);
}

if (!api.isRealSpan(root)) {
return reply.continue();
function instrument<T>(api: PluginTypes.TraceAgent, request: hapi_16.Request | hapi_17.Request, continueCb: () => T): T {
const req = request.raw.req;
const res = request.raw.res;
const originalEnd = res.end;
const options: PluginTypes.RootSpanOptions = {
name: req.url ? (urlParse(req.url).pathname || '') : '',
url: req.url,
traceContext:
getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME),
skipFrames: 3
};
return api.runInRootSpan(options, (root) => {
// Set response trace context.
const responseTraceContext = api.getResponseTraceContext(
options.traceContext || null, api.isRealSpan(root));
if (responseTraceContext) {
res.setHeader(
api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext);
}

if (!api.isRealSpan(root)) {
return continueCb();
}

api.wrapEmitter(req);
api.wrapEmitter(res);

const url = `${req.headers['X-Forwarded-Proto'] || 'http'}://${
req.headers.host}${req.url}`;

// we use the path part of the url as the span name and add the full
// url as a label
// req.path would be more desirable but is not set at the time our
// middleware runs.
root.addLabel(api.labels.HTTP_METHOD_LABEL_KEY, req.method);
root.addLabel(api.labels.HTTP_URL_LABEL_KEY, url);
root.addLabel(api.labels.HTTP_SOURCE_IP, req.connection.remoteAddress);

// wrap end
res.end = function(this: ServerResponse) {
res.end = originalEnd;
const returned = res.end.apply(this, arguments);

if (request.route && request.route.path) {
root.addLabel('hapi/request.route.path', request.route.path);
}
root.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode);
root.endSpan();

api.wrapEmitter(req);
api.wrapEmitter(res);

const url = `${req.headers['X-Forwarded-Proto'] || 'http'}://${
req.headers.host}${req.url}`;

// we use the path part of the url as the span name and add the full
// url as a label
// req.path would be more desirable but is not set at the time our
// middleware runs.
root.addLabel(api.labels.HTTP_METHOD_LABEL_KEY, req.method);
root.addLabel(api.labels.HTTP_URL_LABEL_KEY, url);
root.addLabel(api.labels.HTTP_SOURCE_IP, req.connection.remoteAddress);

// wrap end
res.end = function(this: ServerResponse) {
res.end = originalEnd;
const returned = res.end.apply(this, arguments);
return returned;
};

if (request.route && request.route.path) {
root.addLabel('hapi/request.route.path', request.route.path);
}
root.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode);
root.endSpan();
// if the event is aborted, end the span (as res.end will not be called)
req.once('aborted', () => {
root.addLabel(api.labels.ERROR_DETAILS_NAME, 'aborted');
root.addLabel(
api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request');
root.endSpan();
});

return returned;
};
return continueCb();
});
}

// if the event is aborted, end the span (as res.end will not be called)
req.once('aborted', () => {
root.addLabel(api.labels.ERROR_DETAILS_NAME, 'aborted');
root.addLabel(
api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request');
root.endSpan();
const plugin: PluginTypes.Plugin = [
{
versions: '8 - 16',
patch: function(hapi, api) {
function createMiddleware(): hapi_16.ServerExtRequestHandler {
return function middleware(request, reply) {
return instrument(api, request, () => reply.continue());
};
}

shimmer.wrap(hapi.Server.prototype, 'connection', (connection) => {
return function connectionTrace(this: hapi_16.Server) {
const server = connection.apply(this, arguments);
server.ext('onRequest', createMiddleware());
return server;
};
});
},
unpatch: function(hapi) {
shimmer.unwrap(hapi.Server.prototype, 'connection');
}
} as PluginTypes.Patch<Hapi16Module>, {
versions: '17',
patch: (hapi, api) => {
function createMiddleware(): hapi_17.Lifecycle.Method {
return function middleware(request, h) {
return instrument(api, request, () => h.continue);
};
}

return reply.continue();
});
};
}

const plugin: PluginTypes.Plugin = [{
file: '',
versions: SUPPORTED_VERSIONS,
patch: (hapi, api) => {
shimmer.wrap(hapi.Server.prototype, 'connection', (connection) => {
return function connectionTrace(this: {}) {
const server: hapi_16.Server = connection.apply(this, arguments);
server.ext('onRequest', createMiddleware(api));
return server;
type Hapi17ServerConstructor = { new (): hapi_17.Server };

const constructorWrap = <T>(hapiServer: T & Hapi17ServerConstructor): T & Hapi17ServerConstructor => {
return Object.assign(function connectionTrace(this: hapi_17.Server) {
hapiServer.apply(this, arguments);
const server: hapi_17.Server = this;
server.ext('onRequest', createMiddleware());
return server;
}, hapiServer);
};
});
},
unpatch: (hapi) => {
shimmer.unwrap(hapi.Server.prototype, 'connection');
}
} as PluginTypes.Patch<Hapi16Module>];

shimmer.wrap(hapi, 'Server', constructorWrap);
// Untyped hapi.server is an alias for hapi.Server.
if ((hapi as any).server) {
shimmer.wrap<Hapi17Module, 'Server'>(hapi, 'server' as any, constructorWrap);
}
},
unpatch: (hapi) => {
shimmer.unwrap(hapi, 'Server');
if ((hapi as any).server) {
shimmer.unwrap<Hapi17Module>(hapi, 'server' as any);
}
}
} as PluginTypes.Patch<Hapi17Module>];
export = plugin;
2 changes: 2 additions & 0 deletions src/plugins/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import * as connect_3 from './connect_3'; // connect@3
import * as express_4 from './express_4'; // express@4
import * as hapi_16 from './hapi_16'; // hapi@16
import * as hapi_17 from './hapi_17'; // hapi@17
import * as koa_2 from './koa_2'; // koa@2
import * as pg_7 from './pg_7'; // pg@7
import * as restify_5 from './restify_5'; // restify@5
Expand Down Expand Up @@ -88,6 +89,7 @@ export {
connect_3,
express_4,
hapi_16,
hapi_17,
koa_1,
koa_2,
pg_6,
Expand Down
28 changes: 4 additions & 24 deletions test/fixtures/plugin-fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,11 @@
"hapi-plugin-mysql": "^3.1.3"
}
},
"hapi10": {
"dependencies": {
"hapi": "^10.5.0"
}
},
"hapi11": {
"dependencies": {
"hapi": "^11.1.4"
}
},
"hapi12": {
"dependencies": {
"hapi": "^12.1.0"
}
},
"hapi13": {
"dependencies": {
"hapi": "^13.2.1"
}
},
"hapi14": {
"dependencies": {
"hapi": "^14.0.0"
}
},
"hapi15": {
"dependencies": {
"hapi": "^15.0.0"
Expand All @@ -74,14 +54,14 @@
"hapi": "^16.0.0"
}
},
"hapi8": {
"hapi17": {
"dependencies": {
"hapi": "^8.8.1"
"hapi": "^17.0.0"
}
},
"hapi9": {
"hapi8": {
"dependencies": {
"hapi": "^9.5.1"
"hapi": "^8.8.1"
}
},
"knex0.10": {
Expand Down
2 changes: 1 addition & 1 deletion test/test-mysql-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ if (semver.satisfies(process.version, '>=4')) {
samplingRate: 0,
enhancedDatabaseReporting: true
});
Hapi = require('./plugins/fixtures/hapi13');
Hapi = require('./plugins/fixtures/hapi16');
});

it('should work with connection pool access', function(done) {
Expand Down
5 changes: 3 additions & 2 deletions test/test-trace-web-frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {assertSpanDuration, DEFAULT_SPAN_DURATION, isServerSpan, wait} from './u
import {WebFramework, WebFrameworkConstructor} from './web-frameworks/base';
import {Connect3} from './web-frameworks/connect';
import {Express4} from './web-frameworks/express';
import {Hapi12, Hapi15, Hapi16, Hapi8} from './web-frameworks/hapi';
import {Hapi12, Hapi15, Hapi16, Hapi8} from './web-frameworks/hapi8_16';
import {Hapi17} from './web-frameworks/hapi17';
import {Koa1} from './web-frameworks/koa1';
import {Koa2} from './web-frameworks/koa2';
import {Restify3, Restify4, Restify5, Restify6} from './web-frameworks/restify';
Expand All @@ -44,7 +45,7 @@ type TraceSpanStackFrames = {
const ABORTED_SPAN_RETRIES = 3;
// The list of web frameworks to test.
const FRAMEWORKS: WebFrameworkConstructor[] = [
Connect3, Express4, Hapi8, Hapi12, Hapi15, Hapi16, Koa1, Koa2, Restify3,
Connect3, Express4, Hapi8, Hapi12, Hapi15, Hapi16, Hapi17, Koa1, Koa2, Restify3,
Restify4, Restify5, Restify6
];

Expand Down
44 changes: 44 additions & 0 deletions test/web-frameworks/hapi17.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {hapi_17} from '../../src/plugins/types';

import {WebFramework, WebFrameworkAddHandlerOptions} from './base';

export class Hapi17 implements WebFramework {
static commonName = `hapi@17`;
static expectedTopStackFrame = 'middleware';
static versionRange = '>=7.5';

server: hapi_17.Server;

constructor() {
const hapi = require('../plugins/fixtures/hapi17') as typeof hapi_17;
this.server = new hapi.Server();
}

addHandler(options: WebFrameworkAddHandlerOptions): void {
throw new Error();
}

async listen(port: number): Promise<number> {
return Number(this.server.info!.port);
}

shutdown(): void {
this.server.stop();
}
}
File renamed without changes.

0 comments on commit 092bb6b

Please sign in to comment.