elastic-apm-node
Version:
The official Elastic APM agent for Node.js
323 lines (278 loc) • 8.77 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
;
const truncate = require('unicode-byte-truncate');
const { INTAKE_STRING_MAX_SIZE } = require('../constants');
const constants = require('../constants');
const { SpanCompression } = require('./span-compression');
const Timer = require('./timer');
const TraceContext = require('../tracecontext');
const { TraceParent } = require('../tracecontext/traceparent');
module.exports = GenericSpan;
/**
* GenericSpan is an internal base class for Span and Transaction.
*
* @param {Agent} agent
* @param {Object} opts
* TODO: document supported opts
*/
function GenericSpan(agent, opts) {
this._timer = new Timer(opts.timer, opts.startTime);
this._context = TraceContext.startOrResume(
opts.childOf,
agent._conf,
opts.tracestate,
);
this._hasPropagatedTraceContext = false;
this._parentSpan = null;
if (opts.childOf instanceof GenericSpan) {
this.setParentSpan(opts.childOf);
}
this._compression = new SpanCompression(agent);
this._compression.setBufferedSpan(null);
this._agent = agent;
this._labels = null;
this._ids = null; // Populated by sub-types of GenericSpan
this._otelKind = null;
this._otelAttributes = null;
this._links = [];
if (opts.links) {
for (let i = 0; i < opts.links.length; i++) {
const link = linkFromLinkArg(opts.links[i]);
if (link) {
this._links.push(link);
}
}
}
this.timestamp = this._timer.start;
this.ended = false;
this._duration = null; // Duration in milliseconds. Set on `.end()`.
this._endTimestamp = null;
this.outcome = constants.OUTCOME_UNKNOWN;
// Freezing the outcome allows us to prefer a value set from
// from the API and allows a span to keep its unknown status
// even if it succesfully ends.
this._isOutcomeFrozen = false;
}
Object.defineProperty(GenericSpan.prototype, 'id', {
enumerable: true,
get() {
return this._context.traceparent.id;
},
});
Object.defineProperty(GenericSpan.prototype, 'traceId', {
enumerable: true,
get() {
return this._context.traceparent.traceId;
},
});
Object.defineProperty(GenericSpan.prototype, 'parentId', {
enumerable: true,
get() {
return this._context.traceparent.parentId;
},
});
Object.defineProperty(GenericSpan.prototype, 'sampled', {
enumerable: true,
get() {
return this._context.traceparent.recorded;
},
});
Object.defineProperty(GenericSpan.prototype, 'sampleRate', {
enumerable: true,
get() {
const rate = parseFloat(this._context.tracestate.getValue('s'));
if (rate >= 0 && rate <= 1) {
return rate;
}
return null;
},
});
Object.defineProperty(GenericSpan.prototype, 'traceparent', {
enumerable: true,
get() {
return this._context.toString();
},
});
// The duration of the span, in milliseconds.
GenericSpan.prototype.duration = function () {
if (!this.ended) {
this._agent.logger.debug(
'tried to call duration() on un-ended transaction/span %o',
{
id: this.id,
parent: this.parentId,
trace: this.traceId,
name: this.name,
type: this.type,
},
);
return null;
}
return this._duration;
};
// The 'stringify' option is for backward compatibility and will likely be
// removed in the next major version.
GenericSpan.prototype.setLabel = function (key, value, stringify = true) {
const makeLabelValue = () => {
if (
!stringify &&
(typeof value === 'boolean' || typeof value === 'number')
) {
return value;
}
return truncate(String(value), INTAKE_STRING_MAX_SIZE);
};
if (!key) return false;
if (!this._labels) this._labels = {};
var skey = key.replace(/[.*"]/g, '_');
if (key !== skey) {
this._agent.logger.warn('Illegal characters used in tag key: %s', key);
}
this._labels[skey] = makeLabelValue();
return true;
};
GenericSpan.prototype.addLabels = function (labels, stringify) {
if (!labels) return false;
var keys = Object.keys(labels);
for (const key of keys) {
if (!this.setLabel(key, labels[key], stringify)) {
return false;
}
}
return true;
};
// Add span links.
//
// @param {Array} links - An array of objects with a `context` property that is
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
// or a W3C trace-context 'traceparent' string.
GenericSpan.prototype.addLinks = function (links) {
if (links) {
for (let i = 0; i < links.length; i++) {
this.addLink(links[i]);
}
}
};
// Add a span link.
//
// @param {Link} link - An object with a `context` property that is
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
// or a W3C trace-context 'traceparent' string.
GenericSpan.prototype.addLink = function (linkArg) {
const link = linkFromLinkArg(linkArg);
if (link) {
this._links.push(link);
}
};
GenericSpan.prototype._freezeOutcome = function () {
this._isOutcomeFrozen = true;
};
GenericSpan.prototype._isValidOutcome = function (outcome) {
return (
outcome === constants.OUTCOME_FAILURE ||
outcome === constants.OUTCOME_SUCCESS ||
outcome === constants.OUTCOME_UNKNOWN
);
};
GenericSpan.prototype.propagateTraceContextHeaders = function (
carrier,
setter,
) {
this._hasPropagatedTraceContext = true;
return this._context.propagateTraceContextHeaders(carrier, setter);
};
GenericSpan.prototype.setParentSpan = function (span) {
this._parentSpan = span;
};
GenericSpan.prototype.getParentSpan = function (span) {
return this._parentSpan;
};
GenericSpan.prototype.getBufferedSpan = function () {
return this._compression.getBufferedSpan();
};
GenericSpan.prototype.setBufferedSpan = function (span) {
return this._compression.setBufferedSpan(span);
};
GenericSpan.prototype.isCompositeSameKind = function () {
return this._compression.isCompositeSameKind();
};
GenericSpan.prototype.isComposite = function () {
return this._compression.isComposite();
};
GenericSpan.prototype.getCompositeSum = function () {
return this._compression.composite.sum;
};
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-api-otel.md#span-kind
// @param {String} kind
GenericSpan.prototype._setOTelKind = function (kind) {
this._otelKind = kind;
};
// This returns the internal OTel attributes object so it can be mutated.
GenericSpan.prototype._getOTelAttributes = function () {
if (!this._otelAttributes) {
this._otelAttributes = {};
}
return this._otelAttributes;
};
// Serialize OTel-related fields into the given payload, if any.
GenericSpan.prototype._serializeOTel = function (payload) {
if (this._otelKind) {
payload.otel = {
span_kind: this._otelKind,
};
}
if (this._otelAttributes) {
// Though the spec allows it ("MAY"), we are opting *not* to report OTel
// span attributes as labels for older (<7.16) versions of APM server.
// This is to avoid the added complexity of guarding allowed attribute
// value types to those supported by the APM server intake API.
if (!payload.otel) {
payload.otel = {};
}
payload.otel.attributes = this._otelAttributes;
}
};
// Translate a `opts.links` entry (see the `Link` type in "index.d.ts") to a
// span link as it will be serialized and sent to APM server. If the linkArg is
// invalid, this will return null.
//
// @param {Object} linkArg - An object with a `context` property that is
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
// or a W3C trace-context 'traceparent' string.
function linkFromLinkArg(linkArg) {
if (!linkArg || !linkArg.context) {
return null;
}
const ctx = linkArg.context;
let traceId;
let spanId;
if (ctx.traceId && ctx.spanId) {
// Duck-typing for an OTel SpanContext. APM intake v2 only supports the
// trace id and span id fields for span links, so we only need care about
// those attributes.
traceId = ctx.traceId;
spanId = ctx.spanId;
} else if (ctx._context instanceof TraceContext) {
// Transaction or Span
traceId = ctx._context.traceparent.traceId;
spanId = ctx._context.traceparent.id;
} else if (ctx instanceof TraceParent) {
traceId = ctx.traceId;
spanId = ctx.id;
} else if (typeof ctx === 'string') {
// Note: Unfortunately TraceParent.fromString doesn't validate the string.
const traceparent = TraceParent.fromString(ctx);
traceId = traceparent.traceId;
spanId = traceparent.id;
} else {
return null;
}
return {
trace_id: traceId,
span_id: spanId,
};
}