UNPKG

@instana/core

Version:
422 lines (368 loc) 11.8 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2018 */ /* eslint-env es6 */ /* eslint-disable */ // This evolved from a copy of // Jeff-Lewis, fix(destroy): destroy based on asyncId only (3cf7539 on Jul 25, 2017) // https://github.com/Jeff-Lewis/cls-hooked/blob/066c6c4027a7924b06997cc6b175b1841342abdc/context.js // Changes: // - rename the symbols to avoid name clashes // - have Namespace.prototype.set return a function which can be used to unset the value from the context // on which it was originally set. 'use strict'; const util = require('util'); const assert = require('assert'); // @ts-ignore const wrapEmitter = require('./emitter-listener'); const async_hooks = require('async_hooks'); const unset = require('./unset'); const CONTEXTS_SYMBOL = 'instanaClsHooked@contexts'; let currentUid = -1; module.exports = { getNamespace, createNamespace, destroyNamespace, reset }; /** * @typedef {Object.<string, *>} InstanaCLSContext */ /** * Creates a new CLS namespace. */ /** * @param {string} name */ function Namespace(name) { this.name = name; // changed in 2.7: no default context this.active = null; /** @type {Array.<InstanaCLSContext>} */ this._set = []; this.id = null; this._contexts = new Map(); this._indent = 0; } /** * Sets a key/value pair in the current CLS context. It can be retrieved later, but only from the same context * or a child context. * @param {string} key * @param {*} value * @returns */ Namespace.prototype.set = function set(key, value) { if (!this.active) { throw new Error('No context available. ns.run() or ns.bind() must be called first.'); } const context = this.active; context[key] = value; return unset.bind(null, context, key, value); }; /** * Retrieves a value by key from the current CLS context (or a parent context), assuming the key/value pair has * been set earlier in this context. * @param {string} key */ Namespace.prototype.get = function get(key) { if (!this.active) { return undefined; } return this.active[key]; }; /** * Creates a new CLS context in this namespace as a child of the currently active context (or as a root context if no * context is currently active). */ Namespace.prototype.createContext = function createContext() { // Prototype inherit existing context if created a new child context within existing context. let context = Object.create(this.active ? this.active : Object.prototype); context._ns_name = this.name; context.id = currentUid; return context; }; /** * Creates a new root CLS context in this namespace, independent of the currently active context. */ Namespace.prototype.createRootContext = function createRootContext() { let context = Object.create(Object.prototype); context._ns_name = this.name; context.id = currentUid; return context; }; /** * Runs a function in a new CLS context. The context is left after the function terminates. Asynchronous work * started in this function will happen in that new context. The return value from that function (if any) is discarded. * If you aren't 100% certain that the function never returns a value or that client code never relies on that value, * use runAndReturn instead. * @param {Function} fn * @param {InstanaCLSContext} ctx */ Namespace.prototype.run = function run(fn, ctx) { let context = ctx || this.createContext(); this.enter(context); try { fn(context); return context; } finally { this.exit(context); } }; /** * Runs a function in a new CLS context and returns its return value. The context is left after the function * terminates. Asynchronous work started in this function will happen in that new context. * @param {Function} fn * @param {InstanaCLSContext} ctx */ Namespace.prototype.runAndReturn = function runAndReturn(fn, ctx) { let value; this.run((/** @type {InstanaCLSContext} */ context) => { value = fn(context); }, ctx); return value; }; /** * Runs a function which returns a promise in a new CLS context and returns said promise. The context is left * as soon as the the promise resolves/is rejected. Asynchronous work started in this promise will happen in that new * context. * * If the given function does not create a then-able, an error will be thrown. * * This function assumes that the returned promise is CLS-friendly or wrapped already. * @param {Function} fn * @param {InstanaCLSContext} ctx */ Namespace.prototype.runPromise = function runPromise(fn, ctx) { let context = ctx || this.createContext(); this.enter(context); let promise = fn(context); if (!promise || !promise.then || !promise.catch) { throw new Error('fn must return a promise.'); } return promise .then((/** @type {*} */ result) => { this.exit(context); return result; }) .catch((/** @type {*} */ err) => { this.exit(context); throw err; }); }; /** * Runs a function (which might or might not return a promise) in a new CLS context. If the given function indeed * returns a then-able, this behaves like runPromise. If not, this behaves like runAndReturn. In particular, no error is * thrown if the given function does not return a promise. * * This function assumes that the returned promise (if any) is CLS-friendly or wrapped already. * @param {Function} fn * @param {InstanaCLSContext} ctx */ Namespace.prototype.runPromiseOrRunAndReturn = function runPromiseOrRunAndReturn(fn, ctx) { let isPromise = false; let valueOrPromise; const context = ctx || this.createContext(); this.enter(context); try { valueOrPromise = fn(context); isPromise = valueOrPromise && valueOrPromise.then && valueOrPromise.catch; if (isPromise) { // fn returned a promise, so we behave like this.runPromise. return valueOrPromise .then((/** @type {*} */ result) => { this.exit(context); return result; }) .catch((/** @type {*} */ err) => { this.exit(context); throw err; }); } } finally { if (!isPromise) { // fn did not return a promise, so we behave like this.runAndReturn. this.exit(context); } } return valueOrPromise; }; /** * Returns a wrapper around the given function which will enter CLS context which is active at the time of calling bind * and leave that context once the function terminates. If no context is active, a new context will be created. * @param {Function} fn * @param {InstanaCLSContext} context */ Namespace.prototype.bind = function bind(fn, context) { if (!context) { if (!this.active) { context = this.createContext(); } else { context = this.active; } } let self = this; return function clsBind() { self.enter(context); try { return fn.apply(this, arguments); } finally { self.exit(context); } }; }; /** * Binds the given emitter to the currently active CLS context. Work triggered by an emit from that emitter will happen * in that CLS context. * @param {import('events').EventEmitter} emitter */ Namespace.prototype.bindEmitter = function bindEmitter(emitter) { assert.ok(emitter.on && emitter.addListener && emitter.emit, 'can only bind real EEs'); let namespace = this; let thisSymbol = `context@${this.name}`; /** * Capture the context active at the time the emitter is bound. * @param {*} listener */ function attach(listener) { if (!listener) { return; } if (!listener[CONTEXTS_SYMBOL]) { listener[CONTEXTS_SYMBOL] = Object.create(null); } listener[CONTEXTS_SYMBOL][thisSymbol] = { namespace, context: namespace.active }; } /** * At emit time, bind the listener within the correct context. * @param {*} unwrapped */ function bind(unwrapped) { if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { return unwrapped; } let wrapped = unwrapped; let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL]; Object.keys(unwrappedContexts).forEach(name => { let thunk = unwrappedContexts[name]; wrapped = thunk.namespace.bind(wrapped, thunk.context); }); return wrapped; } wrapEmitter(emitter, attach, bind); }; /** * @param {InstanaCLSContext} context */ Namespace.prototype.enter = function enter(context) { assert.ok(context, 'context must be provided for entering'); this._set.push(this.active); this.active = context; }; /** * @param {InstanaCLSContext} context */ Namespace.prototype.exit = function exit(context) { assert.ok(context, 'context must be provided for exiting'); // Fast path for most exits that are at the top of the stack if (this.active === context) { assert.ok(this._set.length, "can't remove top context"); this.active = this._set.pop(); return; } // Fast search in the stack using lastIndexOf let index = this._set.lastIndexOf(context); if (index < 0) { assert.ok( index >= 0, `context not currently entered; can't exit. \n${util.inspect(this)}\n${util.inspect(context)}` ); } else { assert.ok(index, "can't remove top context"); this._set.splice(index, 1); } }; /** * @param {string} name */ function getNamespace(name) { // @ts-ignore return process.instanaNamespaces[name]; } /** * @param {string} name */ function createNamespace(name) { assert.ok(name, 'namespace must be given a name.'); let namespace = new Namespace(name); namespace.id = currentUid; const hook = async_hooks.createHook({ init(asyncId, _type, _triggerId, _resource) { currentUid = async_hooks.executionAsyncId(); if (namespace.active) { namespace._contexts.set(asyncId, namespace.active); } else if (currentUid === 0) { // CurrentId will be 0 when triggered from C++. Promise events // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid const triggerId = async_hooks.triggerAsyncId(); const triggerIdContext = namespace._contexts.get(triggerId); if (triggerIdContext) { namespace._contexts.set(asyncId, triggerIdContext); } } }, before(asyncId) { currentUid = async_hooks.executionAsyncId(); let context; //HACK to work with promises until they are fixed in node > 8.1.1 context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid); if (context) { namespace.enter(context); } }, after(asyncId) { currentUid = async_hooks.executionAsyncId(); let context; //HACK to work with promises until they are fixed in node > 8.1.1 context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid); if (context) { namespace.exit(context); } }, destroy(asyncId) { currentUid = async_hooks.executionAsyncId(); namespace._contexts.delete(asyncId); } }); hook.enable(); // @ts-ignore process.instanaNamespaces[name] = namespace; return namespace; } /** * @param {string} name */ function destroyNamespace(name) { let namespace = getNamespace(name); assert.ok(namespace, `can't delete nonexistent namespace! "${name}"`); assert.ok(namespace.id, `don't assign to process.instanaNamespaces directly! ${util.inspect(namespace)}`); // @ts-ignore process.instanaNamespaces[name] = null; } function reset() { // must unregister async listeners // @ts-ignore if (process.instanaNamespaces) { // @ts-ignore Object.keys(process.instanaNamespaces).forEach(name => { destroyNamespace(name); }); } // @ts-ignore process.instanaNamespaces = Object.create(null); } // @ts-ignore process.instanaNamespaces = process.instanaNamespaces || {};