File

src/util.ts

Index

Methods

Methods

disable
disable()
Defined in src/util.ts:59
Returns : void
enable
enable()
Defined in src/util.ts:58
Returns : void
import * as path from 'path';
import * as sourceMapSupport from 'source-map-support';

const {
  hexToDec,
  decToHex,
}: // eslint-disable-next-line @typescript-eslint/no-var-requires
{[key: string]: (input: string) => string} = require('hex2dec');

export {hexToDec, decToHex};

// This symbol must be exported (for now).
// See: https://github.com/Microsoft/TypeScript/issues/20080
export const kSingleton = Symbol();

/**
 * Trace API expects stack frames to be a JSON string with the following
 * structure:
 * STACK_TRACE := { "stack_frame" : [ FRAMES ] }
 * FRAMES := { "class_name" : CLASS_NAME, "file_name" : FILE_NAME,
 *             "line_number" : LINE_NUMBER, "method_name" : METHOD_NAME }*
 *
 * While the API doesn't expect a column_number at this point, it does accept,
 * and ignore it.
 */
export interface StackFrame {
  class_name?: string;
  method_name?: string;
  file_name?: string;
  line_number?: number;
  column_number?: number;
}

export interface Constructor<T, ConfigType, LoggerType> {
  new (config: ConfigType, logger: LoggerType): T;
  prototype: T;
  name: string;
}

export const FORCE_NEW = Symbol('force-new');

export type Forceable<T> = T & {[FORCE_NEW]?: boolean};

export interface Component {
  enable(): void;
  disable(): void;
}

/**
 * A class that provides access to a singleton.
 * We assume that any such singleton is always constructed with two arguments:
 * An arbitrary configuration object and a logger.
 * Instances of this type should only be constructed in module scope.
 */
export class Singleton<T, ConfigType, LoggerType> {
  // Note: private[symbol] is enforced by clang-format.
  private [kSingleton]: T | null = null;

  constructor(private implementation: Constructor<T, ConfigType, LoggerType>) {}

  create(config: Forceable<ConfigType>, logger: LoggerType): T {
    if (!this[kSingleton] || config[FORCE_NEW]) {
      const s = this[kSingleton] as Partial<Component>;
      if (s && s.disable) {
        s.disable();
      }
      this[kSingleton] = new this.implementation(config, logger);
      return this[kSingleton]!;
    } else {
      throw new Error(`${this.implementation.name} has already been created.`);
    }
  }

  get(): T {
    if (!this[kSingleton]) {
      throw new Error(`${this.implementation.name} has not yet been created.`);
    }
    return this[kSingleton]!;
  }

  exists(): boolean {
    return !!this[kSingleton];
  }
}

/**
 * Returns the last parameter that is not null, undefined, or NaN.
 * @param defaultValue The first parameter. This must not be null/undefined/NaN.
 * @param otherValues Other parameters, which may be null/undefined/NaN.
 */
export function lastOf<T>(
  defaultValue: T,
  ...otherValues: Array<T | null | undefined>
): T {
  for (let i = otherValues.length - 1; i >= 0; i--) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    if (
      otherValues[i] !== null &&
      otherValues[i] !== undefined &&
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (typeof otherValues[i] !== 'number' || !isNaN(otherValues[i] as any))
    ) {
      return otherValues[i] as T;
    }
  }
  return defaultValue;
}

/**
 * Truncates the provided `string` to be at most `length` bytes
 * after utf8 encoding and the appending of '...'.
 * We produce the result by iterating over input characters to
 * avoid truncating the string potentially producing partial unicode
 * characters at the end.
 */
export function truncate(str: string, length: number) {
  if (Buffer.byteLength(str, 'utf8') <= length) {
    return str;
  }
  str = str.substr(0, length - 3);
  while (Buffer.byteLength(str, 'utf8') > length - 3) {
    str = str.substr(0, str.length - 1);
  }
  return str + '...';
}

// Includes support for npm '@org/name' packages
// Regex: .*?node_modules(?!.*node_modules)\/(@[^\/]*\/[^\/]*|[^\/]*).*
// Tests: https://regex101.com/r/lW2bE3/6
const moduleRegex = new RegExp(
  [
    '.*?node_modules(?!.*node_modules)\\',
    '(@[^\\',
    ']*\\',
    '[^\\',
    ']*|[^\\',
    ']*).*',
  ].join(path.sep)
);

export interface TraceContext {
  traceId: string;
  spanId: string;
  options?: number;
}

/**
 * Parse a cookie-style header string to extract traceId, spandId and options
 * ex: '123456/667;o=3'
 * -> {traceId: '123456', spanId: '667', options: '3'}
 * note that we ignore trailing garbage if there is more than one '='
 * Returns null if traceId or spanId could not be found.
 *
 * @param str string representation of the trace headers
 * @return object with keys. null if there is a problem.
 */
export function parseContextFromHeader(str: string): TraceContext | null {
  if (!str) {
    return null;
  }
  const matches = str.match(/^([0-9a-fA-F]+)(?:\/([0-9]+))(?:;o=(.*))?/);
  if (
    !matches ||
    matches.length !== 4 ||
    matches[0] !== str ||
    (matches[2] && isNaN(Number(matches[2])))
  ) {
    return null;
  }
  return {
    traceId: matches[1],
    spanId: matches[2],
    options: isNaN(Number(matches[3])) ? undefined : Number(matches[3]),
  };
}

/**
 * Generates a trace context header value that can be used
 * to follow the associated request through other Google services.
 *
 * @param traceContext An object with information sufficient for creating a
 *        serialized trace context.
 */
export function generateTraceContext(traceContext: TraceContext): string {
  if (!traceContext) {
    return '';
  }
  let header = `${traceContext.traceId}/${traceContext.spanId}`;
  if (typeof traceContext.options !== 'undefined') {
    header += `;o=${traceContext.options}`;
  }
  return header;
}

/**
 * Retrieves a package name from the full import path.
 * For example:
 *   './node_modules/bar/index/foo.js' => 'bar'
 *
 * @param path The full import path.
 */
export function packageNameFromPath(importPath: string) {
  const matches = moduleRegex.exec(importPath);
  return matches && matches.length > 1 ? matches[1] : null;
}

/**
 * Creates a StackFrame object containing a structured stack trace.
 * @param numFrames The number of frames to retain.
 * @param skipFrames The number of top-most frames to remove.
 * @param constructorOpt A function passed to Error.captureStackTrace, which
 *   causes it to ignore the frames above the top-most call to this function.
 */
export function createStackTrace(
  numFrames: number,
  skipFrames: number,
  constructorOpt?: Function
): StackFrame[] {
  // This is a mechanism to get the structured stack trace out of V8.
  // prepareStackTrace is called the first time the Error#stack property is
  // accessed. The original behavior is to format the stack as an exception
  // throw, which is not what we like. We customize it.
  //
  // See: https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
  //
  if (numFrames === 0) {
    return [];
  }

  const origLimit = Error.stackTraceLimit;
  Error.stackTraceLimit = numFrames + skipFrames;

  const origPrepare = Error.prepareStackTrace;
  Error.prepareStackTrace = (
    error: Error,
    structured: NodeJS.CallSite[]
  ): NodeJS.CallSite[] => {
    return structured.map(sourceMapSupport.wrapCallSite);
  };
  const e: {stack?: NodeJS.CallSite[]} = {};
  Error.captureStackTrace(e, constructorOpt);

  const stackFrames: StackFrame[] = [];
  if (e.stack) {
    e.stack.forEach((callSite, i) => {
      if (i < skipFrames) {
        return;
      }
      // TODO(kjin): Check if callSite getters actually return null or
      // undefined. Docs say undefined but we guard it here just in case.
      const functionName = callSite.getFunctionName();
      const methodName = callSite.getMethodName();
      const name =
        methodName && functionName
          ? functionName + ' [as ' + methodName + ']'
          : functionName || methodName || '<anonymous function>';
      const stackFrame: StackFrame = {
        method_name: name,
        file_name: callSite.getFileName() || undefined,
        line_number: callSite.getLineNumber() || undefined,
        column_number: callSite.getColumnNumber() || undefined,
      };
      stackFrames.push(stackFrame);
    });
  }
  Error.stackTraceLimit = origLimit;
  Error.prepareStackTrace = origPrepare;
  return stackFrames;
}

/**
 * Serialize the given trace context into a Buffer.
 * @param traceContext The trace context to serialize.
 */
export function serializeTraceContext(traceContext: TraceContext): Buffer {
  //  0           1           2
  //  0 1 2345678901234567 8 90123456 7 8
  // -------------------------------------
  // | | |                | |        | | |
  // -------------------------------------
  //  ^ ^      ^           ^    ^     ^ ^
  //  | |      |           |    |     | `-- options value (traceContext.options)
  //  | |      |           |    |     `---- options field ID (2)
  //  | |      |           |    `---------- spanID value (traceConext.spanID)
  //  | |      |           `--------------- spanID field ID (1)
  //  | |      `--------------------------- traceID value (traceContext.traceID)
  //  | `---------------------------------- traceID field ID (0)
  //  `------------------------------------ version (0)
  const result = Buffer.alloc(29, 0);
  result.write(traceContext.traceId, 2, 16, 'hex');
  result.writeUInt8(1, 18);
  // Convert Span ID from decimal to base 16 representation, then left pad into
  // a length-16 hex string. (decToHex prepends its output with '0x', so we
  // also slice that off.)
  const base16SpanId = `0000000000000000${decToHex(traceContext.spanId).slice(
    2
  )}`.slice(-16);
  result.write(base16SpanId, 19, 8, 'hex');
  result.writeUInt8(2, 27);
  result.writeUInt8(traceContext.options || 0, 28);
  return result;
}

/**
 * Deseralize the given trace context from binary encoding. If the input is a
 * Buffer of incorrect size or unexpected fields, then this function will return
 * null.
 * @param buffer The trace context to deserialize.
 */
export function deserializeTraceContext(buffer: Buffer): TraceContext | null {
  const result: TraceContext = {traceId: '', spanId: ''};
  // Length must be 29.
  if (buffer.length !== 29) {
    return null;
  }
  // Check version and field numbers.
  if (
    buffer.readUInt8(0) !== 0 ||
    buffer.readUInt8(1) !== 0 ||
    buffer.readUInt8(18) !== 1 ||
    buffer.readUInt8(27) !== 2
  ) {
    return null;
  }
  // See serializeTraceContext for byte offsets.
  result.traceId = buffer.slice(2, 18).toString('hex');
  result.spanId = hexToDec(buffer.slice(19, 27).toString('hex'));
  result.options = buffer.readUInt8(28);
  return result;
}

results matching ""

    No results matching ""