diff --git a/.gitignore b/.gitignore index ae9c56dea..a06ca18a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -node_modules +node_modules/ npm-debug.log coverage .DS_Store .vscode build/ *.tgz +src/plugins/types/* +!src/plugins/types/index.d.ts diff --git a/package-lock.json b/package-lock.json index c50c5c085..a0e76838e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -288,6 +288,12 @@ "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==", "dev": true }, + "@types/methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.0.tgz", + "integrity": "sha512-ROomEm+QHlUmcQoDr3CBo3GRm0w4PVoFYjVT9YcfyBha/Per4deb1IpvHU7KTK7YBZCIvOYbSADoEyDnFgaWLA==", + "dev": true + }, "@types/minimatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.1.tgz", diff --git a/package.json b/package.json index 65d2aa4fa..d150a739b 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "system-test": "ts-node -P ./scripts ./scripts npm-compile decrypt-service-account-key run-system-tests", "changelog": "./bin/run-changelog.sh", "check-install": "ts-node -P ./scripts ./scripts npm-compile check-install", + "get-plugin-types": "ts-node -P ./scripts ./scripts get-plugin-types", "coverage": "ts-node -P ./scripts ./scripts npm-check npm-compile init-test-fixtures run-unit-tests-with-coverage report-coverage", "bump": "./bin/run-bump.sh", "check": "gts check", "clean": "gts clean", "compile-all": "tsc -p ./tsconfig.full.json", "compile-strict": "tsc -p .", - "compile": "ts-node -P ./scripts ./scripts npm-compile-all npm-compile-strict", + "compile": "ts-node -P ./scripts ./scripts get-plugin-types npm-compile-all npm-compile-strict", "fix": "gts fix", "prepare": "ts-node -P ./scripts ./scripts npm-clean npm-compile" }, @@ -50,6 +51,7 @@ "@types/extend": "^3.0.0", "@types/glob": "^5.0.32", "@types/is": "0.0.17", + "@types/methods": "^1.1.0", "@types/mocha": "^2.2.44", "@types/ncp": "^2.0.1", "@types/node": "^8.0.53", diff --git a/scripts/get-plugin-types.ts b/scripts/get-plugin-types.ts new file mode 100644 index 000000000..58050d35a --- /dev/null +++ b/scripts/get-plugin-types.ts @@ -0,0 +1,40 @@ +import { flatten, globP, mkdirP, ncpP, readFileP, spawnP, tmpDirP, writeFileP } from './utils'; + +const TYPES_DIRECTORY = 'src/plugins/types'; + +async function mkdirSafeP(dir: string) { + try { + await mkdirP(dir); + return true; + } catch (e) { + if (e.code !== 'EEXIST') { + throw new Error(`Error creating directory ${dir}`); + } + return false; + } +} + +export default async function() { + await mkdirSafeP(TYPES_DIRECTORY); + + const indexTs = (await readFileP(`${TYPES_DIRECTORY}/index.d.ts`, 'utf8') as string) + .split('\n'); + for (const line of indexTs) { + const matches = line.match(/^import \* as .* from '\.\/(.*)';\s*\/\/\s*(.*)@(.*)$/); + if (!matches) { + continue; + } + const [_0, packageName, name, version] = matches; + const installDir = `${TYPES_DIRECTORY}/${packageName}`; + if (await mkdirSafeP(installDir)) { + await spawnP('npm', ['init', '-y'], { + cwd: installDir + }); + await spawnP('npm', ['install', `@types/${name}@${version}`], { + cwd: installDir + }); + await writeFileP(`${installDir}/index.ts`, + `import * as _ from '${name}'; export = _;\n`, 'utf8'); + } + } +} diff --git a/scripts/index.ts b/scripts/index.ts index 5fef90ab5..2b47c68c8 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -2,6 +2,7 @@ const [bin, script, ...steps] = process.argv; import checkInstall from './check-install'; import decryptServiceAccountKey from './decrypt-service-account-key'; +import getPluginTypes from './get-plugin-types'; import initTestFixtures from './init-test-fixtures'; import reportCoverage from './report-coverage'; import runTests from './run-tests'; @@ -33,6 +34,9 @@ async function run(steps: string[]) { console.log('> Not decrypting service account key in PRs'); } break; + case 'get-plugin-types': + await getPluginTypes(); + break; case 'init-test-fixtures': await initTestFixtures(); break; diff --git a/scripts/utils.ts b/scripts/utils.ts index ff26eb076..6c3643ba6 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,4 +1,4 @@ -import { Stats, stat, readFile } from 'fs'; +import { mkdir, Stats, stat, readFile, writeFile } from 'fs'; import * as glob from 'glob'; import { ncp } from 'ncp'; import * as path from 'path'; @@ -10,15 +10,21 @@ import * as tmp from 'tmp'; export const BUILD_DIRECTORY = 'build'; export const globP: (pattern: string) => Promise = pify(glob); +export const mkdirP: (path: string) => Promise = pify(mkdir); export const ncpP: (src: string, dest: string) => Promise = pify(ncp); export const readFileP: (path: string, encoding?: string) => Promise = pify(readFile); export const statP: (path: string) => Promise = pify(stat); export const tmpDirP: () => Promise = pify(tmp.dir); +export const writeFileP: (path: string, data: any, encoding?: string) => Promise = pify(writeFile); export function nodule(nodule: string) { return path.relative(BUILD_DIRECTORY, `node_modules/${nodule}`); } +export function flatten(arr: Array>): Array { + return arr.reduce((acc, e) => acc.concat(e), []); +} + export function existsP(path: string): Promise { return statP(path).then( () => Promise.resolve(true), diff --git a/src/plugins/plugin-connect.ts b/src/plugins/plugin-connect.ts index 19471a61a..45d860383 100644 --- a/src/plugins/plugin-connect.ts +++ b/src/plugins/plugin-connect.ts @@ -13,26 +13,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -'use strict'; -var urlParse = require('url').parse; +// tslint:disable-next-line:no-reference +/// -var SUPPORTED_VERSIONS = '3.x'; +import {IncomingMessage, ServerResponse} from 'http'; +import * as shimmer from 'shimmer'; +import {parse as urlParse} from 'url'; -function createMiddleware(api) { - return function middleware(req, res, next) { - var options = { - name: urlParse(req.originalUrl).pathname, +import {PluginTypes} from '..'; + +import {connect_3} from './types'; + +type Connect3 = typeof connect_3; +// Connect docs note that routed requests have an originalUrl property. +// https://github.com/senchalabs/connect/tree/3.6.5#appuseroute-fn +type Request = IncomingMessage&{originalUrl?: string}; + +const SUPPORTED_VERSIONS = '3.x'; + +function getFirstHeader(req: IncomingMessage, key: string) { + let headerValue = req.headers[key]; + if (headerValue && typeof headerValue !== 'string') { + headerValue = headerValue[0]; + } + return headerValue; +} + +function createMiddleware(api: PluginTypes.TraceAgent): + connect_3.NextHandleFunction { + return function middleware(req: Request, res, next) { + const options = { + name: req.originalUrl ? (urlParse(req.originalUrl).pathname || '') : '', url: req.originalUrl, - traceContext: req.headers[api.constants.TRACE_CONTEXT_HEADER_NAME.toLowerCase()], + traceContext: + getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), skipFrames: 3 }; - api.runInRootSpan(options, function(root) { + api.runInRootSpan(options, (root) => { // Set response trace context. - var responseTraceContext = - api.getResponseTraceContext(options.traceContext, !!root); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext || null, !!root); if (responseTraceContext) { - res.setHeader(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + res.setHeader( + api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); } if (!root) { @@ -42,8 +66,8 @@ function createMiddleware(api) { api.wrapEmitter(req); api.wrapEmitter(res); - var url = (req.headers['X-Forwarded-Proto'] || 'http') + - '://' + req.headers.host + req.originalUrl; + const url = (req.headers['X-Forwarded-Proto'] || 'http') + '://' + + req.headers.host + req.originalUrl; // we use the path part of the url as the span name and add the full // url as a label @@ -52,18 +76,13 @@ function createMiddleware(api) { root.addLabel(api.labels.HTTP_SOURCE_IP, req.connection.remoteAddress); // wrap end - var originalEnd = res.end; - res.end = function() { + const originalEnd = res.end; + res.end = function(this: ServerResponse) { res.end = originalEnd; - var returned = res.end.apply(this, arguments); - - if (req.route && req.route.path) { - root.addLabel( - 'connect/request.route.path', req.route.path); - } + const returned = res.end.apply(this, arguments); - root.addLabel( - api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); + root.addLabel('connect/request.route.path', req.originalUrl); + root.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); root.endSpan(); return returned; @@ -74,19 +93,16 @@ function createMiddleware(api) { }; } -module.exports = [ - { - file: '', - versions: SUPPORTED_VERSIONS, - intercept: function(connect, api) { - return function() { - var app = connect(); - app.use(createMiddleware(api)); - return app; - }; - } +const plugin: PluginTypes.Plugin = [{ + file: '', + versions: SUPPORTED_VERSIONS, + intercept: (connect, api) => { + return function(this: {}) { + const app = connect(); + app.use(createMiddleware(api)); + return app; + }; } -]; - +} as PluginTypes.Intercept]; -export default {}; +export = plugin; diff --git a/src/plugins/plugin-express.ts b/src/plugins/plugin-express.ts index c4199bf03..3bc673060 100644 --- a/src/plugins/plugin-express.ts +++ b/src/plugins/plugin-express.ts @@ -14,35 +14,36 @@ * limitations under the License. */ -'use strict'; -var shimmer = require('shimmer'); -var methods = require('methods').concat('use', 'route', 'param', 'all'); +// tslint:disable-next-line:no-reference +/// -var SUPPORTED_VERSIONS = '4.x'; +import * as httpMethods from 'methods'; +import * as shimmer from 'shimmer'; -function patchModuleRoot(express, api) { - var labels = api.labels; - function applicationActionWrap(method) { - return function expressActionTrace() { - if (!this._google_trace_patched && !this._router) { - this._google_trace_patched = true; - this.use(middleware); - } - return method.apply(this, arguments); - }; - } +import {PluginTypes} from '..'; + +import {express_4} from './types'; + +// application is an undocumented member of the express object. +type Express4Module = typeof express_4&{application: express_4.Application}; - function middleware(req, res, next) { - var options = { +const methods = httpMethods.concat('use', 'route', 'param', 'all'); + +const SUPPORTED_VERSIONS = '4.x'; + +function patchModuleRoot(express: Express4Module, api: PluginTypes.TraceAgent) { + const labels = api.labels; + const middleware: express_4.RequestHandler = (req, res, next) => { + const options: PluginTypes.RootSpanOptions = { name: req.path, traceContext: req.get(api.constants.TRACE_CONTEXT_HEADER_NAME), url: req.originalUrl, skipFrames: 3 }; - api.runInRootSpan(options, function(rootSpan) { + api.runInRootSpan(options, (rootSpan) => { // Set response trace context. - var responseTraceContext = - api.getResponseTraceContext(options.traceContext, !!rootSpan); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext || null, !!rootSpan); if (responseTraceContext) { res.set(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); } @@ -55,16 +56,16 @@ function patchModuleRoot(express, api) { api.wrapEmitter(req); api.wrapEmitter(res); - var url = req.protocol + '://' + req.hostname + req.originalUrl; + const url = req.protocol + '://' + req.hostname + req.originalUrl; rootSpan.addLabel(labels.HTTP_METHOD_LABEL_KEY, req.method); rootSpan.addLabel(labels.HTTP_URL_LABEL_KEY, url); rootSpan.addLabel(labels.HTTP_SOURCE_IP, req.connection.remoteAddress); // wrap end - var originalEnd = res.end; - res.end = function() { + const originalEnd = res.end; + res.end = function(this: express_4.Response) { res.end = originalEnd; - var returned = res.end.apply(this, arguments); + const returned = res.end.apply(this, arguments); if (req.route && req.route.path) { rootSpan.addLabel('express/request.route.path', req.route.path); @@ -76,23 +77,34 @@ function patchModuleRoot(express, api) { next(); }); + }; + + function applicationActionWrap(method: T): () => T { + return function expressActionTrace(this: express_4.Application& + PluginTypes.TraceAgentExtension) { + if (!this._google_trace_patched && !this._router) { + this._google_trace_patched = true; + this.use(middleware); + } + return method.apply(this, arguments); + }; } - methods.forEach(function(method) { + methods.forEach((method) => { shimmer.wrap(express.application, method, applicationActionWrap); }); } -function unpatchModuleRoot(express) { - methods.forEach(function(method) { +function unpatchModuleRoot(express: Express4Module) { + methods.forEach((method) => { shimmer.unwrap(express.application, method); }); } -module.exports = [{ - versions: SUPPORTED_VERSIONS, - patch: patchModuleRoot, - unpatch: unpatchModuleRoot -}]; +const plugin: PluginTypes.Plugin = [{ + versions: SUPPORTED_VERSIONS, + patch: patchModuleRoot, + unpatch: unpatchModuleRoot +} as PluginTypes.Patch]; -export default {}; +export = plugin; diff --git a/src/plugins/plugin-hapi.ts b/src/plugins/plugin-hapi.ts index 95c7ff05e..1e1a29651 100644 --- a/src/plugins/plugin-hapi.ts +++ b/src/plugins/plugin-hapi.ts @@ -13,40 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -'use strict'; -var shimmer = require('shimmer'); -var urlParse = require('url').parse; +// TODO(kjin): Remove this when @types/shimmer are published. +// tslint:disable-next-line:no-reference +/// -var SUPPORTED_VERSIONS = '8 - 16'; +import {IncomingMessage, ServerResponse} from 'http'; +import * as shimmer from 'shimmer'; +import {parse as urlParse} from 'url'; -function createConnectionWrap(api) { - return function connectionWrap(connection) { - return function connectionTrace() { - var server = connection.apply(this, arguments); - server.ext('onRequest', createMiddleware(api)); - return server; - }; - }; +import {PluginTypes} from '..'; + +import {hapi_16} from './types'; + +type Hapi16Module = typeof hapi_16; + +const SUPPORTED_VERSIONS = '8 - 16'; + +function getFirstHeader(req: IncomingMessage, key: string) { + let headerValue = req.headers[key]; + if (headerValue && typeof headerValue !== 'string') { + headerValue = headerValue[0]; + } + return headerValue; } -function createMiddleware(api) { +function createMiddleware(api: PluginTypes.TraceAgent): + hapi_16.ServerExtRequestHandler { return function middleware(request, reply) { - var req = request.raw.req; - var res = request.raw.res; - var originalEnd = res.end; - var options = { - name: urlParse(req.url).pathname, + 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: req.headers[api.constants.TRACE_CONTEXT_HEADER_NAME], + traceContext: + getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), skipFrames: 3 }; - api.runInRootSpan(options, function(root) { + api.runInRootSpan(options, (root) => { // Set response trace context. - var responseTraceContext = - api.getResponseTraceContext(options.traceContext, !!root); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext || null, !!root); if (responseTraceContext) { - res.setHeader(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + res.setHeader( + api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); } if (!root) { @@ -56,36 +67,36 @@ function createMiddleware(api) { api.wrapEmitter(req); api.wrapEmitter(res); - var url = (req.headers['X-Forwarded-Proto'] || 'http') + - '://' + req.headers.host + req.url; - + 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. + // 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() { + res.end = function(this: ServerResponse) { res.end = originalEnd; - var returned = res.end.apply(this, arguments); + const returned = res.end.apply(this, arguments); - if (req.route && req.route.path) { - root.addLabel( - 'hapi/request.route.path', req.route.path); + 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.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); root.endSpan(); return returned; }; // if the event is aborted, end the span (as res.end will not be called) - req.once('aborted', function() { + req.once('aborted', () => { root.addLabel(api.labels.ERROR_DETAILS_NAME, 'aborted'); - root.addLabel(api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request'); + root.addLabel( + api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request'); root.endSpan(); }); @@ -94,19 +105,21 @@ function createMiddleware(api) { }; } -module.exports = [ - { - file: '', - versions: SUPPORTED_VERSIONS, - patch: function(hapi, api) { - shimmer.wrap(hapi.Server.prototype, - 'connection', - createConnectionWrap(api)); - }, - unpatch: function(hapi) { - shimmer.unwrap(hapi.Server.prototype, 'connection'); - } +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; + }; + }); + }, + unpatch: (hapi) => { + shimmer.unwrap(hapi.Server.prototype, 'connection'); } -]; - -export default {}; +} as PluginTypes.Patch]; +export = plugin; diff --git a/src/plugins/plugin-koa.ts b/src/plugins/plugin-koa.ts index b95aad7e7..3184d2539 100644 --- a/src/plugins/plugin-koa.ts +++ b/src/plugins/plugin-koa.ts @@ -13,134 +13,169 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -'use strict'; -const shimmer = require('shimmer'); -const urlParse = require('url').parse; +// TODO(kjin): Remove this when @types/shimmer are published. +// tslint:disable-next-line:no-reference +/// -function startSpanForRequest(api, req, res, next) { +import {IncomingMessage, ServerResponse} from 'http'; +import * as shimmer from 'shimmer'; +import {parse as urlParse} from 'url'; + +import {PluginTypes} from '..'; + +import {koa_1, koa_2} from './types'; + +type Koa1Module = typeof koa_1; +type Koa2Module = typeof koa_2; +// routePath is populated if the user uses the koa-route module. +type KoaContext = (koa_1.Context|koa_2.Context)&{routePath?: string | RegExp}; + +interface KoaModule { + // TypeScript isn't expressive enough, but KoaModule#use should return `this`. + // tslint:disable-next-line:no-any + readonly prototype: {use: (m: T) => any}; +} + +// Function signature for createMiddleware[2x] +type CreateMiddlewareFn = (api: PluginTypes.TraceAgent) => T; +// Function signature for a function that returns the value of the "next" +// middleware function parameter, wrapped to propagate context based on the +// propagateContext flag. The type of "next" differs between Koa 1 and 2. +type GetNextFn = (propagateContext: boolean) => T; + +function getFirstHeader(req: IncomingMessage, key: string) { + let headerValue = req.headers[key]; + if (headerValue && typeof headerValue !== 'string') { + headerValue = headerValue[0]; + } + return headerValue; +} + +function startSpanForRequest( + api: PluginTypes.TraceAgent, ctx: KoaContext, getNext: GetNextFn): T { + const {req, res} = ctx; const originalEnd = res.end; const options = { - name: urlParse(req.url).pathname, + name: req.url ? (urlParse(req.url).pathname || '') : '', url: req.url, - traceContext: req.headers[api.constants.TRACE_CONTEXT_HEADER_NAME], + traceContext: getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), skipFrames: 4 }; - return api.runInRootSpan(options, function(root) { + return api.runInRootSpan(options, root => { // Set response trace context. const responseTraceContext = - api.getResponseTraceContext(options.traceContext, !!root); + api.getResponseTraceContext(options.traceContext || null, !!root); if (responseTraceContext) { - res.setHeader(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + res.setHeader( + api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); } - + if (!root) { - return next; + return getNext(false); } api.wrapEmitter(req); api.wrapEmitter(res); - const url = (req.headers['X-Forwarded-Proto'] || 'http') + - '://' + req.headers.host + req.url; + 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 middlewear runs. + // req.path would be more desirable but is not set at the time our + // middlewear 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() { + res.end = function(this: ServerResponse) { res.end = originalEnd; const returned = res.end.apply(this, arguments); - if (req.route && req.route.path) { - root.addLabel( - 'koa/request.route.path', req.route.path); + if (ctx.routePath) { + root.addLabel('koa/request.route.path', ctx.routePath); } - root.addLabel( - api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); + root.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); root.endSpan(); return returned; }; // if the event is aborted, end the span (as res.end will not be called) - req.once('aborted', function() { + req.once('aborted', () => { root.addLabel(api.labels.ERROR_DETAILS_NAME, 'aborted'); - root.addLabel(api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request'); + root.addLabel( + api.labels.ERROR_DETAILS_MESSAGE, 'client aborted the request'); root.endSpan(); }); - // In Koa 1, next is a Generator object. - // We wrap Generator#next here. - if (!next.apply && next.next) { - next.next = api.wrap(next.next); - return next; - } else { - return api.wrap(next); - } + return getNext(true); }); } -function createMiddleware(api) { - return function* middleware(next) { - /* jshint validthis:true */ - const req = this.req; - const res = this.res; - - next = startSpanForRequest(api, req, res, next); - +function createMiddleware(api: PluginTypes.TraceAgent): koa_1.Middleware { + // Koa 1 type definitions use any here. + // tslint:disable-next-line:no-any + return function* middleware(this: koa_1.Context, next: IterableIterator<{}>) { + next = startSpanForRequest(api, this, (propagateContext: boolean) => { + if (propagateContext) { + next.next = api.wrap(next.next); + } + return next; + }); yield next; }; } -function createMiddleware2x(api) { +function createMiddleware2x(api: PluginTypes.TraceAgent): koa_2.Middleware { return function middleware(ctx, next) { - const req = ctx.req; - const res = ctx.res; - - next = startSpanForRequest(api, req, res, next); - + next = startSpanForRequest( + api, ctx, + (propagateContext: boolean) => + propagateContext ? api.wrap(next) : next); return next(); }; } -function patchUse(koa, api, createMiddlewareFunction) { - shimmer.wrap(koa.prototype, 'use', function useWrap(use) { - return function useTrace() { - if (!this._google_trace_patched) { - this._google_trace_patched = true; - this.use(createMiddlewareFunction(api)); - } - return use.apply(this, arguments); - }; +function patchUse( + koa: KoaModule, api: PluginTypes.TraceAgent, + createMiddlewareFunction: CreateMiddlewareFn) { + shimmer.wrap(koa.prototype, 'use', (use) => { + return function useTrace(this: typeof koa.prototype& + PluginTypes.TraceAgentExtension): + typeof koa.prototype { + if (!this._google_trace_patched) { + this._google_trace_patched = true; + this.use(createMiddlewareFunction(api)); + } + return use.apply(this, arguments); + }; }); } -module.exports = [ +const plugin: PluginTypes.Plugin = [ { file: '', versions: '1.x', - patch: function(koa, api) { + patch: (koa, api) => { patchUse(koa, api, createMiddleware); }, - unpatch: function(koa) { + unpatch: (koa) => { shimmer.unwrap(koa.prototype, 'use'); } - }, + } as PluginTypes.Patch, { file: '', versions: '2.x', - patch: function(koa, api) { + patch: (koa, api) => { patchUse(koa, api, createMiddleware2x); }, - unpatch: function(koa) { + unpatch: (koa) => { shimmer.unwrap(koa.prototype, 'use'); } - } + } as PluginTypes.Patch ]; -export default {}; +export = plugin; diff --git a/src/plugins/plugin-restify.ts b/src/plugins/plugin-restify.ts index adc552c63..a4e5c3898 100644 --- a/src/plugins/plugin-restify.ts +++ b/src/plugins/plugin-restify.ts @@ -14,43 +14,58 @@ * limitations under the License. */ -'use strict'; +// TODO(kjin): Remove this when @types/shimmer are published. +// tslint:disable-next-line:no-reference +/// -var shimmer = require('shimmer'); +import {ServerResponse} from 'http'; +import * as shimmer from 'shimmer'; +import {parse as urlParse} from 'url'; -var SUPPORTED_VERSIONS = '<=6.x'; +import {PluginTypes} from '..'; -function unpatchRestify(restify) { +import {restify_5} from './types'; + +type Restify5 = typeof restify_5; +type Request = restify_5.Request&{route?: {path: string | RegExp}}; +type Response = restify_5.Response; +type Next = restify_5.Next; +type CreateServerFn = (options?: restify_5.ServerOptions) => restify_5.Server; + +const SUPPORTED_VERSIONS = '<=6.x'; + +function unpatchRestify(restify: Restify5) { shimmer.unwrap(restify, 'createServer'); } -function patchRestify(restify, api) { - shimmer.wrap(restify, 'createServer', createServerWrap); +function patchRestify(restify: Restify5, api: PluginTypes.TraceAgent) { + shimmer.wrap(restify, 'createServer', createServerWrap); - function createServerWrap(createServer) { - return function createServerTrace() { - var server = createServer.apply(this, arguments); + function createServerWrap(createServer: CreateServerFn): CreateServerFn { + return function createServerTrace(this: {}) { + const server = createServer.apply(this, arguments) as restify_5.Server; server.use(middleware); return server; }; } - function middleware(req, res, next) { - var options = { + function middleware(req: Request, res: Response, next: Next): void { + const options = { // we use the path part of the url as the span name and add the full url // as a label later. name: req.path(), url: req.url, - traceContext: req.header(api.constants.TRACE_CONTEXT_HEADER_NAME, null), + traceContext: req.header(api.constants.TRACE_CONTEXT_HEADER_NAME), skipFrames: 3 }; - api.runInRootSpan(options, function(rootSpan) { + api.runInRootSpan(options, rootSpan => { // Set response trace context. - var responseTraceContext = - api.getResponseTraceContext(options.traceContext, !!rootSpan); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext, !!rootSpan); if (responseTraceContext) { - res.header(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + res.header( + api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); } if (!rootSpan) { @@ -60,23 +75,23 @@ function patchRestify(restify, api) { api.wrapEmitter(req); api.wrapEmitter(res); - var fullUrl = req.header('X-Forwarded-Proto', 'http') + '://' + - req.header('host') + req.url; + const fullUrl = req.header('X-Forwarded-Proto', 'http') + '://' + + req.header('host') + req.url; rootSpan.addLabel(api.labels.HTTP_METHOD_LABEL_KEY, req.method); rootSpan.addLabel(api.labels.HTTP_URL_LABEL_KEY, fullUrl); - rootSpan.addLabel(api.labels.HTTP_SOURCE_IP, - req.connection.remoteAddress); + rootSpan.addLabel( + api.labels.HTTP_SOURCE_IP, req.connection.remoteAddress); - var originalEnd = res.end; - res.end = function() { + const originalEnd = res.end; + res.end = function(this: ServerResponse) { res.end = originalEnd; - var returned = res.end.apply(this, arguments); + const returned = res.end.apply(this, arguments); if (req.route && req.route.path) { rootSpan.addLabel('restify/request.route.path', req.route.path); } - rootSpan.addLabel(api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, - res.statusCode); + rootSpan.addLabel( + api.labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); rootSpan.endSpan(); return returned; }; @@ -86,8 +101,10 @@ function patchRestify(restify, api) { } } -module.exports = [ - {versions: SUPPORTED_VERSIONS, patch: patchRestify, unpatch: unpatchRestify} -]; +const plugin: PluginTypes.Plugin = [{ + versions: SUPPORTED_VERSIONS, + patch: patchRestify, + unpatch: unpatchRestify +} as PluginTypes.Patch]; -export default {}; +export = plugin; diff --git a/src/plugins/types/index.d.ts b/src/plugins/types/index.d.ts new file mode 100644 index 000000000..95587b8ff --- /dev/null +++ b/src/plugins/types/index.d.ts @@ -0,0 +1,36 @@ +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 koa_2 from './koa_2'; // koa@2 +import * as restify_5 from './restify_5'; // restify@5 + +//---koa@1---// + +declare class koa_1 { + use(middleware: koa_1.Middleware): this; +} + +declare namespace koa_1 { + interface Middleware { + (this: Context, next: IterableIterator): any; + } + + // Koa 1 and 2 differ primarily in the middleware passed to Koa#use. + // For our purposes we can borrow type definitions from Koa 2. + // + // References: + // https://github.com/koajs/koa/issues/533 + // https://github.com/koajs/koa/blob/master/History.md#200-alpha1--2015-10-22 + interface Context extends koa_2.Context {} +} + +//---exports---// + +export { + connect_3, + express_4, + hapi_16, + koa_1, + koa_2, + restify_5 +}; diff --git a/tsconfig.full.json b/tsconfig.full.json index 81c95cfcf..336e867a2 100644 --- a/tsconfig.full.json +++ b/tsconfig.full.json @@ -11,7 +11,8 @@ "noEmitOnError": false }, "include": [ - "src/**/*.ts", + "src/*.ts", + "src/plugins/*.ts", "test/*.ts", "test/plugins/*.ts" ] diff --git a/tsconfig.json b/tsconfig.json index fb34f597a..0bb6404b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,12 @@ "src/*.ts", "src/plugins/plugin-http2.ts", "src/plugins/plugin-https.ts", + "src/plugins/plugin-connect.ts", + "src/plugins/plugin-express.ts", + "src/plugins/plugin-hapi.ts", + "src/plugins/plugin-https.ts", + "src/plugins/plugin-koa.ts", + "src/plugins/plugin-restify.ts", "test/plugins/test-trace-http2.ts" ] }