File

src/trace-api.ts

Description

StackdriverTracer exposes a number of methods to create trace spans and propagate trace context across asynchronous boundaries.

Implements

Tracer

Index

Properties
Methods

Constructor

constructor(name: string)

Constructs a new StackdriverTracer instance.

Parameters :
Name Type Optional Description
name string No

A string identifying this StackdriverTracer instance in logs.

Properties

Readonly constants
Default value : Constants
Readonly labels
Default value : TraceLabels
Readonly propagation
Type : Propagation
Default value : { extract: getHeader => { // If enabled, this.propagationMechanism is non-null. if (!this.enabled) { return null; } // OpenCensus propagation libraries expect span IDs to be size-16 hex // strings. In the future it might be worthwhile to change how span IDs // are stored in this library to avoid excessive base 10<->16 conversions. const result = this.headerPropagation!.extract({ getHeader: (...args) => { const result = getHeader(...args); if (result === null) { return; // undefined } return result; }, }); if (result) { result.spanId = util.hexToDec(result.spanId); } return result; }, inject: (setHeader, value) => { // If enabled, this.propagationMechanism is non-null. // Also, don't inject a falsey value. if (!this.enabled || !value) { return; } // Convert back to base-10 span IDs. See the wrapper for `extract` // for more details. value = Object.assign({}, value, { spanId: `0000000000000000${util.decToHex(value.spanId).slice(2)}`.slice( -16 ), }); this.headerPropagation!.inject({setHeader}, value); }, }
Readonly spanTypes
Default value : SpanType
Readonly traceContextUtils
Type : object
Default value : { encodeAsByteArray: util.serializeTraceContext, decodeFromByteArray: util.deserializeTraceContext, }

Methods

createChildSpan
createChildSpan(options?: SpanOptions)
Parameters :
Name Type Optional
options SpanOptions Yes
Returns : Span
disable
disable()

Disable this instance. This function is only for internal use and unit tests.

Returns : void
enable
enable(config: StackdriverTracerConfig, components: StackdriverTracerComponents)

Enables this instance. This function is only for internal use and unit tests. A separate TraceWriter instance should be initialized beforehand. be configured. by this instance.

Parameters :
Name Type Optional Description
config StackdriverTracerConfig No

An object specifying how this instance should be configured.

components StackdriverTracerComponents No

An collection of externally-instantiated objects used by this instance.

Returns : void
enhancedDatabaseReportingEnabled
enhancedDatabaseReportingEnabled()
Returns : boolean
getConfig
getConfig()
getCurrentContextId
getCurrentContextId()
Returns : string | null
getCurrentRootSpan
getCurrentRootSpan()
Returns : RootSpan
getProjectId
getProjectId()
Returns : Promise<string>
getResponseTraceContext
getResponseTraceContext(incomingTraceContext: util.TraceContext | null, isTraced: boolean)
Parameters :
Name Type Optional
incomingTraceContext util.TraceContext | null No
isTraced boolean No
Returns : { traceId: any; spanId: any; options: number; }
getWriterProjectId
getWriterProjectId()
Returns : string | null
isActive
isActive()

Returns whether the StackdriverTracer instance is active. This function is only for internal use and unit tests; under normal circumstances it will always return true.

Returns : boolean
isRealSpan
isRealSpan(span: Span)
Parameters :
Name Type Optional
span Span No
Returns : boolean
runInRootSpan
runInRootSpan(options: RootSpanOptions, fn: (span: RootSpan) => void)
Type parameters :
  • T
Parameters :
Name Type Optional
options RootSpanOptions No
fn function No
Returns : T
wrap
wrap(fn: Func)
Type parameters :
  • T
Parameters :
Name Type Optional
fn Func<T> No
Returns : Func<T>
wrapEmitter
wrapEmitter(emitter: EventEmitter)
Parameters :
Name Type Optional
emitter EventEmitter No
Returns : void
import {EventEmitter} from 'events';
import * as uuid from 'uuid';

import {cls, RootContext} from './cls';
import {OpenCensusPropagation, TracePolicy} from './config';
import {Constants, SpanType} from './constants';
import {Logger} from './logger';
import {
  Func,
  Propagation,
  RootSpan,
  RootSpanOptions,
  Span,
  SpanOptions,
  Tracer,
} from './plugin-types';
import {
  RootSpanData,
  UNCORRELATED_CHILD_SPAN,
  UNCORRELATED_ROOT_SPAN,
  DISABLED_CHILD_SPAN,
  DISABLED_ROOT_SPAN,
  UntracedRootSpanData,
} from './span-data';
import {TraceLabels} from './trace-labels';
import {traceWriter} from './trace-writer';
import {neverTrace} from './tracing-policy';
import * as util from './util';

/**
 * An interface describing configuration fields read by the StackdriverTracer
 * object. This includes fields read by the trace policy.
 */
export interface StackdriverTracerConfig {
  enhancedDatabaseReporting: boolean;
  rootSpanNameOverride: (path: string) => string;
  spansPerTraceSoftLimit: number;
  spansPerTraceHardLimit: number;
}

/**
 * A collection of externally-instantiated objects used by StackdriverTracer.
 */
export interface StackdriverTracerComponents {
  logger: Logger;
  tracePolicy: TracePolicy;
  propagation: OpenCensusPropagation;
}

/**
 * StackdriverTracer exposes a number of methods to create trace spans and
 * propagate trace context across asynchronous boundaries.
 */
export class StackdriverTracer implements Tracer {
  readonly constants = Constants;
  readonly labels = TraceLabels;
  readonly spanTypes = SpanType;
  readonly traceContextUtils = {
    encodeAsByteArray: util.serializeTraceContext,
    decodeFromByteArray: util.deserializeTraceContext,
  };
  readonly propagation: Propagation = {
    extract: getHeader => {
      // If enabled, this.propagationMechanism is non-null.
      if (!this.enabled) {
        return null;
      }
      // OpenCensus propagation libraries expect span IDs to be size-16 hex
      // strings. In the future it might be worthwhile to change how span IDs
      // are stored in this library to avoid excessive base 10<->16 conversions.
      const result = this.headerPropagation!.extract({
        getHeader: (...args) => {
          const result = getHeader(...args);
          if (result === null) {
            return; // undefined
          }
          return result;
        },
      });
      if (result) {
        result.spanId = util.hexToDec(result.spanId);
      }
      return result;
    },
    inject: (setHeader, value) => {
      // If enabled, this.propagationMechanism is non-null.
      // Also, don't inject a falsey value.
      if (!this.enabled || !value) {
        return;
      }
      // Convert back to base-10 span IDs. See the wrapper for `extract`
      // for more details.
      value = Object.assign({}, value, {
        spanId: `0000000000000000${util.decToHex(value.spanId).slice(2)}`.slice(
          -16
        ),
      });
      this.headerPropagation!.inject({setHeader}, value);
    },
  };

  private enabled = false;
  private pluginName: string;
  private pluginNameToLog: string;
  private logger: Logger | null = null;
  private config: StackdriverTracerConfig | null = null;
  private policy: TracePolicy | null = null;
  // The underlying propagation mechanism used by this.propagation.
  private headerPropagation: OpenCensusPropagation | null = null;

  /**
   * Constructs a new StackdriverTracer instance.
   * @param name A string identifying this StackdriverTracer instance in logs.
   */
  constructor(name: string) {
    this.pluginName = name;
    this.pluginNameToLog = this.pluginName ? this.pluginName : 'no-plugin-name';
    this.disable(); // disable immediately
  }

  /**
   * Enables this instance. This function is only for internal use and
   * unit tests. A separate TraceWriter instance should be initialized
   * beforehand.
   * @param config An object specifying how this instance should
   * be configured.
   * @param components An collection of externally-instantiated objects used
   * by this instance.
   * @private
   */
  enable(
    config: StackdriverTracerConfig,
    components: StackdriverTracerComponents
  ) {
    this.config = config;
    this.logger = components.logger;
    this.policy = components.tracePolicy;
    this.headerPropagation = components.propagation;
    this.enabled = true;
  }

  /**
   * Disable this instance. This function is only for internal use and
   * unit tests.
   * @private
   */
  disable() {
    // Even though plugins should be unpatched, setting a new policy that
    // never generates traces allows persisting wrapped methods (either because
    // they are already instantiated or the plugin doesn't unpatch them) to
    // short-circuit out of trace generation logic.
    this.policy = neverTrace();
    this.enabled = false;
  }

  /**
   * Returns whether the StackdriverTracer instance is active. This function is
   * only for internal use and unit tests; under normal circumstances it will
   * always return true.
   * @private
   */
  isActive(): boolean {
    return this.enabled;
  }

  enhancedDatabaseReportingEnabled(): boolean {
    return !!this.config && this.config.enhancedDatabaseReporting;
  }

  getConfig(): StackdriverTracerConfig {
    if (!this.config) {
      throw new Error('Configuration is not available.');
    }
    return this.config;
  }

  runInRootSpan<T>(options: RootSpanOptions, fn: (span: RootSpan) => T): T {
    if (!this.isActive()) {
      return fn(DISABLED_ROOT_SPAN);
    }

    options = options || {name: ''};

    // Don't create a root span if we are already in a root span
    const rootSpan = cls.get().getContext();
    if (rootSpan.type === SpanType.ROOT && !rootSpan.span.endTime) {
      this.logger!.warn(
        `TraceApi#runInRootSpan: [${this.pluginNameToLog}] Cannot create nested root spans.`
      );
      return fn(UNCORRELATED_ROOT_SPAN);
    }

    // Ensure that the trace context, if it exists, has an options field.
    const canonicalizeTraceContext = (
      traceContext?: util.TraceContext | null
    ) => {
      if (!traceContext) {
        return null;
      }
      if (traceContext.options !== undefined) {
        return traceContext as Required<util.TraceContext>;
      }
      return {
        traceId: traceContext.traceId,
        spanId: traceContext.spanId,
        options: 1,
      };
    };
    const traceContext = canonicalizeTraceContext(options.traceContext);

    // Consult the trace policy.
    const shouldTrace = this.policy!.shouldTrace({
      timestamp: Date.now(),
      url: options.url || '',
      method: options.method || '',
      traceContext,
      options,
    });

    const traceId = traceContext
      ? traceContext.traceId
      : uuid.v4().split('-').join('');
    let rootContext: RootSpan & RootContext;

    // Create an "untraced" root span (one that won't be published) if the
    // trace policy disallows it.
    if (!shouldTrace) {
      rootContext = new UntracedRootSpanData(traceId);
    } else {
      // Create a new root span, and invoke fn with it.
      rootContext = new RootSpanData(
        // Trace object
        {
          projectId: '',
          traceId,
          spans: [],
        },
        // Span name
        this.config!.rootSpanNameOverride(options.name),
        // Parent span ID
        traceContext ? traceContext.spanId : '0',
        // Number of stack frames to skip
        options.skipFrames || 0
      );
    }

    return cls.get().runWithContext(() => {
      return fn(rootContext);
    }, rootContext);
  }

  getCurrentRootSpan(): RootSpan {
    if (!this.isActive()) {
      return DISABLED_ROOT_SPAN;
    }
    return cls.get().getContext();
  }

  getCurrentContextId(): string | null {
    // In v3, this will be deprecated for getCurrentRootSpan.
    const traceContext = this.getCurrentRootSpan().getTraceContext();
    return traceContext ? traceContext.traceId : null;
  }

  getProjectId(): Promise<string> {
    if (traceWriter.exists() && traceWriter.get().isActive) {
      return traceWriter.get().getProjectId();
    } else {
      return Promise.reject(
        new Error('The Project ID could not be retrieved.')
      );
    }
  }

  getWriterProjectId(): string | null {
    // In v3, this will be deprecated for getProjectId.
    if (traceWriter.exists() && traceWriter.get().isActive) {
      return traceWriter.get().projectId;
    } else {
      return null;
    }
  }

  createChildSpan(options?: SpanOptions): Span {
    if (!this.isActive()) {
      return DISABLED_CHILD_SPAN;
    }

    options = options || {name: ''};
    const rootSpan = cls.get().getContext();
    if (rootSpan.type === SpanType.ROOT) {
      if (rootSpan.span.endTime) {
        // A closed root span suggests that we either have context confusion or
        // some work is being done after the root request has been completed.
        // The first case could lead to a memory leak, if somehow all spans end
        // up getting misattributed to the same root span – we get a root span
        // with continuously growing number of child spans. The second case
        // seems to have some value, but isn't representable. The user probably
        // needs a custom outer span that encompasses the entirety of work.
        this.logger!.warn(
          `TraceApi#createChildSpan: [${this.pluginNameToLog}] Creating phantom child span [${options.name}] because root span [${rootSpan.span.name}] was already closed.`
        );
        return UNCORRELATED_CHILD_SPAN;
      }
      if (rootSpan.trace.spans.length >= this.config!.spansPerTraceHardLimit) {
        // As in the previous case, a root span with a large number of child
        // spans suggests a memory leak stemming from context confusion. This
        // is likely due to userspace task queues or Promise implementations.
        this.logger!.warn(
          `TraceApi#createChildSpan: [${
            this.pluginNameToLog
          }] Creating phantom child span [${
            options.name
          }] because the trace with root span [${
            rootSpan.span.name
          }] has reached a limit of ${
            this.config!.spansPerTraceHardLimit
          } spans. This is likely a memory leak.`
        );
        this.logger!.warn(
          [
            'TraceApi#createChildSpan: Please see',
            'https://github.com/googleapis/cloud-trace-nodejs/wiki',
            'for details and suggested actions.',
          ].join(' ')
        );
        return UNCORRELATED_CHILD_SPAN;
      }
      if (rootSpan.trace.spans.length === this.config!.spansPerTraceSoftLimit) {
        // As in the previous case, a root span with a large number of child
        // spans suggests a memory leak stemming from context confusion. This
        // is likely due to userspace task queues or Promise implementations.

        // Note that since child spans can be created by users directly on a
        // RootSpanData instance, this block might be skipped because it only
        // checks equality -- this is OK because no automatic tracing plugin
        // uses the RootSpanData API directly.
        this.logger!.warn(
          `TraceApi#createChildSpan: [${
            this.pluginNameToLog
          }] Adding child span [${
            options.name
          }] will cause the trace with root span [${
            rootSpan.span.name
          }] to contain more than ${
            this.config!.spansPerTraceSoftLimit
          } spans. This is likely a memory leak.`
        );
        this.logger!.warn(
          [
            'TraceApi#createChildSpan: Please see',
            'https://github.com/googleapis/cloud-trace-nodejs/wiki',
            'for details and suggested actions.',
          ].join(' ')
        );
      }
      // Create a new child span and return it.
      const childContext = rootSpan.createChildSpan({
        name: options.name,
        skipFrames: options.skipFrames ? options.skipFrames + 1 : 1,
      });
      this.logger!.info(
        `TraceApi#createChildSpan: [${this.pluginNameToLog}] Created child span [${options.name}]`
      );
      return childContext;
    } else if (rootSpan.type === SpanType.UNSAMPLED) {
      // "Untraced" child spans don't incur a memory penalty.
      return rootSpan.createChildSpan();
    } else if (rootSpan.type === SpanType.DISABLED) {
      return DISABLED_CHILD_SPAN;
    } else {
      // Context was lost.
      this.logger!.warn(
        `TraceApi#createChildSpan: [${this.pluginNameToLog}] Creating phantom child span [${options.name}] because there is no root span.`
      );
      return UNCORRELATED_CHILD_SPAN;
    }
  }

  isRealSpan(span: Span): boolean {
    return span.type === SpanType.ROOT || span.type === SpanType.CHILD;
  }

  getResponseTraceContext(
    incomingTraceContext: util.TraceContext | null,
    isTraced: boolean
  ) {
    if (!this.isActive() || !incomingTraceContext) {
      return null;
    }
    return {
      traceId: incomingTraceContext.traceId,
      spanId: incomingTraceContext.spanId,
      options: (incomingTraceContext.options || 0) & (isTraced ? 1 : 0),
    };
  }

  wrap<T>(fn: Func<T>): Func<T> {
    if (!this.isActive()) {
      return fn;
    }

    return cls.get().bindWithCurrentContext(fn);
  }

  wrapEmitter(emitter: EventEmitter): void {
    if (!this.isActive()) {
      return;
    }

    cls.get().patchEmitterToPropagateContext(emitter);
  }
}

results matching ""

    No results matching ""