diff --git a/packages/opentelemetry-plugin-express/README.md b/packages/opentelemetry-plugin-express/README.md index b8dc226b8c4..c230463c66c 100644 --- a/packages/opentelemetry-plugin-express/README.md +++ b/packages/opentelemetry-plugin-express/README.md @@ -45,6 +45,20 @@ const registry = new NodeTracerRegistry(); See [examples/express](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/express) for a short example. +### Express Plugin Options + +Express plugin has few options available to choose from. You can set the following: + +| Options | Type | Description | +| ------- | ---- | ----------- | +| `ignoreLayers` | `IgnoreMatcher[]` | Express plugin will not trace all layers that match. | +| `ignoreLayersType`| `ExpressLayerType[]` | Express plugin will ignore the layers that match based on their type. | + +For reference, here are the three different layer type: + - `router` is the name of `express.Router()` + - `middleware` + - `request_handler` is the name for anything thats not a router or a middleware. + ## Useful links - For more information on OpenTelemetry, visit: - For more about OpenTelemetry JavaScript: diff --git a/packages/opentelemetry-plugin-express/src/express.ts b/packages/opentelemetry-plugin-express/src/express.ts index 706c3a28ae5..6a621b99c6c 100644 --- a/packages/opentelemetry-plugin-express/src/express.ts +++ b/packages/opentelemetry-plugin-express/src/express.ts @@ -27,8 +27,15 @@ import { Parameters, PathParams, _MIDDLEWARES_STORE_PROPERTY, + ExpressPluginConfig, + ExpressLayerType, } from './types'; -import { getLayerMetadata, storeLayerPath, patchEnd } from './utils'; +import { + getLayerMetadata, + storeLayerPath, + patchEnd, + isLayerIgnored, +} from './utils'; import { VERSION } from './version'; /** @@ -41,6 +48,7 @@ export const kLayerPatched: unique symbol = Symbol('express-layer-patched'); export class ExpressPlugin extends BasePlugin { readonly _COMPONENT = 'express'; readonly supportedVersions = ['^4.0.0']; + protected _config!: ExpressPluginConfig; constructor(readonly moduleName: string) { super('@opentelemetry/plugin-express', VERSION); @@ -74,6 +82,15 @@ export class ExpressPlugin extends BasePlugin { return this._moduleExports; } + /** Unpatches all Express patched functions. */ + unpatch(): void { + const routerProto = (this._moduleExports + .Router as unknown) as express.Router; + shimmer.unwrap(routerProto, 'use'); + shimmer.unwrap(routerProto, 'route'); + shimmer.unwrap(this._moduleExports.application, 'use'); + } + /** * Get the patch for Router.route function * @param original @@ -141,13 +158,6 @@ export class ExpressPlugin extends BasePlugin { } 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'); - } - /** Patch each express layer to create span and propagate scope */ private _applyPatch(layer: ExpressLayer, layerPath?: string) { const plugin = this; @@ -170,7 +180,13 @@ export class ExpressPlugin extends BasePlugin { [AttributeNames.HTTP_ROUTE]: route.length > 0 ? route : undefined, }; const metadata = getLayerMetadata(layer, layerPath); - + const type = metadata.attributes[ + AttributeNames.EXPRESS_TYPE + ] as ExpressLayerType; + // verify against the config if the layer should be ignored + if (isLayerIgnored(metadata.name, type, plugin._config)) { + return original.apply(this, arguments); + } const span = plugin._tracer.startSpan(metadata.name, { parent: plugin._tracer.getCurrentSpan(), attributes: Object.assign(attributes, metadata.attributes), diff --git a/packages/opentelemetry-plugin-express/src/types.ts b/packages/opentelemetry-plugin-express/src/types.ts index b152a237c25..bcd329c3b6e 100644 --- a/packages/opentelemetry-plugin-express/src/types.ts +++ b/packages/opentelemetry-plugin-express/src/types.ts @@ -16,6 +16,7 @@ import { kLayerPatched } from './express'; import { Request } from 'express'; +import { PluginConfig, Attributes } from '@opentelemetry/types'; export const _MIDDLEWARES_STORE_PROPERTY = '__ot_middlewares'; @@ -45,6 +46,11 @@ export type ExpressLayer = { regexp: RegExp; }; +export type LayerMetadata = { + attributes: Attributes; + name: string; +}; + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls export enum AttributeNames { COMPONENT = 'component', @@ -58,3 +64,15 @@ export enum ExpressLayerType { MIDDLEWARE = 'middleware', REQUEST_HANDLER = 'request_handler', } + +export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); + +/** + * Options available for the Express Plugin (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-express#express-plugin-options)) + */ +export interface ExpressPluginConfig extends PluginConfig { + /** Ingore specific based on their name */ + ignoreLayers?: IgnoreMatcher[]; + /** Ignore specific layers based on their type */ + ignoreLayersType?: ExpressLayerType[]; +} diff --git a/packages/opentelemetry-plugin-express/src/utils.ts b/packages/opentelemetry-plugin-express/src/utils.ts index 0229964bc24..f688c8e409a 100644 --- a/packages/opentelemetry-plugin-express/src/utils.ts +++ b/packages/opentelemetry-plugin-express/src/utils.ts @@ -21,6 +21,8 @@ import { PatchedRequest, _MIDDLEWARES_STORE_PROPERTY, ExpressLayerType, + IgnoreMatcher, + ExpressPluginConfig, } from './types'; /** @@ -62,6 +64,7 @@ export const getLayerMetadata = ( } else if (layer.name === 'bound dispatch') { return { attributes: { + [AttributeNames.EXPRESS_NAME]: layerPath ?? 'request handler', [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.REQUEST_HANDLER, }, name: 'request handler', @@ -99,3 +102,55 @@ export const patchEnd = (span: Span, resultHandler: Function): Function => { return resultHandler.apply(this, args); }; }; + +/** + * Check whether the given obj match pattern + * @param constant e.g URL of request + * @param obj obj to inspect + * @param pattern Match pattern + */ +const satisfiesPattern = ( + constant: string, + pattern: IgnoreMatcher +): boolean => { + if (typeof pattern === 'string') { + return pattern === constant; + } else if (pattern instanceof RegExp) { + return pattern.test(constant); + } else if (typeof pattern === 'function') { + return pattern(constant); + } else { + throw new TypeError('Pattern is in unsupported datatype'); + } +}; + +/** + * Check whether the given request is ignored by configuration + * It will not re-throw exceptions from `list` provided by the client + * @param constant e.g URL of request + * @param [list] List of ignore patterns + * @param [onException] callback for doing something when an exception has + * occurred + */ +export const isLayerIgnored = ( + name: string, + type: ExpressLayerType, + config?: ExpressPluginConfig +): boolean => { + if ( + Array.isArray(config?.ignoreLayersType) && + config?.ignoreLayersType?.includes(type) + ) { + return true; + } + if (Array.isArray(config?.ignoreLayers) === false) return false; + try { + for (const pattern of config!.ignoreLayers!) { + if (satisfiesPattern(name, pattern)) { + return true; + } + } + } catch (e) {} + + return false; +}; diff --git a/packages/opentelemetry-plugin-express/test/express.test.ts b/packages/opentelemetry-plugin-express/test/express.test.ts index 9e715c3fdc8..f691d5d50dd 100644 --- a/packages/opentelemetry-plugin-express/test/express.test.ts +++ b/packages/opentelemetry-plugin-express/test/express.test.ts @@ -25,7 +25,11 @@ import { InMemorySpanExporter, SimpleSpanProcessor, } from '@opentelemetry/tracing'; -import { AttributeNames } from '../src/types'; +import { + AttributeNames, + ExpressPluginConfig, + ExpressLayerType, +} from '../src/types'; const httpRequest = { get: (options: http.ClientRequestArgs | string) => { @@ -58,6 +62,10 @@ describe('Express Plugin', () => { plugin.enable(express, registry, logger); }); + afterEach(() => { + memoryExporter.reset(); + }); + describe('Instrumenting normal get operations', () => { it('should create a child span for middlewares', done => { const rootSpan = tracer.startSpan('rootSpan'); @@ -123,4 +131,76 @@ describe('Express Plugin', () => { }); }); }); + + describe('Instrumenting with specific config', () => { + it('should ignore specific middlewares based on config', done => { + plugin.disable(); + const config: ExpressPluginConfig = { + ignoreLayersType: [ExpressLayerType.MIDDLEWARE], + }; + plugin.enable(express, registry, logger, config); + const rootSpan = tracer.startSpan('rootSpan'); + const app = express(); + app.use(express.json()); + app.use(function customMiddleware(req, res, next) { + for (let i = 0; i < 1000; i++) { + continue; + } + return next(); + }); + const server = http.createServer(app); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + tracer.withSpan(rootSpan, async () => { + await httpRequest.get(`http://localhost:${port}/toto/tata`); + rootSpan.end(); + assert.deepEqual( + memoryExporter + .getFinishedSpans() + .filter( + span => + span.attributes[AttributeNames.EXPRESS_TYPE] === + ExpressLayerType.MIDDLEWARE + ).length, + 0 + ); + let exportedRootSpan = memoryExporter + .getFinishedSpans() + .find(span => span.name === 'rootSpan'); + assert(exportedRootSpan !== undefined); + server.close(); + return done(); + }); + }); + }); + }); + + describe('Disabling plugin', () => { + it('should not create new spans', done => { + plugin.disable(); + const rootSpan = tracer.startSpan('rootSpan'); + const app = express(); + app.use(express.json()); + app.use(function customMiddleware(req, res, next) { + for (let i = 0; i < 1000; i++) { + continue; + } + return next(); + }); + const server = http.createServer(app); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + tracer.withSpan(rootSpan, async () => { + await httpRequest.get(`http://localhost:${port}/toto/tata`); + rootSpan.end(); + assert.deepEqual(memoryExporter.getFinishedSpans().length, 1); + assert(memoryExporter.getFinishedSpans()[0] !== undefined); + server.close(); + return done(); + }); + }); + }); + }); }); diff --git a/packages/opentelemetry-plugin-express/test/utils.test.ts b/packages/opentelemetry-plugin-express/test/utils.test.ts new file mode 100644 index 00000000000..fcc5eed180a --- /dev/null +++ b/packages/opentelemetry-plugin-express/test/utils.test.ts @@ -0,0 +1,147 @@ +/*! + * Copyright 2020, 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 * as utils from '../src/utils'; +import * as assert from 'assert'; +import { + ExpressLayerType, + ExpressPluginConfig, + ExpressLayer, + AttributeNames, +} from '../src/types'; + +describe('Utils', () => { + describe('isLayerIgnored()', () => { + it('should not fail with invalid config', () => { + assert.doesNotThrow(() => + utils.isLayerIgnored('', ExpressLayerType.MIDDLEWARE) + ); + assert.doesNotThrow(() => + utils.isLayerIgnored( + '', + ExpressLayerType.MIDDLEWARE, + {} as ExpressPluginConfig + ) + ); + assert.doesNotThrow(() => + utils.isLayerIgnored('', ExpressLayerType.MIDDLEWARE, { + ignoreLayersType: {}, + } as ExpressPluginConfig) + ); + assert.doesNotThrow(() => + utils.isLayerIgnored('', ExpressLayerType.MIDDLEWARE, { + ignoreLayersType: {}, + ignoreLayers: {}, + } as ExpressPluginConfig) + ); + }); + + it('should ignore based on type', () => { + assert.deepEqual( + utils.isLayerIgnored('', ExpressLayerType.MIDDLEWARE, { + ignoreLayersType: [ExpressLayerType.MIDDLEWARE], + }), + true + ); + assert.deepEqual( + utils.isLayerIgnored('', ExpressLayerType.ROUTER, { + ignoreLayersType: [ExpressLayerType.MIDDLEWARE], + }), + false + ); + }); + + it('should ignore based on the name', () => { + assert.deepEqual( + utils.isLayerIgnored('bodyParser', ExpressLayerType.MIDDLEWARE, { + ignoreLayers: ['bodyParser'], + }), + true + ); + assert.deepEqual( + utils.isLayerIgnored('bodyParser', ExpressLayerType.MIDDLEWARE, { + ignoreLayers: [(name: string) => name === 'bodyParser'], + }), + true + ); + assert.deepEqual( + utils.isLayerIgnored('bodyParser', ExpressLayerType.MIDDLEWARE, { + ignoreLayers: [/bodyParser/], + }), + true + ); + assert.deepEqual( + utils.isLayerIgnored('test', ExpressLayerType.ROUTER, { + ignoreLayers: ['bodyParser'], + }), + false + ); + }); + }); + + describe('getLayerMetadata()', () => { + it('should return router metadata', () => { + assert.deepEqual( + utils.getLayerMetadata( + { + name: 'router', + } as ExpressLayer, + '/test' + ), + { + attributes: { + [AttributeNames.EXPRESS_NAME]: '/test', + [AttributeNames.EXPRESS_TYPE]: 'router', + }, + name: `router - /test`, + } + ); + }); + + it('should return request handler metadata', () => { + assert.deepEqual( + utils.getLayerMetadata( + { + name: 'bound dispatch', + } as ExpressLayer, + '/:id' + ), + { + attributes: { + [AttributeNames.EXPRESS_NAME]: '/:id', + [AttributeNames.EXPRESS_TYPE]: 'request_handler', + }, + name: 'request handler', + } + ); + }); + + it('should return middleware metadata', () => { + assert.deepEqual( + utils.getLayerMetadata({ + name: 'bodyParser', + } as ExpressLayer), + { + attributes: { + [AttributeNames.EXPRESS_NAME]: 'bodyParser', + [AttributeNames.EXPRESS_TYPE]: 'middleware', + }, + name: 'middleware - bodyParser', + } + ); + }); + }); +});