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 2023 Supertenant Ltd. - all rights reserved.
// See LICENSE file in project root for license terms.

'use strict';

/**
 * @typedef {import('../../clsHooked/context').InstanaCLSContext} InstanaCLSContext
 * @typedef {import('@supertenant/superbrain/types/binding.cjs').OpenSpanResult} OpenSpanResult
 * @typedef {import('@supertenant/superbrain/types/binding.cjs').JSOpenSpanResult} JSOpenSpanResult
 * @typedef {import('@supertenant/superbrain/types/binding.cjs').SpanLabels} SpanLabels
 */

const coreHttpsModule = require('https');
const coreHttpModule = require('http');

const { superbrain } = require('@supertenant/superbrain');
const superconsts = require('@supertenant/superconsts');
const { getOrCreateTask } = require('../../taskManager');
const { getWaitPollInterval, getHttpRejectActionStatusCode, getDelayDuration } = require('../../actions');

const constants = require('../../constants');
const tracingHeaders = require('../../tracingHeaders');
const { filterParams, sanitizeUrl } = require('../../../util/url');
const {
  getExtraHeadersFromMessage,
  mergeExtraHeadersFromServerResponseOrClientRequest
} = require('./captureHttpHeadersUtil');
const shimmer = require('shimmer');
const cls = require('../../cls');
const { dedupReportError } = require('../../../logger');

let extraHttpHeadersToCapture;
let isActive = false;
let serviceName = null;

exports.spanName = 'node.http.server';

exports.init = function init(config) {
  shimmer.wrap(coreHttpModule.Server && coreHttpModule.Server.prototype, 'emit', shimEmit);
  shimmer.wrap(coreHttpsModule.Server && coreHttpsModule.Server.prototype, 'emit', shimEmit);
  extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture;
  serviceName = config.serviceName;
};

exports.updateConfig = function updateConfig(config) {
  extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture;
};

exports.activate = function activate(extraConfig) {
  if (
    extraConfig &&
    extraConfig.tracing &&
    extraConfig.tracing.http &&
    Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture)
  ) {
    extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture;
  }
  isActive = true;
};

exports.deactivate = function deactivate() {
  isActive = false;
};

exports.startHttpSpan = startSpan;

/**
 * @param {JSOpenSpanResult} openSpanResult
 * @param {import('http').ClientRequest & {_stExecuted?: boolean}} req
 * @param {import('http').ServerResponse} res
 */
function handleAction(openSpanResult, req, res, realEmit, originalThis, originalArgs, firstCall = true) {
  let actionRef;
  if (firstCall) {
    actionRef = superbrain.getAction(openSpanResult.action);
  } else {
    actionRef = superbrain.pollSpanAction(openSpanResult.openSpanResult);
  }
  if (actionRef.Action === superconsts.Action.Execute) {
    if (!res.writableEnded) {
      req._stExecuted = true;
      return realEmit.apply(originalThis, originalArgs);
    }
  }
  if (actionRef.Action === superconsts.Action.Wait) {
    const pollInterval = getWaitPollInterval(actionRef);
    return new Promise((resolve) => setTimeout(resolve, pollInterval).unref()).then(cls.ns.bind(() => {
      return handleAction(openSpanResult, req, res, realEmit, originalThis, originalArgs, false);
    }));
  }
  if (actionRef.Action === superconsts.Action.Delay) {
    const delayDuration = getDelayDuration(actionRef);
    return new Promise((resolve) => setTimeout(resolve, delayDuration).unref()).then(cls.ns.bind(() => {
      return function () {
        if (!res.writableEnded) {
          req._stExecuted = true;
          return realEmit.apply(originalThis, originalArgs);
        }
      }();
    }));
  }
  if (actionRef.Action === superconsts.Action.Reject) {
    if (!res.writableEnded) {
      const statusCode = getHttpRejectActionStatusCode(actionRef);
      res.statusCode = statusCode;
      res.statusMessage = 'SuperTenant rejected';
      res.setHeader('x-supertenant-rejected', 'true'); // TODO: move this to superconsts.
      res.end();
      req._stExecuted = true;
    }
  } else {
    dedupReportError('httpServer:UNKNOWN_ACTION', 'received unknown action ID', actionRef);
    if (!res.writableEnded) {
      return realEmit.apply(originalThis, originalArgs);
    }
  }
}

function shimEmit(realEmit) {
  return function (type, req, res) {
    if (type !== 'request' || !isActive) {
      return realEmit.apply(this, arguments);
    }

    const originalThis = this;
    const originalArgs = arguments;

    return cls.ns.runAndReturn(() => {
      if (req && req.on && req.addListener && req.emit) {
        cls.ns.bindEmitter(req);
      }
      if (res && res.on && res.addListener && res.emit) {
        cls.ns.bindEmitter(res);
      }

      const result = startSpan(req);
      let span = result.span;
      /** @type {JSOpenSpanResult} */
      let openSpanResult = result.openSpanResult;

      // Support for automatic client/back end EUM correlation: We add our key-value pair to the Server-Timing header
      // (the key intid is short for SuperTenant Trace ID). This abbreviation is small enough to not incur a notable
      // overhead while at the same time being unique enough to avoid name collisions.
      const serverTimingValue = `stid;desc=${span.t}`;
      res.setHeader('Server-Timing', serverTimingValue);
      shimmer.wrap(
        res,
        'setHeader',
        realSetHeader =>
          function shimmedSetHeader(key, value) {
            if (key.toLowerCase() === 'server-timing') {
              if (value == null) {
                return realSetHeader.call(this, key, serverTimingValue);
              } else if (Array.isArray(value)) {
                if (value.find(kv => kv.indexOf('stid;') === 0)) {
                  // If the application code sets stid, do not append another stid value. Actually, the application
                  // has no business setting an stid key-value pair, but it could happen theoretically for a proxy-like
                  // Node.js app (which blindly copies headers from downstream responses) in combination with a
                  // downstream service that is also instrumented by Instana (and adds the stid key-value pair).
                  return realSetHeader.apply(this, arguments);
                } else {
                  return realSetHeader.call(this, key, value.concat(serverTimingValue));
                }
              } else if (typeof value === 'string' && value.indexOf('stid;') >= 0) {
                // Do not add another stid key-value pair, see above.
                return realSetHeader.apply(this, arguments);
              } else {
                return realSetHeader.call(this, key, `${value}, ${serverTimingValue}`);
              }
            }
            return realSetHeader.apply(this, arguments);
          }
      );

      req.on('aborted', () => {
        finishSpan();
      });

      res.on('finish', () => {
        finishSpan();
      });

      res.on('close', () => {
        // This is purely a safe guard: in all known scenarios, one of the other events that finishes the HTTP entry
        // span should have been called before (res#finish or req#aborted).
        finishSpan();
      });

      function finishSpan() {
        // Always capture duration and HTTP response details, no matter if a higher level instrumentation
        // (like graphql.server) has modified the span or not.
        if (openSpanResult != null) {
          /** @type {SpanLabels} */
          let closeSpanData = {};
          if (!req._stExecuted) {
            // #860q6qe8z we need to mark the requet is completed so we won't try to execute it
            closeSpanData[superconsts.Label.SupertenantCanceled] = 'true';
          }
          closeSpanData[superconsts.Label.HttpStatus] = res.statusCode;
          if (res.statusCode >= 500) {
            closeSpanData[superconsts.Label.SupertenantError] = 'true';
          }
          superbrain.closeSpan(openSpanResult.spanId, closeSpanData);
        }

        span.d = Date.now() - span.ts;
        span.data.http = span.data.http || {};
        if (res.headersSent) {
          span.data.http.status = res.statusCode;
          span.data.http.header = mergeExtraHeadersFromServerResponseOrClientRequest(
            span.data.http.header,
            res,
            extraHttpHeadersToCapture
          );
        }

        if (!span.postponeTransmit) {
          // Do not overwrite the error count if an instrumentation with a higher priority (like graphql.server) has
          // already made a decision about it.
          span.ec = res.statusCode >= 500 ? 1 : 0;
        }

        span.transmit();
      }

      if (openSpanResult != null) {
        return handleAction(openSpanResult, req, res, realEmit, originalThis, originalArgs);
      } else {
        return realEmit.apply(originalThis, originalArgs);
      }
    });
  };
}

function startSpan(req, tenantId) {
    const headers = tracingHeaders.fromHttpRequest(req);
    const w3cTraceContext = headers.w3cTraceContext;

    if (typeof headers.level === 'string' && headers.level.indexOf('0') === 0) {
      cls.setTracingLevel('0');
      if (w3cTraceContext) {
        w3cTraceContext.disableSampling();
      }
    }

    if (w3cTraceContext) {
      // Ususally we commit the W3C trace context to CLS in start span, but in some cases (e.g. when suppressed),
      // we don't call startSpan, so we write to CLS here unconditionally. If we also write an updated trace context
      // later, the one written here will be overwritten.
      cls.setW3cTraceContext(w3cTraceContext);
    }

    //  removed check for tracing supressed as it is not used and is simpler that way

    const span = cls.startSpan(exports.spanName, constants.ENTRY, headers.traceId, headers.parentId, w3cTraceContext);
    tracingHeaders.setSpanAttributes(span, headers);

        // Capture the URL before application code gets access to the incoming message. Libraries like express manipulate
      // req.url when routers are used.
      const urlParts = req.url.split('?');
      if (urlParts.length >= 2) {
        urlParts[1] = filterParams(urlParts[1]);
      }
      const sanitizedUrl = sanitizeUrl(urlParts.shift());
      const reqHeaders = getExtraHeadersFromMessage(req);
      span.data.http = {
        method: req.method,
        url: sanitizedUrl,
        params: urlParts.length > 0 ? urlParts.join('?') : undefined,
        host: req.headers.host,
        header: reqHeaders
      };

      const incomingServiceName =
        span.data.http.header && span.data.http.header[constants.serviceNameHeaderNameLowerCase];
      if (incomingServiceName != null) {
        span.data.service = incomingServiceName;
      }

      if (!req.headers['x-supertenant-t']) {
        // In cases where we have started a fresh trace (that is, there is no X-SUPERTENANT-T in the incoming request
        // headers, we add the new trace ID to the incoming request so a customer's app can render it reliably into the
        // EUM snippet, see
        // eslint-disable-next-line max-len
        // https://www.ibm.com/docs/de/obi/current?topic=websites-backend-correlation#retrieve-the-backend-trace-id-in-nodejs
        req.headers['x-supertenant-t'] = span.t;
      }

      /** @type {JSOpenSpanResult} */
      let openSpanResult = null;
      const stTaskId = getOrCreateTask();
      if (stTaskId !== 0) {
        /** @type {SpanLabels} */
        let spanData = {};
        spanData[superconsts.Label.SupertenantResourceType] = superconsts.ResourceType.HttpServer;

        let resourceId = null;
        if (incomingServiceName != null && incomingServiceName !== undefined) {
          resourceId = incomingServiceName;
        } else if (serviceName != null && serviceName !== undefined) {
          resourceId = serviceName;
        } else if (req.headers.host != null && req.headers.host !== undefined) {
          resourceId = req.headers.host;
        }
        if (resourceId != null && resourceId !== undefined) {
          spanData[superconsts.Label.IntegrationModuleResourceId] = resourceId;
        }
        if (req.headers.host != null && req.headers.host !== undefined) {
          spanData[superconsts.Label.HttpHost] = req.headers.host;
        }
        spanData[superconsts.Label.HttpMethod] = req.method;
        spanData[superconsts.Label.HttpPath] = sanitizedUrl;
        if (urlParts.length >= 2) {
          spanData[superconsts.Label.HttpParams] = urlParts[1];
        }
        if (reqHeaders) {
          Object.keys(reqHeaders).forEach(headerName => {
            spanData[`${superconsts.Label.HttpHeaderPrefix}.${headerName}`] = reqHeaders[headerName];
            if (headerName.toLowerCase() === 'user-agent') {
              spanData[superconsts.Label.HttpUserAgent] = reqHeaders[headerName];
            }
          });
        }
        if (tenantId != null && tenantId !== undefined) {
          spanData[superconsts.Label.SupertenantTenantId] = tenantId;
        }
        openSpanResult = superbrain.openSpan(stTaskId, superconsts.SpanType.ServerRequest, spanData);
      }
      return {
        openSpanResult,
        span
      };
}