From c5b8964f817051cfa4e0da9d6feca3c3c40b7342 Mon Sep 17 00:00:00 2001 From: "Fabio.Gomes" Date: Wed, 20 Mar 2024 19:24:27 +0000 Subject: [PATCH] feat(koa): Adds support to ignore a span by its layer name --- .../README.md | 7 + .../src/instrumentation.ts | 13 +- .../src/types.ts | 4 + .../src/utils.ts | 54 +++++++- .../test/koa.test.ts | 2 +- .../test/utils.test.ts | 124 ++++++++++++++++-- 6 files changed, 190 insertions(+), 14 deletions(-) diff --git a/plugins/node/opentelemetry-instrumentation-koa/README.md b/plugins/node/opentelemetry-instrumentation-koa/README.md index 43e717a171..a5023ca32b 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/README.md +++ b/plugins/node/opentelemetry-instrumentation-koa/README.md @@ -54,8 +54,15 @@ Note that generator-based middleware are deprecated and won't be instrumented. | Options | Type | Example | Description | | ------------------ | ----------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------- | | `ignoreLayersType` | `KoaLayerType[]` | `['middleware']` | Ignore layers of specified type. | +| `ignoreLayers` | `IgnoreMatcher[]` | `['logger', /router/]` | Ignore layers with specified names. | | `requestHook` | `KoaRequestCustomAttributeFunction` | `(span, info) => {}` | Function for adding custom attributes to Koa middleware layers. Receives params: `Span, KoaRequestInfo`. | +`ignoreLayers` accepts an array of elements of types: + +- `string` for full match of the path, +- `RegExp` for partial match of the path, +- `function` in the form of `(path) => boolean` for custom logic. + `ignoreLayersType` accepts an array of `KoaLayerType` which can take the following string values: - `router`, diff --git a/plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts index 3d7eb864a4..d4c0b52068 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts @@ -26,7 +26,11 @@ import type * as koa from 'koa'; import { KoaLayerType, KoaInstrumentationConfig } from './types'; /** @knipignore */ import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; -import { getMiddlewareMetadata, isLayerIgnored } from './utils'; +import { + getMiddlewareMetadata, + isLayerNameIgnored, + isLayerTypeIgnored, +} from './utils'; import { getRPCMetadata, RPCType } from '@opentelemetry/core'; import { kLayerPatched, @@ -136,7 +140,7 @@ export class KoaInstrumentation extends InstrumentationBase extends InstrumentationConfig { /** Ignore specific layers based on their type */ ignoreLayersType?: KoaLayerType[]; + /** Ignore specific layers based on their name */ + ignoreLayers?: IgnoreMatcher[]; /** Function for adding custom attributes to each middleware layer span */ requestHook?: KoaRequestCustomAttributeFunction< KoaContextType, KoaMiddlewareType >; } + +export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); diff --git a/plugins/node/opentelemetry-instrumentation-koa/src/utils.ts b/plugins/node/opentelemetry-instrumentation-koa/src/utils.ts index 2bbfd027ef..c30189b614 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/src/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/src/utils.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { KoaLayerType, KoaInstrumentationConfig } from './types'; +import { KoaLayerType, KoaInstrumentationConfig, IgnoreMatcher } from './types'; import { KoaContext, KoaMiddleware } from './internal-types'; import { AttributeNames } from './enums/AttributeNames'; import { Attributes } from '@opentelemetry/api'; @@ -49,12 +49,12 @@ export const getMiddlewareMetadata = ( }; /** - * Check whether the given request is ignored by configuration + * Check whether the given request layer type is ignored by configuration * @param [list] List of ignore patterns * @param [onException] callback for doing something when an exception has * occurred */ -export const isLayerIgnored = ( +export const isLayerTypeIgnored = ( type: KoaLayerType, config?: KoaInstrumentationConfig ): boolean => { @@ -63,3 +63,51 @@ export const isLayerIgnored = ( config?.ignoreLayersType?.includes(type) ); }; + +/** + * Check whether the given request layer name is ignored by configuration + * @param [list] List of ignore patterns + * @param [onException] callback for doing something when an exception has + * occurred + */ +export const isLayerNameIgnored = ( + name: string, + config?: KoaInstrumentationConfig +): boolean => { + if (Array.isArray(config?.ignoreLayers) === false || !config?.ignoreLayers) + return false; + try { + for (const pattern of config.ignoreLayers) { + if (satisfiesPattern(name, pattern)) { + return true; + } + } + } catch (e) { + /* catch block*/ + } + + return false; +}; + +/** + * Check whether the given obj match pattern + * @param constant e.g URL of request + * @param obj obj to inspect + * @param pattern Match pattern + */ +export const satisfiesPattern = ( + constant: string, + pattern: IgnoreMatcher +): boolean => { + console.warn(`constant: ${constant}`); + console.warn(`pattern: ${pattern}`); + 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'); + } +}; diff --git a/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts b/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts index c1b894a026..1be51b580d 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts @@ -750,7 +750,7 @@ describe('Koa Instrumentation', function () { '--experimental-loader=@opentelemetry/instrumentation/hook.mjs', NODE_NO_WARNINGS: '1', }, - checkResult: (err, stdout, stderr) => { + checkResult: (err: any, stdout: any, stderr: any) => { assert.ifError(err); }, checkCollector: (collector: testUtils.TestCollector) => { diff --git a/plugins/node/opentelemetry-instrumentation-koa/test/utils.test.ts b/plugins/node/opentelemetry-instrumentation-koa/test/utils.test.ts index b9a1ab4160..94d86bd362 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/test/utils.test.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/test/utils.test.ts @@ -16,27 +16,34 @@ import * as utils from '../src/utils'; import * as assert from 'assert'; -import { KoaInstrumentationConfig, KoaLayerType } from '../src/types'; +import { + IgnoreMatcher, + KoaInstrumentationConfig, + KoaLayerType, +} from '../src/types'; describe('Utils', () => { - describe('isLayerIgnored()', () => { + describe('isLayerTypeIgnored()', () => { it('should not fail with invalid config', () => { - assert.strictEqual(utils.isLayerIgnored(KoaLayerType.MIDDLEWARE), false); assert.strictEqual( - utils.isLayerIgnored( + utils.isLayerTypeIgnored(KoaLayerType.MIDDLEWARE), + false + ); + assert.strictEqual( + utils.isLayerTypeIgnored( KoaLayerType.MIDDLEWARE, {} as KoaInstrumentationConfig ), false ); assert.strictEqual( - utils.isLayerIgnored(KoaLayerType.MIDDLEWARE, { + utils.isLayerTypeIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: {}, } as KoaInstrumentationConfig), false ); assert.strictEqual( - utils.isLayerIgnored(KoaLayerType.ROUTER, { + utils.isLayerTypeIgnored(KoaLayerType.ROUTER, { ignoreLayersType: {}, } as KoaInstrumentationConfig), false @@ -45,17 +52,118 @@ describe('Utils', () => { it('should ignore based on type', () => { assert.strictEqual( - utils.isLayerIgnored(KoaLayerType.MIDDLEWARE, { + utils.isLayerTypeIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: [KoaLayerType.MIDDLEWARE], }), true ); assert.strictEqual( - utils.isLayerIgnored(KoaLayerType.ROUTER, { + utils.isLayerTypeIgnored(KoaLayerType.ROUTER, { ignoreLayersType: [KoaLayerType.MIDDLEWARE], }), false ); }); }); + describe('isLayerNameIgnored()', () => { + it('should not fail with invalid config', () => { + assert.strictEqual(utils.isLayerNameIgnored('name', {}), false); + assert.strictEqual( + utils.isLayerNameIgnored('name', {} as KoaInstrumentationConfig), + false + ); + assert.strictEqual( + utils.isLayerNameIgnored('name', { + ignoreLayers: {}, + } as KoaInstrumentationConfig), + false + ); + assert.strictEqual(utils.isLayerNameIgnored('name'), false); + }); + + it('should ignore based on name', () => { + assert.strictEqual( + utils.isLayerNameIgnored('logger', { + ignoreLayers: ['logger'], + }), + true + ); + assert.strictEqual( + utils.isLayerNameIgnored('logger', { + ignoreLayers: ['logger'], + }), + true + ); + assert.strictEqual( + utils.isLayerNameIgnored('', { + ignoreLayers: ['logger'], + }), + false + ); + assert.strictEqual( + utils.isLayerNameIgnored('logger - test', { + ignoreLayers: [/logger/], + }), + true + ); + assert.strictEqual( + utils.isLayerNameIgnored('router - test', { + ignoreLayers: [/logger/], + }), + false + ); + assert.strictEqual( + utils.isLayerNameIgnored('test', { + ignoreLayers: [(name: string) => name === 'test'], + }), + true + ); + assert.strictEqual( + utils.isLayerNameIgnored('test', { + ignoreLayers: [(name: string) => name === 'router'], + }), + false + ); + }); + }); +}); + +describe('Utility', () => { + describe('satisfiesPattern()', () => { + it('string pattern', () => { + const answer1 = utils.satisfiesPattern('localhost', 'localhost'); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern('hostname', 'localhost'); + assert.strictEqual(answer2, false); + }); + + it('regex pattern', () => { + const answer1 = utils.satisfiesPattern('LocalHost', /localhost/i); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern('Montreal.ca', /montreal.ca/); + assert.strictEqual(answer2, false); + }); + + it('should throw if type is unknown', () => { + try { + utils.satisfiesPattern('google.com', true as unknown as IgnoreMatcher); + assert.fail(); + } catch (error) { + assert.strictEqual(error instanceof TypeError, true); + } + }); + + it('function pattern', () => { + const answer1 = utils.satisfiesPattern( + 'montreal.ca', + (url: string) => url === 'montreal.ca' + ); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern( + 'montreal.ca', + (url: string) => url !== 'montreal.ca' + ); + assert.strictEqual(answer2, false); + }); + }); });