UNPKG

@instana/core

Version:
754 lines (669 loc) 23.8 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2017 */ 'use strict'; const leftPad = require('./leftPad'); const spanBuffer = require('./spanBuffer'); const tracingUtil = require('./tracingUtil'); const { ENTRY, EXIT, INTERMEDIATE, isExitSpan } = require('./constants'); const hooked = require('./clsHooked'); const tracingMetrics = require('./metrics'); const { applyFilter } = require('../util/spanFilter'); /** @type {import('../core').GenericLogger} */ let logger; const currentEntrySpanKey = 'com.instana.entry'; const currentSpanKey = 'com.instana.span'; const reducedSpanKey = 'com.instana.reduced'; const tracingLevelKey = 'com.instana.tl'; const w3cTraceContextKey = 'com.instana.w3ctc'; // eslint-disable-next-line no-undef-init /** @type {String} */ let serviceName; /** @type {import('../../../collector/src/pidStore')} */ let processIdentityProvider = null; /** @type {Boolean} */ let allowRootExitSpan; /** @type {Boolean} */ let ignoreEndpointsDisableDownStreamSuppression; /* * Access the Instana namespace in continuation local storage. * * Usage: * cls.ns.get(key); * cls.ns.set(key); * cls.ns.run(function() {}); */ const ns = hooked.createNamespace('instana.collector'); /** * @param {import('../util/normalizeConfig').InstanaConfig} config * @param {import('../../../collector/src/pidStore')} [_processIdentityProvider] */ function init(config, _processIdentityProvider) { logger = config.logger; if (config && config.serviceName) { serviceName = config.serviceName; } processIdentityProvider = _processIdentityProvider; allowRootExitSpan = config?.tracing?.allowRootExitSpan; ignoreEndpointsDisableDownStreamSuppression = config?.tracing?.ignoreEndpointsDisableSuppression; } class InstanaSpan { /** * @param {string} name * @param {object} [data] */ constructor(name, data) { // 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(); } // The property span.ec is defined with a getter/setter instead of being a plain property. We need to be able to // prohibit overwriting span.ec from auto-tracing instrumentations after it has been set manually via the // SDK (spanHandle.markAsErroneous). Object.defineProperty(this, '_ec', { value: 0, writable: true, enumerable: false }); Object.defineProperty(this, 'ec', { get() { return this._ec; }, set(value) { if (!this.ecHasBeenSetManually) { this._ec = value; } }, enumerable: true }); Object.defineProperty(this, 'ecHasBeenSetManually', { value: false, writable: true, enumerable: false }); this.ts = Date.now(); this.d = 0; /** @type {Array.<*>} */ this.stack = []; /** @type {Object.<string, *>} */ this.data = 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 }); // Indicates whether the span is ignored. // If true, the span will be excluded from tracing and not propagated. Object.defineProperty(this, 'isIgnored', { value: false, writable: true, enumerable: false }); // This property "shouldSuppressDownstream" currently applicable only for ignoring endpoints. // When a span is ignored, downstream suppression is automatically enabled. // However, if required, downstream suppression propagation can be explicitly disabled // via the env variable `INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION`. Object.defineProperty(this, 'shouldSuppressDownstream', { value: false, 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; } } } /** * This class represents an ignored span. We need this representation for the ignoring endpoints feature because * when a customer accesses the current span via `instana.currentSpan()`, we do not want to return a "NoopSpan". * Instead, we return this ignored span instance so the trace ID remains accessible. * It overrides the `transmit` and `cancel` methods to to ensure that the span is never sent, only cleaned up. */ // eslint-disable-next-line no-unused-vars class InstanaIgnoredSpan extends InstanaSpan { /** * @param {string} name * @param {object} data */ constructor(name, data) { super(name, data); this.isIgnored = true; // By default, downstream suppression for ignoring endpoints is enabled. // If the environment variable `INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION` is set, // should not suppress the downstream calls. this.shouldSuppressDownstream = !ignoreEndpointsDisableDownStreamSuppression; } 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 {Object} spanAttributes * @param {string} [spanAttributes.spanName] * @param {number} [spanAttributes.kind] * @param {string} [spanAttributes.traceId] * @param {string} [spanAttributes.parentSpanId] * @param {import('./w3c_trace_context/W3cTraceContext')} [spanAttributes.w3cTraceContext] * @param {Object} [spanAttributes.spanData] * @returns {InstanaSpan} */ function startSpan(spanAttributes = {}) { let { spanName, kind, traceId, parentSpanId, w3cTraceContext, spanData } = spanAttributes; tracingMetrics.incrementOpened(); if (!kind || (kind !== ENTRY && kind !== EXIT && kind !== INTERMEDIATE)) { logger.warn(`Invalid span (${spanName}) without kind/with invalid kind: ${kind}, assuming EXIT.`); kind = EXIT; } const span = new InstanaSpan(spanName, spanData); 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) { // The incoming trace ID/span ID from an upstream tracer could be shorter than the standard length. We normalize // both IDs here by left-padding with 0 characters. // Maintenance note (128-bit-trace-ids): When we switch to 128 bit trace IDs, we need to left-pad the trace ID to 32 // characters instead of 16. span.t = leftPad(traceId, 16); if (parentSpanId) { span.p = leftPad(parentSpanId, 16); } } 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)); } const spanIsTraced = applyFilter(span); // If the span was filtered out, we do not process it further. // Instead, we return an 'InstanaIgnoredSpan' instance to explicitly indicate that it was excluded from tracing. if (!spanIsTraced) { return setIgnoredSpan({ spanName: span.n, kind: span.k, traceId: span.t, parentId: span.p, data: span.data }); } 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 (${spanName}) without kind/with invalid kind: ${kind}, assuming EXIT.`); 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; } /** * Adds an ignored span to the CLS context, serving as a holder for a trace ID and span ID. * Customers can access the current span via `instana.currentSpan()`, and we avoid returning a "NoopSpan". * We need this ignored span instance to ensure the trace ID remains accessible for future cases such as propagating * the trace ID although suppression is on to reactivate a trace. * These spans will not be sent to the backend. * @param {Object} options - The options for the span. * @param {string} options.spanName * @param {number} options.kind * @param {string} options.traceId * @param {string} options.parentId * @param {Object} options.data */ function setIgnoredSpan({ spanName, kind, traceId, parentId, data = {} }) { if (!kind || (kind !== ENTRY && kind !== EXIT && kind !== INTERMEDIATE)) { logger.warn(`Invalid ignored span (${spanName}) without kind/with invalid kind: ${kind}, assuming EXIT.`); kind = EXIT; } const span = new InstanaIgnoredSpan(spanName, data); span.k = kind; span.t = traceId; span.p = parentId; // Setting the 'parentId' of the span to 'span.s' to ensure trace continuity. // Although this span doesn't physically exist, we are ignoring it, but retaining its parentId. // This parentId is propagated downstream. // The spanId does not need to be retained. span.s = parentId; 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)); // For entry spans, we need to retain suppression information to ensure that // tracing is suppressed for all internal (!) subsequent outgoing (exit) calls. // By default, downstream suppression for ignored spans is enabled. // If the environment variable `INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION is set, // should not suppress the downstream calls. if (span.shouldSuppressDownstream) setTracingLevel('0'); } // 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 {import('../core').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. * | skipIsTracing | Instrumentation wants to handle `cls.isTracing` on it's own (e.g db2) * | checkReducedSpan | If no active entry span is present, there is an option for * | | falling back to the most recent parent span stored as reduced span * | | by setting checkReducedSpan attribute to true. * | skipAllowRootExitSpanPresence | An instrumentation can ignore this feature to reduce noise. * * @param {Object.<string, *>} options */ function skipExitTracing(options) { const opts = Object.assign( { isActive: true, extendedResponse: false, skipParentSpanCheck: false, skipIsTracing: false, checkReducedSpan: false, skipAllowRootExitSpanPresence: false }, options ); let isReducedSpan = false; let parentSpan = getCurrentSpan(); // If there is no active entry span, we fall back to the reduced span of the most recent entry span. // See comment in packages/core/src/tracing/clsHooked/unset.js#storeReducedSpan. if (opts.checkReducedSpan && !parentSpan) { parentSpan = getReducedSpan(); // We need to remember if a reduced span was used, because for reduced spans // we do NOT trace anymore. The async context got already closed. if (parentSpan) { isReducedSpan = true; } } const suppressed = tracingSuppressed(); const isParentSpanAnExitSpan = isExitSpan(parentSpan); // CASE: first ask for suppressed, because if we skip the entry span, we won't have a parentSpan // on the exit span, which would create noisy log messages. if (suppressed) { if (opts.extendedResponse) { return { skip: true, suppressed, isExitSpan: isParentSpanAnExitSpan, parentSpan, allowRootExitSpan }; } return true; } // DESC: If `allowRootExitSpan` is true, then we have to ignore if there is a parent or not. // NOTE: The feature completely ignores the state of `isTracing`, because // every exit span would be a separate trace. `isTracing` is always false, // because we don't have a parent span. The http server span also does not check of `isTracing`, // because it's the root span. // CASE: Instrumentations can disable the `allowRootExitSpan` feature e.g. loggers. if (!opts.skipAllowRootExitSpanPresence && allowRootExitSpan) { if (opts.extendedResponse) { return { skip: false, suppressed, isExitSpan: isParentSpanAnExitSpan, parentSpan, allowRootExitSpan }; } return false; } // Parent span check is required skipParentSpanCheck and no parent is present but an exit span only if (!opts.skipParentSpanCheck && (!parentSpan || isParentSpanAnExitSpan)) { if (opts.extendedResponse) { return { skip: true, suppressed, isExitSpan: isParentSpanAnExitSpan, parentSpan, allowRootExitSpan }; } return true; } const skipIsActive = opts.isActive === false; let skipIsTracing = !opts.skipIsTracing ? !isTracing() : false; // See comment on top. if (isReducedSpan) { skipIsTracing = false; } const skip = skipIsActive || skipIsTracing; if (opts.extendedResponse) { return { skip, suppressed, isExitSpan: isParentSpanAnExitSpan, parentSpan, allowRootExitSpan }; } return skip; } 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 };