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',
+ }
+ );
+ });
+ });
+});