diff --git a/src/plugins/plugin-hapi.ts b/src/plugins/plugin-hapi.ts index 0e6b33f28..a38beabce 100644 --- a/src/plugins/plugin-hapi.ts +++ b/src/plugins/plugin-hapi.ts @@ -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; @@ -34,87 +33,134 @@ 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( + 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); + return returned; + }; - // wrap end - res.end = function(this: ServerResponse) { - res.end = originalEnd; - const returned = res.end.apply(this, arguments); + // 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(); + }); - 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(); + return continueCb(); + }); +} - return returned; - }; +const plugin: PluginTypes.Plugin = [ + { + versions: '8 - 16', + patch(hapi, api) { + function createMiddleware(): hapi_16.ServerExtRequestHandler { + return function middleware(request, reply) { + return instrument(api, request, () => reply.continue()); + }; + } - // 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(); + 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(hapi) { + shimmer.unwrap(hapi.Server.prototype, 'connection'); + } + } as PluginTypes.Patch, + { + 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(); - }); - }; -} + type Hapi17ServerConstructor = {new (): hapi_17.Server}; -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; + const constructorWrap = ( + 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]; + + shimmer.wrap(hapi, 'Server', constructorWrap); + // Untyped hapi.server is an alias for hapi.Server. + // tslint:disable:no-any + if ((hapi as any).server) { + // Cast `'server'` as any rather than `hapi`, so that we can use + // template types to help the compiler understand the expected type + // of constructorWrap in lieu of inferrable type information. + shimmer.wrap( + hapi, 'server' as any, constructorWrap); + } + // tslint:enable:no-any + }, + unpatch: (hapi) => { + shimmer.unwrap(hapi, 'Server'); + // tslint:disable:no-any + if ((hapi as any).server) { + shimmer.unwrap(hapi, 'server' as any); + } + // tslint:enable:no-any + } + } as PluginTypes.Patch +]; export = plugin; diff --git a/src/plugins/types/index.d.ts b/src/plugins/types/index.d.ts index 27810b3a0..36d4e3add 100644 --- a/src/plugins/types/index.d.ts +++ b/src/plugins/types/index.d.ts @@ -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 @@ -88,6 +89,7 @@ export { connect_3, express_4, hapi_16, + hapi_17, koa_1, koa_2, pg_6, diff --git a/test/fixtures/plugin-fixtures.json b/test/fixtures/plugin-fixtures.json index b8c02c235..662eb25a1 100644 --- a/test/fixtures/plugin-fixtures.json +++ b/test/fixtures/plugin-fixtures.json @@ -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" @@ -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": { diff --git a/test/test-mysql-pool.ts b/test/test-mysql-pool.ts index 8bd8e1eb1..7e0bb0cc5 100644 --- a/test/test-mysql-pool.ts +++ b/test/test-mysql-pool.ts @@ -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) { diff --git a/test/test-trace-web-frameworks.ts b/test/test-trace-web-frameworks.ts index e6d4a6bc4..62ad9a539 100644 --- a/test/test-trace-web-frameworks.ts +++ b/test/test-trace-web-frameworks.ts @@ -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 {Hapi17} from './web-frameworks/hapi17'; +import {Hapi12, Hapi15, Hapi16, Hapi8} from './web-frameworks/hapi8_16'; import {Koa1} from './web-frameworks/koa1'; import {Koa2} from './web-frameworks/koa2'; import {Restify3, Restify4, Restify5, Restify6} from './web-frameworks/restify'; @@ -44,8 +45,8 @@ 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, - Restify4, Restify5, Restify6 + Connect3, Express4, Hapi8, Hapi12, Hapi15, Hapi16, Hapi17, Koa1, Koa2, + Restify3, Restify4, Restify5, Restify6 ]; /** diff --git a/test/web-frameworks/hapi17.ts b/test/web-frameworks/hapi17.ts new file mode 100644 index 000000000..e486eda92 --- /dev/null +++ b/test/web-frameworks/hapi17.ts @@ -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 { + return Number(this.server.info!.port); + } + + shutdown(): void { + this.server.stop(); + } +} diff --git a/test/web-frameworks/hapi.ts b/test/web-frameworks/hapi8_16.ts similarity index 100% rename from test/web-frameworks/hapi.ts rename to test/web-frameworks/hapi8_16.ts