@opentelemetry/context-zone-peer-dep
Version:
OpenTelemetry Context Zone with peer dependency for zone.js
211 lines • 7.89 kB
JavaScript
/*
* Copyright The 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.
*/
/// <reference types="zone.js" />
import { ROOT_CONTEXT } from '@opentelemetry/api';
import { isListenerObject } from './util';
/* Key name to be used to save a context reference in Zone */
const ZONE_CONTEXT_KEY = 'OT_ZONE_CONTEXT';
/**
* ZoneContextManager
* This module provides an easy functionality for tracing action between asynchronous operations in web.
* It was not possible with standard [StackContextManager]{@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-web/src/StackContextManager.ts}.
* It heavily depends on [zone.js]{@link https://www.npmjs.com/package/zone.js}.
* It stores the information about context in zone. Each Context will have always new Zone;
* It also supports binding a certain Span to a target that has "addEventListener" and "removeEventListener".
* When this happens a new zone is being created and the provided Span is being assigned to this zone.
*/
export class ZoneContextManager {
/**
* whether the context manager is enabled or not
*/
_enabled = false;
/**
* Helps to create a unique name for the zones - part of zone name
*/
_zoneCounter = 0;
/**
* Returns the active context from certain zone name
* @param activeZone
*/
_activeContextFromZone(activeZone) {
return (activeZone && activeZone.get(ZONE_CONTEXT_KEY)) || ROOT_CONTEXT;
}
/**
* @param context A context (span) to be executed within target function
* @param target Function to be executed within the context
*/
// eslint-disable-next-line @typescript-eslint/ban-types
_bindFunction(context, target) {
const manager = this;
const contextWrapper = function (...args) {
return manager.with(context, () => target.apply(this, args));
};
Object.defineProperty(contextWrapper, 'length', {
enumerable: false,
configurable: true,
writable: false,
value: target.length,
});
return contextWrapper;
}
/**
* @param context A context (span) to be bind to target
* @param obj target object on which the listeners will be patched
*/
_bindListener(context, obj) {
const target = obj;
if (target.__ot_listeners !== undefined) {
return obj;
}
target.__ot_listeners = {};
if (typeof target.addEventListener === 'function') {
target.addEventListener = this._patchAddEventListener(target, target.addEventListener, context);
}
if (typeof target.removeEventListener === 'function') {
target.removeEventListener = this._patchRemoveEventListener(target, target.removeEventListener);
}
return obj;
}
/**
* Creates a new unique zone name
*/
_createZoneName() {
this._zoneCounter++;
const random = Math.random();
return `${this._zoneCounter}-${random}`;
}
/**
* Creates a new zone
* @param zoneName zone name
* @param context A context (span) to be bind with Zone
*/
_createZone(zoneName, context) {
return Zone.current.fork({
name: zoneName,
properties: {
[ZONE_CONTEXT_KEY]: context,
},
});
}
/**
* Returns the active zone
*/
_getActiveZone() {
return Zone.current;
}
/**
* Patches addEventListener method
* @param target any target that has "addEventListener" method
* @param original reference to the patched method
* @param [context] context to be bind to the listener
*/
_patchAddEventListener(target, original, context) {
const contextManager = this;
return function (event, listener, opts) {
if (target.__ot_listeners === undefined) {
target.__ot_listeners = {};
}
let listeners = target.__ot_listeners[event];
if (listeners === undefined) {
listeners = new WeakMap();
target.__ot_listeners[event] = listeners;
}
const patchedListener = contextManager.bind(context, listener);
// store a weak reference of the user listener to ours
listeners.set(listener, patchedListener);
return original.call(this, event, patchedListener, opts);
};
}
/**
* Patches removeEventListener method
* @param target any target that has "removeEventListener" method
* @param original reference to the patched method
*/
_patchRemoveEventListener(target, original) {
return function (event, listener) {
if (target.__ot_listeners === undefined ||
target.__ot_listeners[event] === undefined) {
return original.call(this, event, listener);
}
const events = target.__ot_listeners[event];
const patchedListener = events.get(listener);
events.delete(listener);
return original.call(this, event, patchedListener || listener);
};
}
/**
* Returns the active context
*/
active() {
if (!this._enabled) {
return ROOT_CONTEXT;
}
const activeZone = this._getActiveZone();
const active = this._activeContextFromZone(activeZone);
if (active) {
return active;
}
return ROOT_CONTEXT;
}
/**
* Binds a the certain context or the active one to the target function and then returns the target
* @param context A context (span) to be bind to target
* @param target a function or event emitter. When target or one of its callbacks is called,
* the provided context will be used as the active context for the duration of the call.
*/
bind(context, target) {
// if no specific context to propagate is given, we use the current one
if (context === undefined) {
context = this.active();
}
if (typeof target === 'function') {
return this._bindFunction(context, target);
}
else if (isListenerObject(target)) {
this._bindListener(context, target);
}
return target;
}
/**
* Disable the context manager (clears all the contexts)
*/
disable() {
this._enabled = false;
return this;
}
/**
* Enables the context manager and creates a default(root) context
*/
enable() {
this._enabled = true;
return this;
}
/**
* Calls the callback function [fn] with the provided [context].
* If [context] is undefined then it will use the active context.
* The context will be set as active
* @param context A context (span) to be called with provided callback
* @param fn Callback function
* @param thisArg optional receiver to be used for calling fn
* @param args optional arguments forwarded to fn
*/
with(context, fn, thisArg, ...args) {
const zoneName = this._createZoneName();
const newZone = this._createZone(zoneName, context);
return newZone.run(fn, thisArg, args);
}
}
//# sourceMappingURL=ZoneContextManager.js.map