@instana/core
Version:
Core library for Instana's Node.js packages
466 lines (408 loc) • 14.4 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2018
*/
;
const constants = require('./constants');
/** @type {import('../core').GenericLogger} */
let logger;
/**
* Provides very limited access from client code to the current active span.
* @param {import('../core').InstanaBaseSpan} _span
*/
function SpanHandle(_span) {
this.span = _span;
}
/**
* Returns the trace ID of the span.
*/
SpanHandle.prototype.getTraceId = function getTraceId() {
return this.span.t;
};
/**
* Returns the span ID of the span.
*/
SpanHandle.prototype.getSpanId = function getSpanId() {
return this.span.s;
};
/**
* Returns the parent span ID of the span.
*/
SpanHandle.prototype.getParentSpanId = function getParentSpanId() {
return this.span.p;
};
/**
* Returns the name of the span.
*/
SpanHandle.prototype.getName = function getName() {
return this.span.n;
};
/*
* Determine if the span is an entry span (server span).
*/
SpanHandle.prototype.isEntrySpan = function isEntrySpan() {
return constants.isEntrySpan(this.span);
};
/*
* Determine if the span is an exit span (client span).
*/
SpanHandle.prototype.isExitSpan = function isExitSpan() {
return constants.isExitSpan(this.span);
};
/*
* Determine if the span is an intermediate span (local span).
*/
SpanHandle.prototype.isIntermediateSpan = function isIntermediateSpan() {
return constants.isIntermediateSpan(this.span);
};
/**
* Returns the timestamp of the span's start.
*/
SpanHandle.prototype.getTimestamp = function getTimestamp() {
return this.span.ts;
};
/**
* Returns the duration of the span. This method will return 0 if the span has not been completed yet.
*/
SpanHandle.prototype.getDuration = function getDuration() {
return this.span.d;
};
/**
* Returns the error count of the span. This method will usually return 0 if the span has not been completed yet.
*/
SpanHandle.prototype.getErrorCount = function getErrorCount() {
return this.span.ec;
};
/**
* Returns the correlation ID for end user monitoring.
*/
SpanHandle.prototype.getCorrelationId = function getCorrelationId() {
return this.span.crid;
};
/**
* Sets the correlation ID for end user monitoring. This method call is only effective for root entry spans, it will be
* silently ignored otherwise.
*
* @param {string} correlationId the correlation ID
*/
SpanHandle.prototype.setCorrelationId = function setCorrelationId(correlationId) {
if (this.isEntrySpan() && !this.getParentSpanId()) {
this.span.crid = correlationId;
}
};
/**
* Returns the correlation type for end user monitoring.
*/
SpanHandle.prototype.getCorrelationType = function getCorrelationType() {
return this.span.crtp;
};
/**
* Sets the correlation type for end user monitoring. This method call is only effective for root entry spans, it will
* be silently ignored otherwise.
*
* @param {string} correlationType the correlation type, either 'web' or 'mobile'
*/
SpanHandle.prototype.setCorrelationType = function setCorrelationType(correlationType) {
if (this.isEntrySpan() && !this.getParentSpanId()) {
this.span.crtp = correlationType;
}
};
/**
* Adds an annotation (also known as a tag or custom tag) to the span. The path can be provided as a dot-separated
* string or as an array of strings. That is, the following two calls are equivalent:
* - span.annotate('sdk.custom.tags.myTag', 'My Value'), and
* - span.annotate(['sdk', 'custom', 'tags', 'myTag'], 'My Value').
*
* Note that custom tags should always be prefixed by sdk.custom.tags. You can also use this method to override standard
* tags, like the HTTP path template (example: span.annotate('http.path_tpl', '/user/{id}/details')), but it is not
* recommended, unless there are very good reasons to interfere with Instana's auto tracing.
*
* @param {string|Array.<string>} path the path of the annotation in the span object
* @param {*} value the value for the annotation
*/
SpanHandle.prototype.annotate = function annotate(path, value) {
if (path == null) {
return;
}
if (typeof path === 'string') {
_annotateWithString(this.span.data, path, value);
if (path === 'http.path_tpl') {
this.span.freezePathTemplate();
}
} else if (Array.isArray(path)) {
_annotateWithArray(this.span.data, path, value);
if (path[0] === 'http' && path[1] === 'path_tpl') {
this.span.freezePathTemplate();
}
}
};
/**
* @param {Object.<string, *>} target
* @param {string} path
* @param {*} value
*/
function _annotateWithString(target, path, value) {
// remove trailing dots first
if (path.charAt(path.length - 1) === '.') {
_annotateWithString(target, path.substring(0, path.length - 1), value);
return;
}
const idx = path.indexOf('.');
if (idx === 0) {
// key with leading "."
_annotateWithString(target, path.substring(1), value);
} else if (idx >= 1) {
const head = path.substring(0, idx);
const tail = path.substring(idx + 1);
let nestedTarget = target[head];
if (nestedTarget == null || typeof nestedTarget !== 'object') {
target[head] = nestedTarget = {};
}
_annotateWithString(nestedTarget, tail, value);
} else {
target[path] = value;
}
}
/**
* @param {Object.<string, *>} target
* @param {Array.<string>} path
* @param {*} value
*/
function _annotateWithArray(target, path, value) {
if (path.length === 0) {
// eslint-disable-next-line no-useless-return
return;
} else if (path.length === 1) {
_annotateWithString(target, path[0], value);
} else {
const head = path[0];
const tail = path.slice(1);
let nestedTarget = target[head];
if (nestedTarget == null || typeof nestedTarget !== 'object') {
target[head] = nestedTarget = {};
}
_annotateWithArray(nestedTarget, tail, value);
}
}
/**
* Marks the span as an error (that is, it sets the error count for the span to 1). You can optionally provide an error
* message. If no message is provided, a default error message will be set.
*
* @param {string?} errorMessage the error message to add as an annotation
* @param {string|Array.<string>} errorMessagePath the annotation path where the error message will be written to;
* there is usually no need to provide this argument as this will be handled automatically
*/
SpanHandle.prototype.markAsErroneous = function markAsErroneous(
errorMessage = 'This call has been marked as erroneous via the Instana Node.js SDK, no error message has been ' +
'supplied.',
errorMessagePath
) {
// We deliberately write directly to the protected property span._ec instead of span.ec here to circumvent the write
// protection established by ecHasBeenSetManually. A direct manual write via the SDK always has priority over anything
// else that has potentially written before (even other earlier markAsErroneous/markAsNonErroneous calls).
this.span._ec = 1;
this.span.ecHasBeenSetManually = true;
this._annotateErrorMessage(errorMessage, errorMessagePath);
};
/**
* Marks the span as being not an error (that is, it sets the error count for the span to 0). This is useful if the span
* has been marked erroneous previously (either by autotracing or via span.markAsErroneous) and that earlier decision
* needs to be reverted.
* @param {string|Array.<string>} errorMessagePath the annotation path where the error message has been written to
* earlier; there is usually no need to provide this argument as this will be handled automatically
*/
SpanHandle.prototype.markAsNonErroneous = function markAsNonErroneous(errorMessagePath) {
// We deliberately write directly to the protected property span._ec instead of span.ec here to circumvent the write
// protection established by ecHasBeenSetManually. A direct manual write via the SDK always has priority over anything
// else that has potentially written before (even other earlier markAsErroneous/markAsNonErroneous calls).
this.span._ec = 0;
this.span.ecHasBeenSetManually = true;
// reset the error message as well
this._annotateErrorMessage(undefined, errorMessagePath);
};
/**
* @param {string?} errorMessage
* @param {string|Array.<string>} errorMessagePath
*/
SpanHandle.prototype._annotateErrorMessage = function _annotateErrorMessage(errorMessage, errorMessagePath) {
if (errorMessagePath) {
this.annotate(errorMessagePath, errorMessage);
} else {
findAndAnnotateErrorMessage(this.span, errorMessage);
}
};
/**
* @param {import('../core').InstanaBaseSpan} span
* @param {string|Array.<string>} message
*/
function findAndAnnotateErrorMessage(span, message) {
const data = span.data;
if (!data) {
logger.warn(
'The error message annotation cannot be set in span.markAsErroneous/span.markAsNonErroneous, since the ' +
`${span.n} span has no data object.`
);
return;
}
let potentialSpanTypeSpecificDataKeys = Object.keys(data).filter(
key =>
// Some db instrumentations add a span.data.peer object in addition to their main section.
key !== 'peer' &&
// We are only interested in actual object properties, not string properties like span.data.service etc.
data[key] != null &&
typeof data[key] === 'object' &&
!Array.isArray(data[key])
);
if (potentialSpanTypeSpecificDataKeys.length === 0) {
logger.warn(
'The error message annotation cannot be set in span.markAsErroneous/span.markAsNonErroneous, since the ' +
`data object of the ${span.n} span has no keys. Please provide the path to the error message annotation ` +
'explicitly.'
);
return;
}
if (potentialSpanTypeSpecificDataKeys.length > 1 && potentialSpanTypeSpecificDataKeys.includes('sdk')) {
// Example: span.data.(http|rpc|mysql|whatever) _and_ span.data.sdk.custom.tags can legitimately exist on the same
// span when custom annotations have been added to an autotrace span.
// In that case, we want to add the error message to the autotrace span data.
potentialSpanTypeSpecificDataKeys = potentialSpanTypeSpecificDataKeys.filter(key => key !== 'sdk');
}
if (potentialSpanTypeSpecificDataKeys.length > 1) {
logger.warn(
'The error message annotation cannot be set in span.markAsErroneous/span.markAsNonErroneous, since the ' +
`data object of the ${span.n} span has more than one key: ${potentialSpanTypeSpecificDataKeys.join(
', '
)}. Please provide the path to the error message annotation explicitly.`
);
return;
}
const spanTypeSpecificData = data[potentialSpanTypeSpecificDataKeys[0]];
if (message != null) {
spanTypeSpecificData.error = message;
} else {
delete spanTypeSpecificData.error;
}
}
/**
* Switches the span into manual-end-mode. Calls to span#transmit() as used by automatic tracing instrumentation will be
* ignored. Instead, client code needs to finish the span (and trigger transmission) by calling spanHandle#end();
*/
SpanHandle.prototype.disableAutoEnd = function disableAutoEnd() {
this.span.disableAutoEnd();
};
/**
* Finishes a span that has been switched to manual-end-mode before.
* @param {boolean | number} errorCount
*/
SpanHandle.prototype.end = function end(errorCount) {
if (this.span.ts) {
this.span.d = Date.now() - this.span.ts;
}
if (errorCount === true) {
errorCount = 1;
}
if (typeof errorCount === 'number') {
this.span.ec = errorCount;
}
this.span.transmitManual();
};
/**
* Cancels as span that has been switched to manual-end-mode before.
*/
SpanHandle.prototype.cancel = function cancel() {
this.span.cancel();
};
/**
* TODO: make it as a class
* Provides noop operation for the SpanHandle API when automatic tracing is not enabled or no span is currently active.
*/
function NoopSpanHandle() {}
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getTraceId = function getTraceId() {
return null;
};
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getSpanId = function getSpanId() {
return null;
};
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getParentSpanId = function getParentSpanId() {
return null;
};
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getName = function getName() {
return null;
};
NoopSpanHandle.prototype.isEntrySpan = function isEntrySpan() {
return false;
};
NoopSpanHandle.prototype.isExitSpan = function isExitSpan() {
return false;
};
NoopSpanHandle.prototype.isIntermediateSpan = function isIntermediateSpan() {
return false;
};
NoopSpanHandle.prototype.getTimestamp = function getTimestamp() {
return 0;
};
NoopSpanHandle.prototype.getDuration = function getDuration() {
return 0;
};
NoopSpanHandle.prototype.getErrorCount = function getErrorCount() {
return 0;
};
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getCorrelationId = function getCorrelationId() {
return null;
};
NoopSpanHandle.prototype.setCorrelationId = function setCorrelationId() {};
/**
* @returns {null}
*/
NoopSpanHandle.prototype.getCorrelationType = function getCorrelationType() {
return null;
};
NoopSpanHandle.prototype.setCorrelationType = function setCorrelationType() {};
NoopSpanHandle.prototype.annotate = function annotate() {};
NoopSpanHandle.prototype.markAsErroneous = function markAsErroneous() {};
NoopSpanHandle.prototype.markAsNonErroneous = function markAsNonErroneous() {};
NoopSpanHandle.prototype.disableAutoEnd = function disableAutoEnd() {
// provide dummy operation when automatic tracing is not enabled
};
NoopSpanHandle.prototype.end = function end() {
// provide dummy operation when automatic tracing is not enabled
};
NoopSpanHandle.prototype.cancel = function cancel() {
// provide dummy operation when automatic tracing is not enabled
};
/**
* @param {import('../util/normalizeConfig').InstanaConfig} config
*/
exports.init = function init(config) {
logger = config.logger;
};
/**
* @param {import ('./cls')} cls
* @returns {SpanHandle | NoopSpanHandle}
*/
exports.getHandleForCurrentSpan = function getHandleForCurrentSpan(cls) {
if (cls && cls.isTracing()) {
return new SpanHandle(cls.getCurrentSpan());
} else {
return new NoopSpanHandle();
}
};
// Only exported for testing purposese.
exports._SpanHandle = SpanHandle;
// Only exported for testing purposese.
exports._NoopSpanHandle = NoopSpanHandle;