diff --git a/packages/opentelemetry-plugin-express/src/express.ts b/packages/opentelemetry-plugin-express/src/express.ts index ff829c0aacd..a1c130bc4d8 100644 --- a/packages/opentelemetry-plugin-express/src/express.ts +++ b/packages/opentelemetry-plugin-express/src/express.ts @@ -14,27 +14,36 @@ * limitations under the License. */ -// mongodb.Server type is deprecated so every use trigger a lint error -/* tslint:disable:deprecation */ - import { BasePlugin } from '@opentelemetry/core'; -import { CanonicalCode, Span, Attributes } from '@opentelemetry/types'; +import { Attributes } from '@opentelemetry/types'; import * as express from 'express'; +import * as core from "express-serve-static-core"; import * as shimmer from 'shimmer'; import { ExpressLayer, ExpressRouter, AttributeNames, PatchedRequest, + Parameters, + PathParams, + _MIDDLEWARES_STORE_PROPERTY } from './types'; +import { + getLayerMetadata, + storeLayerPath, + patchEnd, +} from './utils' import { VERSION } from './version'; -export const kPatched: unique symbol = Symbol('express-layer-patched'); +/** + * This symbol is used to mark express layer as being already instrumented + * since its possible to use a given layer multiple times (ex: middlewares) + */ +export const kLayerPatched: unique symbol = Symbol('express-layer-patched'); /** Express instrumentation plugin for OpenTelemetry */ export class ExpressPlugin extends BasePlugin { - private readonly _COMPONENT = 'express'; - + readonly _COMPONENT = 'express'; readonly supportedVersions = ['^4.0.0']; constructor(readonly moduleName: string) { @@ -47,87 +56,91 @@ export class ExpressPlugin extends BasePlugin { protected patch() { this._logger.debug('Patching Express'); - if (this._moduleExports) { - const routerProto = (this._moduleExports - .Router as unknown) as express.Router; - const plugin = this; - - this._logger.debug('patching express.Router.prototype.route'); - shimmer.wrap(routerProto, 'route', (original: Function) => { - return function route_trace( - this: ExpressRouter, - arg: string | Function - ) { - const route = original.apply(this, arguments); - const layer = this.stack[this.stack.length - 1] as ExpressLayer; - plugin._applyPatch(layer, typeof arg === 'string' ? arg : undefined); - return route; - // tslint:disable-next-line:no-any - } as any; - }); - this._logger.debug('patching express.Router.prototype.use'); - shimmer.wrap(routerProto, 'use', (original: Function) => { - return function use(this: express.Application, arg: string | Function) { - const route = original.apply(this, arguments); - const layer = this.stack[this.stack.length - 1] as ExpressLayer; - plugin._applyPatch(layer, typeof arg === 'string' ? arg : undefined); - return route; - // tslint:disable-next-line:no-any - } as any; - }); - this._logger.debug('patching express.Application.use'); - shimmer.wrap( - this._moduleExports.application, - 'use', - (original: Function) => { - return function use( - this: { _router: ExpressRouter }, - arg: string | Function - ) { - const route = original.apply(this, arguments); - const layer = this._router.stack[this._router.stack.length - 1]; - plugin._applyPatch( - layer, - typeof arg === 'string' ? arg : undefined - ); - return route; - // tslint:disable-next-line:no-any - } as any; - } - ); + if (this._moduleExports === undefined || this._moduleExports === null) { + return this._moduleExports; } + const routerProto = (this._moduleExports.Router as unknown) as express.Router; + + this._logger.debug('patching express.Router.prototype.route'); + shimmer.wrap(routerProto, 'route', this._getRoutePatch.bind(this)); + + this._logger.debug('patching express.Router.prototype.use'); + shimmer.wrap(routerProto, 'use', this._getRouterUsePatch.bind(this)); + + this._logger.debug('patching express.Application.use'); + shimmer.wrap(this._moduleExports.application, 'use', this._getAppUsePatch.bind(this)); return this._moduleExports; } - /** Unpatches all MongoDB patched functions. */ - unpatch(): void { - shimmer.unwrap(this._moduleExports.Router.prototype, 'use'); - shimmer.unwrap(this._moduleExports.Router.prototype, 'route'); - shimmer.unwrap(this._moduleExports.application, 'use'); + /** + * Get the patch for Router.route function + * @param original + */ + private _getRoutePatch (original: (path: PathParams) => express.IRoute) { + const plugin = this + return function route_trace( + this: ExpressRouter, + ...args: Parameters + ) { + const route = original.apply(this, args); + const layer = this.stack[this.stack.length - 1] as ExpressLayer; + plugin._applyPatch(layer, typeof args[0] === 'string' ? args[0] : undefined); + return route; + }; } /** - * Store layers path in the request to be able to construct route later - * @param request The request where - * @param value the value to push into the array + * Get the patch for Router.use function + * @param original */ - private _storeLayerPath(request: PatchedRequest, value?: string) { - if (Array.isArray(request.__ot_middlewares) === false) { - Object.defineProperty(request, '__ot_middlewares', { - enumerable: false, - value: [], - }); - } - if (value === undefined) return; - (request.__ot_middlewares as string[]).push(value); + private _getRouterUsePatch (original: express.IRouterHandler & express.IRouterMatcher) { + const plugin = this + return function use( + this: express.Application, + ...args: Parameters + ) { + const route = original.apply(this, args); + const layer = this.stack[this.stack.length - 1] as ExpressLayer; + plugin._applyPatch(layer, typeof args[0] === 'string' ? args[0] : undefined); + return route; + // tslint:disable-next-line:no-any + } as any + } + + /** + * Get the patch for Application.use function + * @param original + */ + private _getAppUsePatch (original: core.ApplicationRequestHandler) { + const plugin = this + return function use( + this: { _router: ExpressRouter }, + ...args: Parameters + ) { + const route = original.apply(this, args); + const layer = this._router.stack[this._router.stack.length - 1]; + plugin._applyPatch( + layer, + typeof args[0] === 'string' ? args[0] : undefined + ); + return route; + // tslint:disable-next-line:no-any + } as any; + } + + /** Unpatches all Express patched functions. */ + unpatch(): void { + shimmer.unwrap(this._moduleExports.Router.prototype, 'use'); + shimmer.unwrap(this._moduleExports.Router.prototype, 'route'); + shimmer.unwrap(this._moduleExports.application, 'use'); } - /** Creates spans for Cursor operations */ + /** Patch each express layer to create span and propagate scope */ private _applyPatch(layer: ExpressLayer, layerPath?: string) { const plugin = this; - if (layer[kPatched] === true) return; - layer[kPatched] = true; + if (layer[kLayerPatched] === true) return; + layer[kLayerPatched] = true; this._logger.debug('patching express.Router.Layer.handle'); shimmer.wrap(layer, 'handle', function(original: Function) { if (original.length === 4) return original; @@ -138,31 +151,18 @@ export class ExpressPlugin extends BasePlugin { res: express.Response, next: express.NextFunction ) { - plugin._storeLayerPath(req, layerPath); - const route = (req.__ot_middlewares as string[]).join(''); + storeLayerPath(req, layerPath); + const route = (req[_MIDDLEWARES_STORE_PROPERTY] as string[]).join(''); const attributes: Attributes = { [AttributeNames.COMPONENT]: plugin._COMPONENT, [AttributeNames.HTTP_ROUTE]: route.length > 0 ? route : undefined, }; - let spanName = ''; - if (layer.name === 'router') { - spanName = `express router - ${layerPath}`; - attributes[AttributeNames.EXPRESS_NAME] = layerPath; - attributes[AttributeNames.EXPRESS_TYPE] = 'router'; - } else if (layer.name === 'bound dispatch') { - spanName = `express request handler`; - attributes[AttributeNames.EXPRESS_TYPE] = 'request_handler'; - } else { - spanName = `express middleware - ${layer.name}`; - attributes[AttributeNames.EXPRESS_NAME] = layer.name; - attributes[AttributeNames.EXPRESS_TYPE] = 'middleware'; - } - const span = plugin._tracer.startSpan(spanName, { + const metadata = getLayerMetadata(layer, layerPath) + + const span = plugin._tracer.startSpan(metadata.name, { parent: plugin._tracer.getCurrentSpan(), - attributes, + attributes: Object.assign(attributes, metadata.attributes), }); - // if we cant create a span, abort - if (span === null) return original.apply(this, arguments); // verify we have a callback let callbackIdx = Array.from(arguments).findIndex( arg => typeof arg === 'function' @@ -172,9 +172,9 @@ export class ExpressPlugin extends BasePlugin { arguments[callbackIdx] = function() { callbackHasBeenCalled = true; if (!(req.route && arguments[0] instanceof Error)) { - (req.__ot_middlewares as string[]).pop(); + (req[_MIDDLEWARES_STORE_PROPERTY] as string[]).pop(); } - return plugin._patchEnd(span, plugin._tracer.bind(next))(); + return patchEnd(span, plugin._tracer.bind(next))(); }; } const result = original.apply(this, arguments); @@ -187,29 +187,6 @@ export class ExpressPlugin extends BasePlugin { }; }); } - - /** - * Ends a created span. - * @param span The created span to end. - * @param resultHandler A callback function. - */ - private _patchEnd(span: Span, resultHandler: Function): Function { - return function patchedEnd(this: {}, ...args: unknown[]) { - const error = args[0]; - if (error instanceof Error) { - span.setStatus({ - code: CanonicalCode.INTERNAL, - message: error.message, - }); - } else { - span.setStatus({ - code: CanonicalCode.OK, - }); - } - span.end(); - return resultHandler.apply(this, args); - }; - } } export const plugin = new ExpressPlugin('express'); diff --git a/packages/opentelemetry-plugin-express/src/types.ts b/packages/opentelemetry-plugin-express/src/types.ts index 9fb46599bf4..8644e45f764 100644 --- a/packages/opentelemetry-plugin-express/src/types.ts +++ b/packages/opentelemetry-plugin-express/src/types.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { kPatched } from './express'; +import { kLayerPatched } from './express'; import { Request } from 'express'; -export type PatchedRequest = { ['__ot_middlewares']?: string[] } & Request; +export const _MIDDLEWARES_STORE_PROPERTY = '__ot_middlewares' + +export type Parameters = T extends (...args: infer T) => any ? T : unknown[]; +export type PatchedRequest = { [_MIDDLEWARES_STORE_PROPERTY]?: string[] } & Request; +export type PathParams = string | RegExp | Array; // https://github.com/expressjs/express/blob/master/lib/router/index.js#L53 export type ExpressRouter = { @@ -32,7 +36,7 @@ export type ExpressRouter = { // https://github.com/expressjs/express/blob/master/lib/router/layer.js#L33 export type ExpressLayer = { handle: Function; - [kPatched]?: boolean; + [kLayerPatched]?: boolean; name: string; params: { [key: string]: string }; path: string; @@ -46,3 +50,9 @@ export enum AttributeNames { EXPRESS_TYPE = 'express.type', EXPRESS_NAME = 'express.name', } + +export enum ExpressLayerType { + ROUTER = 'router', + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request_handler' +} diff --git a/packages/opentelemetry-plugin-express/src/utils.ts b/packages/opentelemetry-plugin-express/src/utils.ts new file mode 100644 index 00000000000..589bd0a8d3e --- /dev/null +++ b/packages/opentelemetry-plugin-express/src/utils.ts @@ -0,0 +1,98 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * 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 + * + * https://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 { CanonicalCode, Span, Attributes } from '@opentelemetry/types'; +import { + ExpressLayer, + AttributeNames, + PatchedRequest, + _MIDDLEWARES_STORE_PROPERTY, + ExpressLayerType +} from './types'; + +/** + * Store layers path in the request to be able to construct route later + * @param request The request where + * @param [value] the value to push into the array + */ +export const storeLayerPath = (request: PatchedRequest, value?: string) => { + if (Array.isArray(request[_MIDDLEWARES_STORE_PROPERTY]) === false) { + Object.defineProperty(request, _MIDDLEWARES_STORE_PROPERTY, { + enumerable: false, + value: [], + }); + } + if (value === undefined) return; + (request[_MIDDLEWARES_STORE_PROPERTY] as string[]).push(value); +} + +/** + * Parse express layer context to retrieve a name and attributes. + * @param layer Express layer + * @param [layerPath] if present, the path on which the layer has been mounted + */ +export const getLayerMetadata = (layer: ExpressLayer, layerPath?: string): { + attributes: Attributes, + name: string +} => { + if (layer.name === 'router') { + return { + attributes: { + [AttributeNames.EXPRESS_NAME]: layerPath, + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.ROUTER + }, + name: `router - ${layerPath}` + } + } else if (layer.name === 'bound dispatch') { + return { + attributes: { + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.REQUEST_HANDLER + }, + name: 'request handler' + } + } else { + return { + attributes: { + [AttributeNames.EXPRESS_NAME]: layer.name, + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.MIDDLEWARE + }, + name: `middleware - ${layer.name}` + } + } +} + +/** + * Ends a created span. + * @param span The created span to end. + * @param resultHandler A callback function. + */ +export const patchEnd = (span: Span, resultHandler: Function): Function => { + return function patchedEnd(this: {}, ...args: unknown[]) { + const error = args[0]; + if (error instanceof Error) { + span.setStatus({ + code: CanonicalCode.INTERNAL, + message: error.message, + }); + } else { + span.setStatus({ + code: CanonicalCode.OK, + }); + } + span.end(); + return resultHandler.apply(this, args); + }; +} \ No newline at end of file diff --git a/packages/opentelemetry-plugin-express/test/express.test.ts b/packages/opentelemetry-plugin-express/test/express.test.ts index d09af42c0d5..9e715c3fdc8 100644 --- a/packages/opentelemetry-plugin-express/test/express.test.ts +++ b/packages/opentelemetry-plugin-express/test/express.test.ts @@ -99,7 +99,7 @@ describe('Express Plugin', () => { ); const requestHandlerSpan = memoryExporter .getFinishedSpans() - .find(span => span.name.includes('express request handler')); + .find(span => span.name.includes('request handler')); assert(requestHandlerSpan !== undefined); assert( requestHandlerSpan?.attributes[AttributeNames.COMPONENT] === diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 62d244f2c4b..a17cba83fd0 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -439,13 +439,17 @@ export const getIncomingRequestAttributesOnResponse = ( .join('') : undefined; - return { + const attributes: Attributes = { [AttributeNames.NET_HOST_IP]: localAddress, [AttributeNames.NET_HOST_PORT]: localPort, [AttributeNames.NET_PEER_IP]: remoteAddress, [AttributeNames.NET_PEER_PORT]: remotePort, [AttributeNames.HTTP_STATUS_CODE]: statusCode, [AttributeNames.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(), - [AttributeNames.HTTP_ROUTE]: route, }; + + if (route !== undefined) { + attributes[AttributeNames.HTTP_ROUTE] = route; + } + return attributes; }; diff --git a/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts b/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts index d81cbfb0d4e..fc0a9e1fe9f 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/utils.test.ts @@ -316,7 +316,8 @@ describe('Utility', () => { const request = { __ot_middlewares: ['/test', '/toto', '/'], }; - // @ts-ignore we just want to check that the parsing of express middlewares + // @ts-ignore ignore error about invalid request types since we only want to + // check the parsing of the `__ot_middlewares` property const attributes = utils.getIncomingRequestAttributesOnResponse(request, { socket: {}, }); @@ -324,7 +325,8 @@ describe('Utility', () => { }); it('should succesfully process without middleware stack', () => { const request = {}; - // @ts-ignore we just want to check that the parsing of express middlewares + // @ts-ignore ignore error about invalid request types since we only want to + // check the parsing of the `__ot_middlewares` property const attributes = utils.getIncomingRequestAttributesOnResponse(request, { socket: {}, });