Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
/*
 * (c) Copyright IBM Corp. 2021
 * (c) Copyright Instana Inc. and contributors 2017
 */

'use strict';

const spanBuffer = require('./spanBuffer');
const tracingUtil = require('./tracingUtil');
const { ENTRY, EXIT, INTERMEDIATE, isExitSpan } = require('./constants');
const hooked = require('./clsHooked');
const tracingMetrics = require('./metrics');
/** @type {import('../logger').GenericLogger} */
let logger;
logger = require('../logger').getLogger('tracing/cls', newLogger => {
  logger = newLogger;
});

const currentEntrySpanKey = 'com.supertenant.entry';
const currentSpanKey = 'com.supertenant.span';
const reducedSpanKey = 'com.supertenant.reduced';
const tracingLevelKey = 'com.supertenant.tl';
const w3cTraceContextKey = 'com.supertenant.w3ctc';

// eslint-disable-next-line no-undef-init
/** @type {String} */
let serviceName;
/** @type {import('../../../collector/src/pidStore')} */
let processIdentityProvider = null;

/*
 * Access the SuperTenant namespace in continuation local storage.
 *
 * Usage:
 *   cls.ns.get(key);
 *   cls.ns.set(key);
 *   cls.ns.run(function() {});
 */
const ns = hooked.createNamespace('supertenant.supermeter');

/**
 * @param {import('../util/normalizeConfig').InstanaConfig} config
 * @param {import('../../../collector/src/pidStore')} _processIdentityProvider
 */
function init(config, _processIdentityProvider) {
  if (config && config.serviceName) {
    serviceName = config.serviceName;
  }
  processIdentityProvider = _processIdentityProvider;
}

/**
 * This type is to be used across the code base, as we don't have a public explicit type for a span.
 * Also, it is often that we simply create a literal object and gradually throw few properties on it and
 * handle them as spans. This type has all span properties, but they are all optional, so we can safely
 * type these literal objects.
 * TODO: move InstanaSpan and InstanaPseudoSpan to their own file and make them publicly accessible?
 * @typedef {Object} InstanaBaseSpan
 * @property {string} [t] trace ID
 * @property {string} [p] parent span ID
 * @property {string} [s] span ID
 * @property {string} [n] type/name
 * @property {number} [k] kind
 * @property {number} [ec] error count
 * @property {number} [ts] timestamp
 * @property {number} [d] duration
 * @property {{e?: string, h?: string, hl?: boolean, cp?: string}} [f] from section
 * @property {boolean} [tp] trace ID is from traceparent header
 * @property {string} [lt] long trace ID
 * @property {object} [ia] closest Instana ancestor span
 * @property {string} [crtp] correlation type
 * @property {string} [crid] correlation ID
 * @property {boolean} [sy] synthetic marker
 * @property {boolean} [pathTplFrozen] pathTplFrozen
 * @property {boolean} [transmitted] transmitted
 * @property {boolean} [manualEndMode] manualEndMode
 * @property {*} [stack] stack trace
 * @property {Object.<string, *>} [data]
 * @property {{s?: number, d?: number}} [b] batching information
 * @property {*} [gqd] GraphQL destination
 * @property {Function} [transmit]
 * @property {Function} [freezePathTemplate]
 * @property {Function} [disableAutoEnd]
 * @property {Function} [transmitManual]
 * @property {Function} [cancel]
 * @property {Function} [addCleanup]
 * @property {Function} [cleanup]
 */

class InstanaSpan {
  /**
   * @param {string} name
   */
  constructor(name) {
    // properties that part of our span model
    this.t = undefined;
    this.s = undefined;
    this.p = undefined;
    this.n = name;
    this.k = undefined;
    if (processIdentityProvider && typeof processIdentityProvider.getFrom === 'function') {
      this.f = processIdentityProvider.getFrom();
    }
    this.ec = 0;
    this.ts = Date.now();
    this.d = 0;
    /** @type {Array.<*>} */
    this.stack = [];
    /** @type {Object.<string, *>} */
    this.data = {};

    // Properties for the span that are only used internally but will not be transmitted to the agent/backend,
    // therefore defined as non-enumerabled. NOTE: If you add a new property, make sure that it is also defined as
    // non-enumerable.
    Object.defineProperty(this, 'cleanupFunctions', {
      value: [],
      writable: false,
      enumerable: false
    });
    Object.defineProperty(this, 'transmitted', {
      value: false,
      writable: true,
      enumerable: false
    });
    Object.defineProperty(this, 'pathTplFrozen', {
      value: false,
      writable: true,
      enumerable: false
    });
    Object.defineProperty(this, 'manualEndMode', {
      value: false,
      writable: true,
      enumerable: false
    });
    // Marker for higher level instrumentation (like graphql.server) to not transmit the span once they are done, but
    // instead we wait for the protocol level instrumentation to finish (which then transmits the span).
    Object.defineProperty(this, 'postponeTransmit', {
      value: false,
      configurable: true,
      writable: true,
      enumerable: false
    });
    // Additional special purpose marker that is only used to control transmission logig between the GraphQL server core
    // instrumentation and Apollo Gateway instrumentation
    // (see packages/core/tracing/instrumentation/protocols/graphql.js).
    Object.defineProperty(this, 'postponeTransmitApolloGateway', {
      value: false,
      configurable: true,
      writable: true,
      enumerable: false
    });
  }

  /**
   * @param {Function} fn
   */
  addCleanup(fn) {
    // @ts-ignore
    this.cleanupFunctions.push(fn);
  }

  transmit() {
    if (!this.transmitted && !this.manualEndMode) {
      spanBuffer.addSpan(this);
      this.cleanup();
      tracingMetrics.incrementClosed();
      this.transmitted = true;
    }
  }

  transmitManual() {
    if (!this.transmitted) {
      spanBuffer.addSpan(this);
      this.cleanup();
      tracingMetrics.incrementClosed();
      this.transmitted = true;
    }
  }

  cancel() {
    if (!this.transmitted) {
      this.cleanup();
      tracingMetrics.incrementClosed();
      this.transmitted = true;
    }
  }

  cleanup() {
    // @ts-ignore
    this.cleanupFunctions.forEach(call);
    // @ts-ignore
    this.cleanupFunctions.length = 0;
  }

  freezePathTemplate() {
    this.pathTplFrozen = true;
  }

  disableAutoEnd() {
    this.manualEndMode = true;
  }
}

/**
 * Overrides transmit and cancel so that a pseudo span is not put into the span buffer. All other behaviour is inherited
 * from InstanaSpan.
 */
class InstanaPseudoSpan extends InstanaSpan {
  transmit() {
    if (!this.transmitted && !this.manualEndMode) {
      this.cleanup();
      this.transmitted = true;
    }
  }

  transmitManual() {
    if (!this.transmitted) {
      this.cleanup();
      this.transmitted = true;
    }
  }

  cancel() {
    if (!this.transmitted) {
      this.cleanup();
      this.transmitted = true;
    }
  }
}

/**
 * Start a new span and set it as the current span.
 * @param {string} spanName
 * @param {number} kind
 * @param {string} traceId
 * @param {string} parentSpanId
 * @param {import('./w3c_trace_context/W3cTraceContext')} [w3cTraceContext]
 * @returns {InstanaSpan}
 */
function startSpan(spanName, kind, traceId, parentSpanId, w3cTraceContext) {
  tracingMetrics.incrementOpened();
  if (!kind || (kind !== ENTRY && kind !== EXIT && kind !== INTERMEDIATE)) {
    logger.warn('Invalid span (%s) without kind/with invalid kind: %s, assuming EXIT.', spanName, kind);
    kind = EXIT;
  }
  const span = new InstanaSpan(spanName);
  span.k = kind;

  const parentSpan = getCurrentSpan();
  const parentW3cTraceContext = getW3cTraceContext();

  if (serviceName != null) {
    span.data.service = serviceName;
  }

  // If the client code has specified a trace ID/parent ID, use the provided IDs.
  if (traceId) {
    span.t = traceId;
    if (parentSpanId) {
      span.p = parentSpanId;
    }
  } else if (parentSpan) {
    // Otherwise, use the currently active span (if any) as parent.
    span.t = parentSpan.t;
    span.p = parentSpan.s;
  } else {
    // If no IDs have been provided, we start a new trace by generating a new trace ID. We do not set a parent ID in
    // this case.
    span.t = tracingUtil.generateRandomTraceId();
  }

  // Always generate a new span ID for the new span.
  span.s = tracingUtil.generateRandomSpanId();

  if (!w3cTraceContext && parentW3cTraceContext) {
    // If there is no incoming W3C trace context that has been read from HTTP headers, but there is a parent trace
    // context associated with a parent span, we will create an updated copy of that parent W3C trace context. We must
    // make sure that the parent trace context in the parent cls context is not modified.
    w3cTraceContext = parentW3cTraceContext.clone();
  }

  if (w3cTraceContext) {
    w3cTraceContext.updateParent(span.t, span.s);
    span.addCleanup(ns.set(w3cTraceContextKey, w3cTraceContext));
  }

  if (span.k === ENTRY) {
    // Make the entry span available independently (even if getCurrentSpan would return an intermediate or an exit at
    // any given moment). This is used by the instrumentations of web frameworks like Express.js to add path templates
    // and error messages to the entry span.
    span.addCleanup(ns.set(currentEntrySpanKey, span));
  }

  // Set the span object as the currently active span in the active CLS context and also add a cleanup hook for when
  // this span is transmitted.
  span.addCleanup(ns.set(currentSpanKey, span));
  return span;
}

/**
 * Puts a pseudo span in the CLS context that is simply a holder for a trace ID and span ID. This pseudo span will act
 * as the parent for other child span that are produced but will not be transmitted to the agent itself.
 * @param {string} spanName
 * @param {number} kind
 * @param {string} traceId
 * @param {string} spanId
 */
function putPseudoSpan(spanName, kind, traceId, spanId) {
  if (!kind || (kind !== ENTRY && kind !== EXIT && kind !== INTERMEDIATE)) {
    logger.warn('Invalid pseudo span (%s) without kind/with invalid kind: %s, assuming EXIT.', spanName, kind);
    kind = EXIT;
  }
  const span = new InstanaPseudoSpan(spanName);
  span.k = kind;

  if (!traceId) {
    logger.warn('Cannot start a pseudo span without a trace ID', spanName, kind);
    return;
  }
  if (!spanId) {
    logger.warn('Cannot start a pseudo span without a span ID', spanName, kind);
    return;
  }

  span.t = traceId;
  span.s = spanId;

  if (span.k === ENTRY) {
    // Make the entry span available independently (even if getCurrentSpan would return an intermediate or an exit at
    // any given moment). This is used by the instrumentations of web frameworks like Express.js to add path templates
    // and error messages to the entry span.
    span.addCleanup(ns.set(currentEntrySpanKey, span));
  }

  // Set the span object as the currently active span in the active CLS context and also add a cleanup hook for when
  // this span is transmitted.
  span.addCleanup(ns.set(currentSpanKey, span));
  return span;
}

/*
 * Get the currently active entry span.
 */
function getCurrentEntrySpan() {
  return ns.get(currentEntrySpanKey);
}

/**
 * Set the currently active span.
 * @param {InstanaSpan} span
 */
function setCurrentSpan(span) {
  ns.set(currentSpanKey, span);
}

/**
 * Get the currently active span.
 * @param {boolean} [fallbackToSharedContext=false]
 * @returns {InstanaBaseSpan}
 */
function getCurrentSpan(fallbackToSharedContext = false) {
  return ns.get(currentSpanKey, fallbackToSharedContext);
}

/**
 * Get the reduced backup of the last active span in this cls context.
 * @param {boolean} [fallbackToSharedContext=false]
 */
function getReducedSpan(fallbackToSharedContext = false) {
  return ns.get(reducedSpanKey, fallbackToSharedContext);
}

/**
 * Stores the W3C trace context object.
 * @param {import('./w3c_trace_context/W3cTraceContext')} traceContext
 */
function setW3cTraceContext(traceContext) {
  ns.set(w3cTraceContextKey, traceContext);
}

/*
 * Returns the W3C trace context object.
 */
function getW3cTraceContext() {
  return ns.get(w3cTraceContextKey);
}

/*
 * Determine if we're currently tracing or not.
 */
function isTracing() {
  return !!ns.get(currentSpanKey);
}

/**
 * Set the tracing level
 * @param {string} level
 */
function setTracingLevel(level) {
  ns.set(tracingLevelKey, level);
}

/*
 * Get the tracing level (if any)
 */
function tracingLevel() {
  return ns.get(tracingLevelKey);
}

/*
 * Determine if tracing is suppressed (via tracing level) for this request.
 */
function tracingSuppressed() {
  const tl = tracingLevel();
  return typeof tl === 'string' && tl.indexOf('0') === 0;
}

function getAsyncContext() {
  if (!ns) {
    return null;
  }
  return ns.active;
}

/**
 * Do not use enterAsyncContext unless you absolutely have to. Instead, use one of the methods provided in the sdk,
 * that is, runInAsyncContext or runPromiseInAsyncContext.
 *
 * If you use enterAsyncContext anyway, you are responsible for also calling leaveAsyncContext later on. Leaving the
 * async context is managed automatically for you with the runXxxInAsyncContext functions.
 * @param {import('./clsHooked/context').InstanaCLSContext} context
 */
function enterAsyncContext(context) {
  if (!ns) {
    return;
  }
  if (context == null) {
    logger.warn('Ignoring enterAsyncContext call because passed context was null or undefined.');
    return;
  }
  ns.enter(context);
}

/**
 * Needs to be called if and only if enterAsyncContext has been used earlier.
 * @param {import('./clsHooked/context').InstanaCLSContext} context
 */
function leaveAsyncContext(context) {
  if (!ns) {
    return;
  }
  if (context == null) {
    logger.warn('Ignoring leaveAsyncContext call because passed context was null or undefined.');
    return;
  }
  ns.exit(context);
}

/**
 * @param {import('./clsHooked/context').InstanaCLSContext} context
 * @param {Function} fn
 */
function runInAsyncContext(context, fn) {
  if (!ns) {
    return fn();
  }
  if (context == null) {
    logger.warn('Ignoring runInAsyncContext call because passed context was null or undefined.');
    return fn();
  }
  return ns.runAndReturn(fn, context);
}

/**
 * @param {import('./clsHooked/context').InstanaCLSContext} context
 * @param {Function} fn
 * @returns {Function | *}
 */
function runPromiseInAsyncContext(context, fn) {
  if (!ns) {
    return fn();
  }
  if (context == null) {
    logger.warn('Ignoring runPromiseInAsyncContext call because passed context was null or undefined.');
    return fn();
  }
  return ns.runPromise(fn, context);
}

/**
 * @param {Function} fn
 */
function call(fn) {
  fn();
}
/**
 * This method should be used in all exit instrumentations.
 * It checks whether the tracing shoud be skipped or not.
 *
 * | options             | description
 * --------------------------------------------------------------------------------------------
 * | isActive            | Whether the instrumentation is active or not.
 * | extendedResponse    | By default the method returns a boolean. Sometimes it's helpful to
 * |                     | get the full response when you would like to determine why it was skipped.
 * |                     | For example because of suppression.
 * | skipParentSpanCheck | Some instrumentations have a very specific handling for checking the parent span.
 * |                     | With this flag you can skip the default parent span check.
 * | log                 | Logger instrumentations might not want to log because they run into recursive
 * |                     | problem raising `RangeError: Maximum call stack size exceeded`.
 * | skipIsTracing       | Instrumentation wants to handle `cls.isTracing` on it's own (e.g db2)
 *
 * @param {Object.<string, *>} options
 */
function _skipExitTracing(
  options = {
    isActive: true,
    extendedResponse: false,
    skipParentSpanCheck: false,
    log: true,
    skipIsTracing: false
  }
) {
  const parentSpan = getCurrentSpan();
  const suppressed = tracingSuppressed();
  const isExitSpanResult = isExitSpan(parentSpan);
  if (!options.skipParentSpanCheck && (!parentSpan || isExitSpanResult)) {
    if (options.log) {
      logger.warn(
        // eslint-disable-next-line max-len
        `Cannot start an exit span as this requires an active entry (or intermediate) span as parent. ${
          parentSpan
            ? `But the currently active span is itself an exit span: ${JSON.stringify(parentSpan)}`
            : 'Currently there is no span active at all'
        }`
      );
    }

    if (options.extendedResponse) return { skip: true, suppressed, isExitSpan: isExitSpanResult };
    else return true;
  }

  const skipIsActive = options.isActive === false;
  const skipIsTracing = !options.skipIsTracing ? !isTracing() : false;
  const skip = skipIsActive || skipIsTracing || suppressed;
  if (options.extendedResponse) return { skip, suppressed, isExitSpan: isExitSpanResult };
  else return skip;
}

/**
 * @param {Object.<string, *>} options
 * @returns {any}
 */
function _neverSkipExitTracing(
  options = {
    isActive: true,
    extendedResponse: false,
    skipParentSpanCheck: false,
    log: true,
    skipIsTracing: false
  }
) {
  const suppressed = tracingSuppressed();
  if (options.extendedResponse) return { skip: suppressed, suppressed: suppressed };
  return false;
}

const skipExitTracing = process.env.ST_INSTANA_ENABLED === "true" ? _skipExitTracing : _neverSkipExitTracing

module.exports = {
  skipExitTracing,
  currentEntrySpanKey,
  currentSpanKey,
  reducedSpanKey,
  tracingLevelKey,
  w3cTraceContextKey,
  ns,
  init,
  startSpan,
  putPseudoSpan,
  getCurrentEntrySpan,
  setCurrentSpan,
  getCurrentSpan,
  getReducedSpan,
  setW3cTraceContext,
  getW3cTraceContext,
  isTracing,
  setTracingLevel,
  tracingLevel,
  tracingSuppressed,
  getAsyncContext,
  enterAsyncContext,
  leaveAsyncContext,
  runInAsyncContext,
  runPromiseInAsyncContext
};