Skip to content

Commit

Permalink
feat(koa): Adds support to ignore a span by its layer name
Browse files Browse the repository at this point in the history
  • Loading branch information
MrFabio committed Dec 18, 2024
1 parent 3ef5205 commit d04c139
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 14 deletions.
7 changes: 7 additions & 0 deletions plugins/node/opentelemetry-instrumentation-koa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand All @@ -162,6 +166,11 @@ export class KoaInstrumentation extends InstrumentationBase<KoaInstrumentationCo
isRouter,
layerPath
);

if (isLayerNameIgnored(metadata.name, this.getConfig())) {
return middlewareLayer(context, next);

Check warning on line 171 in plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts

View check run for this annotation

Codecov / codecov/patch

plugins/node/opentelemetry-instrumentation-koa/src/instrumentation.ts#L171

Added line #L171 was not covered by tests
}

const span = this.tracer.startSpan(metadata.name, {
attributes: metadata.attributes,
});
Expand Down
4 changes: 4 additions & 0 deletions plugins/node/opentelemetry-instrumentation-koa/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
54 changes: 51 additions & 3 deletions plugins/node/opentelemetry-instrumentation-koa/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 => {
Expand All @@ -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');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
124 changes: 116 additions & 8 deletions plugins/node/opentelemetry-instrumentation-koa/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
});
});
});

0 comments on commit d04c139

Please sign in to comment.