elastic-apm-node
Version:
The official Elastic APM agent for Node.js
152 lines (137 loc) • 5.67 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.
*/
'use strict';
// A RunContext is the immutable structure that holds which transaction and span
// are currently active, if any, for the running JavaScript code.
//
// Module instrumentation code interacts with run contexts via a number of
// methods on the `Instrumentation` instance at `agent._instrumentation`.
//
// User code using the agent's API (the Agent API, Transaction API, and Span API)
// are not exposes to RunContext instances. However users of the OpenTelemetry
// API, provided by the OpenTelemetry Bridge, *are* exposed to OpenTelemetry
// `Context` instances -- which RunContext implements.
//
// A RunContext holds:
// - a current Transaction, which can be null; and
// - a *stack* of Spans, where the top-of-stack span is the "current" one.
// A stack is necessary to support the semantics of multiple started and ended
// spans in the same async task. E.g.:
// apm.startTransaction('t')
// var s1 = apm.startSpan('s1')
// var s2 = apm.startSpan('s2')
// s2.end()
// assert(apm.currentSpan === s1, 's1 is now the current span')
// - a mapping of "values". This is an arbitrary key-value mapping, but exists
// primarily to implement OpenTelemetry `interface Context`
// https://github.com/open-telemetry/opentelemetry-js-api/blob/v1.1.0/src/context/types.ts#L17-L41
//
// A RunContext is immutable. This means that `runContext.enterSpan(span)` and
// other similar methods return a new/separate RunContext instance. This is
// done so that a run-context change in the current code does not change
// anything for other code bound to the original RunContext (e.g. via
// `ins.bindFunction` or `ins.bindEmitter`).
//
// Warning: Agent code should never use the `RunContext` class directly
// because a subclass can be provided for the `Instrumentation` to use.
// Instead new instances should be built from an existing one, typically the
// active one (`_runCtxMgr.active()`) or the root one (`_runCtxMgr.root()`).
class RunContext {
constructor(trans, spans, parentValues) {
this._trans = trans || null;
this._spans = spans || [];
this._values = parentValues ? new Map(parentValues) : new Map();
}
currTransaction() {
return this._trans;
}
// Returns the currently active span, if any, otherwise null.
currSpan() {
if (this._spans.length > 0) {
return this._spans[this._spans.length - 1];
} else {
return null;
}
}
// Return a new RunContext for a newly active/current Transaction.
enterTrans(trans) {
return new this.constructor(trans, null, this._values);
}
// Return a new RunContext with the given span added to the top of the spans
// stack.
enterSpan(span) {
const newSpans = this._spans.slice();
newSpans.push(span);
return new this.constructor(this._trans, newSpans, this._values);
}
// Return a new RunContext with the given transaction (and hence all of its
// spans) removed.
leaveTrans() {
return new this.constructor(null, null, this._values);
}
// Return a new RunContext with the given span removed, or null if there is
// no change (the given span isn't part of the run context).
//
// Typically this span is the top of stack (i.e. it is the current span).
// However, it is possible to have out-of-order span.end() or even end a span
// that isn't part of the current run context stack at all. (See
// test/instrumentation/run-context/fixtures/end-non-current-spans.js for
// examples.)
leaveSpan(span) {
let newRc = null;
let newSpans;
const lastSpan = this._spans[this._spans.length - 1];
if (lastSpan && lastSpan.id === span.id) {
// Fast path for common case: `span` is top of stack.
newSpans = this._spans.slice(0, this._spans.length - 1);
newRc = new this.constructor(this._trans, newSpans, this._values);
} else {
const stackIdx = this._spans.findIndex((s) => s.id === span.id);
if (stackIdx !== -1) {
newSpans = this._spans
.slice(0, stackIdx)
.concat(this._spans.slice(stackIdx + 1));
newRc = new this.constructor(this._trans, newSpans, this._values);
}
}
return newRc;
}
// A string representation useful for debug logging.
// For example:
// RunContext(Transaction(abc123, 'trans name'), [Span(def456, 'span name', ended)])
// ^^^^^^^-- if the span has ended
// ^^^^^^ ^^^^^^-- id
// ^^^^^^^^^^-- the class name, typically "RunContext", but can be overriden
toString() {
const bits = [];
if (this._trans) {
bits.push(this._trans.toString());
}
if (this._spans.length > 0) {
const spanStrs = this._spans.map((s) => s.toString());
bits.push('[' + spanStrs + ']');
}
return `${this.constructor.name}(${bits.join(', ')})`;
}
// ---- The following implements the OTel Context interface.
// https://github.com/open-telemetry/opentelemetry-js-api/blob/v1.0.4/src/context/types.ts#L17
getValue(key) {
return this._values.get(key);
}
setValue(key, value) {
const rc = new this.constructor(this._trans, this._spans, this._values);
rc._values.set(key, value);
return rc;
}
deleteValue(key) {
const rc = new this.constructor(this._trans, this._spans, this._values);
rc._values.delete(key);
return rc;
}
}
module.exports = {
RunContext,
};