From a7ac489009476c6f3cfdfe1c6f7ad7fedc612b3b Mon Sep 17 00:00:00 2001
From: "Fabio.Gomes" <fabio.gomes@agentifai.com>
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<KoaInstrumentationCo
     // Skip patching layer if its ignored in the config
     if (
       middlewareLayer[kLayerPatched] === true ||
-      isLayerIgnored(layerType, this.getConfig())
+      isLayerTypeIgnored(layerType, this.getConfig())
     )
       return middlewareLayer;
 
@@ -162,6 +166,11 @@ export class KoaInstrumentation extends InstrumentationBase<KoaInstrumentationCo
         isRouter,
         layerPath
       );
+
+      if (isLayerNameIgnored(metadata.name, this.getConfig())) {
+        return middlewareLayer(context, next);
+      }
+
       const span = this.tracer.startSpan(metadata.name, {
         attributes: metadata.attributes,
       });
diff --git a/plugins/node/opentelemetry-instrumentation-koa/src/types.ts b/plugins/node/opentelemetry-instrumentation-koa/src/types.ts
index 494141d0f3..6d0239a790 100644
--- a/plugins/node/opentelemetry-instrumentation-koa/src/types.ts
+++ b/plugins/node/opentelemetry-instrumentation-koa/src/types.ts
@@ -69,9 +69,13 @@ export interface KoaInstrumentationConfig<
 > 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);
+    });
+  });
 });