@instana/core
Version:
Core library for Instana's Node.js packages
422 lines (368 loc) • 11.8 kB
JavaScript
/*
* (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.
;
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 || {};