@instana/core
Version:
Core library for Instana's Node.js packages
396 lines (351 loc) • 11.6 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2019
*/
;
const deepMerge = require('../../util/deepMerge');
const tracingUtil = require('../tracingUtil');
const constants = require('../constants');
/** @typedef {import('../../core').InstanaBaseSpan} InstanaBaseSpan */
let isActive = false;
/**
* @param {boolean} isCallbackApi
*/
exports.generate = function (isCallbackApi) {
/** @type {import('../../core').GenericLogger} */
let logger;
/** @type {import('../cls')} */
let cls = null;
/** @type {Function} */
let runInContext = null;
/**
* Suppression for SDK entry spans do not have a use case right now.
*
* The known use case for SDK entry spans are:
* - receive a message from any external source (which is not traced e.g. IPC)
* and you start a span for that.
* - a worker who executes a task in a loop and you want to trace the task.
*
* Sampling could be added for SDK entry spans in the future.
*
* @param {string} name
* @param {Object.<string, *> | Function} tags
* @param {Object.<string, *> | Function | string} traceId
* @param {string} parentSpanId
* @param {Function | Object.<string, *>} callback
*/
function startEntrySpan(name, tags, traceId, parentSpanId, callback) {
if (isCallbackApi && arguments.length === 2 && typeof arguments[1] === 'function') {
callback = tags;
tags = traceId = parentSpanId = null;
} else if (isCallbackApi && arguments.length === 3 && typeof arguments[2] === 'function') {
callback = /** @type {Function} */ (traceId);
traceId = parentSpanId = null;
}
if (!isActive) {
return callNext(/** @type {Function} */ (callback));
}
const parentSpan = cls.getCurrentSpan();
if (parentSpan) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot start an entry span (${name}) when another span is already active. Currently, the following span is active: ${JSON.stringify(
parentSpan
)}`
);
return callNext(/** @type {Function} */ (callback));
}
return startSdkSpan(
name,
constants.ENTRY,
constants.SDK.ENTRY,
true,
startEntrySpan,
/** @type {Object.<string, *> | null} */ (tags),
/** @type {string} */ (traceId),
parentSpanId,
/** @type {Function} */ (callback)
);
}
/**
* @param {Error} error
* @param {Object.<string, *>} tags
*/
function completeEntrySpan(error, tags) {
if (!isActive) {
return;
}
const span = cls.getCurrentSpan();
if (!span) {
logger.warn(
// eslint-disable-next-line max-len
'Cannot complete an entry span as this requires an entry span to be currently active. Currently there is no span active at all.'
);
return;
}
if (!constants.isEntrySpan(span)) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot complete an entry span as this requires an entry span to be currently active. But the currently active span is not an entry span: ${JSON.stringify(
span
)}`
);
return;
}
completeSpan(error, span, tags);
}
/**
* @param {string} name
* @param {Object.<string, *>} tags
* @param {Function | Object.<string, *>} callback
* @returns {Function | Promise<*>}
*/
function startIntermediateSpan(name, tags, callback) {
if (isCallbackApi && arguments.length === 2 && typeof arguments[1] === 'function') {
callback = tags;
tags = null;
}
if (!isActive) {
return callNext(/** @type {Function} */ (callback));
}
const parentSpan = cls.getCurrentSpan();
if (!parentSpan) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot start an intermediate span (${name}) as this requires an active entry (or intermediate) span as parent. Currently there is no span active at all.`
);
return callNext(/** @type {Function} */ (callback));
}
if (constants.isExitSpan(parentSpan)) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot start an intermediate span (${name}) as this requires an active entry (or intermediate) span as parent. But the currently active span is an exit span: ${JSON.stringify(
parentSpan
)}`
);
return callNext(/** @type {Function} */ (callback));
}
return startSdkSpan(
name,
constants.INTERMEDIATE,
constants.SDK.INTERMEDIATE,
false,
startIntermediateSpan,
tags,
null,
null,
/** @type {Function} */ (callback)
);
}
/**
* @param {Error} error
* @param {Object.<string, *>} tags
* @param {InstanaBaseSpan} spanToBeCompleted
*/
function completeIntermediateSpan(error, tags, spanToBeCompleted) {
if (!isActive) {
return;
}
const span = spanToBeCompleted || cls.getCurrentSpan();
if (!span) {
logger.warn(
// eslint-disable-next-line max-len
'Cannot complete an intermediate span as this requires an intermediate span to be currently active. Currently there is no span active at all.'
);
return;
}
if (!constants.isIntermediateSpan(span)) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot complete an intermediate span as this requires an intermediate span to be currently active. But the currently active span is not an intermediate span: ${JSON.stringify(
span
)}`
);
return;
}
completeSpan(error, span, tags);
}
/**
* @param {string} name
* @param {Object.<string, *>} tags
* @param {Function | Object.<string, *>} callback
* @returns {Function | Promise<*>}
*/
function startExitSpan(name, tags, callback) {
if (isCallbackApi && arguments.length === 2 && typeof arguments[1] === 'function') {
callback = tags;
tags = null;
}
if (cls === null) {
return callNext(/** @type {Function} */ (callback));
}
if (cls.skipExitTracing({ isActive })) {
return callNext(/** @type {Function} */ (callback));
}
return startSdkSpan(
name,
constants.EXIT,
constants.SDK.EXIT,
false,
startExitSpan,
tags,
null,
null,
/** @type {Function} */ (callback)
);
}
/**
* @param {Error} error
* @param {Object.<string, *>} tags
*/
function completeExitSpan(error, tags) {
if (!isActive) {
return;
}
const span = cls.getCurrentSpan();
if (!span) {
logger.warn(
// eslint-disable-next-line max-len
'Cannot complete an exit span as this requires an exit span to be currently active. Currently there is no span active at all.'
);
return;
}
if (!constants.isExitSpan(span)) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot complete an exit span as this requires an exit span to be currently active. But the currently active span is not an exit span: ${JSON.stringify(
span
)}`
);
return;
}
completeSpan(error, span, tags);
}
/**
* @param {string} name
* @param {number} kind
* @param {string} sdkKind
* @param {boolean} forceRootContext
* @param {Function} stackTraceRef
* @param {Object.<string, *>} tags
* @param {string} traceId
* @param {string} parentSpanId
* @param {Function} callback
* @returns {Function | Promise<*>}
*/
function startSdkSpan(name, kind, sdkKind, forceRootContext, stackTraceRef, tags, traceId, parentSpanId, callback) {
const context = forceRootContext
? // SDK entry spans: Force ALS/CLS to use a new, separate root context. There is nothing we ever want to fetch
// from a parent/ancestor context for SDK entry spans. This avoids a memory leak when client code is calling
// sdk.startEntrySpan recursively in the same context over and over again, which creates an evergrowing chain
// of contexts, linked by prototypical inheritance.
cls.ns.createRootContext()
: // Let ALS/CLS create a new child context of the currently active context. This is the correct behavior for
// intermediate and exit spans. We need them to happen in a child/descendant context of the currently active
// entry span.
null;
return runInContext(() => {
const span = cls.startSpan({
spanName: 'sdk',
kind: kind,
traceId: traceId,
parentSpanId: parentSpanId
});
span.stack = tracingUtil.getStackTrace(stackTraceRef);
span.data.sdk = {
name,
type: sdkKind
};
if (tags) {
// We make a copy of the tags object to assure that it is extensible.
// As the tags object is provided by users, they could have been made not extensible with
// Object.freeze or Object.preventExtensions
span.data.sdk.custom = { tags: Object.assign({}, tags) };
}
return callNext(callback);
// The additional argument `context` for runInContext (cls.ns.runAndReturn or cls.ns.runPromise, depending on the
// API type) allows us to force a specific context to be used by those functions. We use a new root context for
// SDK entry spans and null otherwise (intermediate spans, exit spans).
}, context);
}
/**
* @param {Error} error
* @param {import('../../core').InstanaBaseSpan} span
* @param {Object.<string, *>} tags
*/
function completeSpan(error, span, tags) {
if (!span.data.sdk) {
logger.warn(
// eslint-disable-next-line max-len
`Cannot complete an SDK span. The currently active span is not an SDK span, so there seems to be a mismatch in the trace context. This is the currently active span: ${JSON.stringify(
span
)}`
);
return;
}
if (error) {
span.ec = 1;
if (!span.data.sdk.custom) {
span.data.sdk.custom = {
tags: {}
};
}
if (!span.data.sdk.custom.tags) {
span.data.sdk.custom.tags = {};
}
if (span.data.sdk.custom.tags.message == null) {
span.data.sdk.custom.tags.message = tracingUtil.getErrorDetails(error);
}
}
if (span.data.sdk.custom && tags) {
span.data.sdk.custom.tags = deepMerge(span.data.sdk.custom.tags, tags);
} else if (tags) {
span.data.sdk.custom = { tags };
}
span.d = Date.now() - span.ts;
span.transmit();
}
/**
* @param {import('events').EventEmitter} emitter
*/
function bindEmitter(emitter) {
if (isActive) {
cls.ns.bindEmitter(emitter);
}
}
/**
* @param {Function} callback
* @returns {Function | Promise<*>}
*/
function callNext(callback) {
const span = cls ? cls.getCurrentSpan() : null;
return isCallbackApi ? callback(span) : Promise.resolve(span);
}
/**
* @param {import('../../config').InstanaConfig} config
* @param {import('../cls')} _cls
*/
function init(config, _cls) {
logger = config.logger;
cls = _cls;
runInContext = isCallbackApi ? cls.ns.runAndReturn.bind(cls.ns) : cls.ns.runPromise.bind(cls.ns);
}
function activate() {
isActive = true;
}
function deactivate() {
isActive = true;
}
return {
startEntrySpan,
completeEntrySpan,
startIntermediateSpan,
completeIntermediateSpan,
startExitSpan,
completeExitSpan,
bindEmitter,
init,
activate,
deactivate
};
};