diff --git a/package.json b/package.json index aa0e67b3b..6a43ad4a2 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ }, "dependencies": { "@google-cloud/common": "^1.0.0", + "@opencensus/propagation-stackdriver": "0.0.11", "builtin-modules": "^3.0.0", "console-log-level": "^1.4.0", "continuation-local-storage": "^3.2.1", diff --git a/src/config.ts b/src/config.ts index e1243dc6c..cba72f973 100644 --- a/src/config.ts +++ b/src/config.ts @@ -52,6 +52,24 @@ export interface TracePolicy { shouldTrace: (requestDetails: RequestDetails) => boolean; } +export type GetHeaderFunction = { + getHeader: (key: string) => string[]|string|undefined; +}; +export type SetHeaderFunction = { + setHeader: (key: string, value: string) => void; +}; +export interface OpenCensusPropagation { + extract: (getHeader: GetHeaderFunction) => { + traceId: string; + spanId: string; + options?: number + } | null; + inject: (setHeader: SetHeaderFunction, traceContext: { + traceId: string; spanId: string; + options?: number + }) => void; +} + /** * Available configuration options. All fields are optional. See the * defaultConfig object defined in this file for default assigned values. @@ -195,6 +213,13 @@ export interface Config { */ tracePolicy?: TracePolicy; + /** + * If specified, the Trace Agent will use this context header propagation + * implementation instead of @opencensus/propagation-stackdriver, the default + * trace context header format. + */ + propagation?: OpenCensusPropagation; + /** * Buffer the captured traces for `flushDelaySeconds` seconds before * publishing to the Stackdriver Trace API, unless the buffer fills up first. diff --git a/src/index.ts b/src/index.ts index 3f1761707..e7ece4e57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,7 +146,10 @@ function initConfig(userConfig: Forceable): Forceable { contextHeaderBehavior: mergedConfig.contextHeaderBehavior as TraceContextHeaderBehavior }, - overrides: {tracePolicy: mergedConfig.tracePolicy} + overrides: { + tracePolicy: mergedConfig.tracePolicy, + propagation: mergedConfig.propagation + } }; } diff --git a/src/plugin-types.ts b/src/plugin-types.ts index aaeb7bf2a..1669731fe 100644 --- a/src/plugin-types.ts +++ b/src/plugin-types.ts @@ -38,11 +38,10 @@ export interface TraceAgentExtension { */ export interface Span { /** - * Gets the current trace context serialized as a string, or an empty string - * if it can't be generated. - * @return The stringified trace context. + * Gets the current trace context, or null if it can't be retrieved. + * @return The trace context. */ - getTraceContext(): string; + getTraceContext(): TraceContext|null; /** * Adds a key-value pair as a label to the trace span. The value will be @@ -106,10 +105,9 @@ export interface RootSpanOptions extends SpanOptions { /* A Method associated with the root span, if applicable. */ method?: string; /** - * The serialized form of an object that contains information about an - * existing trace context, if it exists. + * An existing trace context, if it exists. */ - traceContext?: string|null; + traceContext?: TraceContext|null; } export interface Tracer { @@ -191,21 +189,22 @@ export interface Tracer { isRealSpan(span: Span): boolean; /** - * Generates a stringified trace context that should be set as the trace + * Generates a trace context object that should be set as the trace * context header in a response to an incoming web request. This value is * based on the trace context header value in the corresponding incoming * request, as well as the result from the local trace policy on whether this * request will be traced or not. * @param incomingTraceContext The trace context that was attached to * the incoming web request, or null if the incoming request didn't have one. - * @param isTraced Whether the incoming was traced. This is determined + * @param isTraced Whether the incoming request was traced. This is determined * by the local tracing policy. * @returns If the response should contain the trace context within its - * header, the string to be set as this header's value. Otherwise, an empty - * string. + * header, the context object to be serialized as this header's value. + * Otherwise, null. */ - getResponseTraceContext(incomingTraceContext: string|null, isTraced: boolean): - string; + getResponseTraceContext( + incomingTraceContext: TraceContext|null, isTraced: boolean): TraceContext + |null; /** * Binds the trace context to the given function. @@ -233,11 +232,21 @@ export interface Tracer { readonly spanTypes: typeof SpanType; /** A collection of functions for encoding and decoding trace context. */ readonly traceContextUtils: { - encodeAsString: (ctx: TraceContext) => string; - decodeFromString: (str: string) => TraceContext | null; encodeAsByteArray: (ctx: TraceContext) => Buffer; decodeFromByteArray: (buf: Buffer) => TraceContext | null; }; + /** + * A collection of functions for dealing with trace context in HTTP headers. + */ + readonly propagation: Propagation; +} + +export type GetHeaderFunction = (key: string) => string[]|string|null|undefined; +export type SetHeaderFunction = (key: string, value: string) => void; +export interface Propagation { + extract: (getHeader: GetHeaderFunction) => TraceContext | null; + inject: + (setHeader: SetHeaderFunction, traceContext: TraceContext|null) => void; } export interface Monkeypatch { diff --git a/src/plugins/plugin-connect.ts b/src/plugins/plugin-connect.ts index 5520ec0b5..2d329f738 100644 --- a/src/plugins/plugin-connect.ts +++ b/src/plugins/plugin-connect.ts @@ -28,14 +28,6 @@ type Request = IncomingMessage&{originalUrl?: string}; const SUPPORTED_VERSIONS = '3.x'; -function getFirstHeader(req: IncomingMessage, key: string): string|null { - let headerValue = req.headers[key] || null; - if (headerValue && typeof headerValue !== 'string') { - headerValue = headerValue[0]; - } - return headerValue; -} - function createMiddleware(api: PluginTypes.Tracer): connect_3.NextHandleFunction { return function middleware(req: Request, res, next) { @@ -43,17 +35,16 @@ function createMiddleware(api: PluginTypes.Tracer): name: req.originalUrl ? (urlParse(req.originalUrl).pathname || '') : '', url: req.originalUrl, method: req.method, - traceContext: - getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), + traceContext: api.propagation.extract((key) => req.headers[key]), skipFrames: 1 }; api.runInRootSpan(options, (root) => { // Set response trace context. const responseTraceContext = api.getResponseTraceContext( - options.traceContext || null, api.isRealSpan(root)); + options.traceContext, api.isRealSpan(root)); if (responseTraceContext) { - res.setHeader( - api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + api.propagation.inject( + (k, v) => res.setHeader(k, v), responseTraceContext); } if (!api.isRealSpan(root)) { diff --git a/src/plugins/plugin-express.ts b/src/plugins/plugin-express.ts index 34fc7df01..d691432e7 100644 --- a/src/plugins/plugin-express.ts +++ b/src/plugins/plugin-express.ts @@ -35,9 +35,9 @@ function patchModuleRoot(express: Express4Module, api: PluginTypes.Tracer) { function middleware( req: express_4.Request, res: express_4.Response, next: express_4.NextFunction) { - const options: PluginTypes.RootSpanOptions = { + const options = { name: req.path, - traceContext: req.get(api.constants.TRACE_CONTEXT_HEADER_NAME), + traceContext: api.propagation.extract((key) => req.get(key)), url: req.originalUrl, method: req.method, skipFrames: 1 @@ -45,9 +45,10 @@ function patchModuleRoot(express: Express4Module, api: PluginTypes.Tracer) { api.runInRootSpan(options, (rootSpan) => { // Set response trace context. const responseTraceContext = api.getResponseTraceContext( - options.traceContext || null, api.isRealSpan(rootSpan)); + options.traceContext, api.isRealSpan(rootSpan)); if (responseTraceContext) { - res.set(api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + api.propagation.inject( + (k, v) => res.setHeader(k, v), responseTraceContext); } if (!api.isRealSpan(rootSpan)) { diff --git a/src/plugins/plugin-grpc.ts b/src/plugins/plugin-grpc.ts index 76cbe7464..ebf97c4b8 100644 --- a/src/plugins/plugin-grpc.ts +++ b/src/plugins/plugin-grpc.ts @@ -19,7 +19,7 @@ import * as grpcModule from 'grpc'; // for types only. import {Client, MethodDefinition, ServerReadableStream, ServerUnaryCall, StatusObject} from 'grpc'; import * as shimmer from 'shimmer'; -import {Plugin, RootSpan, RootSpanOptions, Span, Tracer} from '../plugin-types'; +import {Plugin, RootSpan, RootSpanOptions, Span, TraceContext, Tracer} from '../plugin-types'; // Re-definition of Metadata with private fields type Metadata = grpcModule.Metadata&{ @@ -116,9 +116,7 @@ function patchClient(client: ClientModule, api: Tracer) { * a falsey value, metadata will not be modified. */ function setTraceContextFromString( - metadata: Metadata, stringifiedTraceContext: string): void { - const traceContext = - api.traceContextUtils.decodeFromString(stringifiedTraceContext); + metadata: Metadata, traceContext: TraceContext|null): void { if (traceContext) { const metadataValue = api.traceContextUtils.encodeAsByteArray(traceContext); @@ -292,12 +290,11 @@ function unpatchClient(client: ClientModule) { function patchServer(server: ServerModule, api: Tracer) { /** * Returns a trace context on a Metadata object if it exists and is - * well-formed, or null otherwise. The result will be encoded as a string. + * well-formed, or null otherwise. * @param metadata The Metadata object from which trace context should be * retrieved. */ - function getStringifiedTraceContext(metadata: grpcModule.Metadata): string| - null { + function getTraceContext(metadata: grpcModule.Metadata): TraceContext|null { const metadataValue = metadata.getMap()[api.constants.TRACE_CONTEXT_GRPC_METADATA_NAME] as Buffer; @@ -305,13 +302,7 @@ function patchServer(server: ServerModule, api: Tracer) { if (!metadataValue) { return null; } - const traceContext = - api.traceContextUtils.decodeFromByteArray(metadataValue); - // Value is malformed. - if (!traceContext) { - return null; - } - return api.traceContextUtils.encodeAsString(traceContext); + return api.traceContextUtils.decodeFromByteArray(metadataValue); } /** @@ -356,7 +347,7 @@ function patchServer(server: ServerModule, api: Tracer) { const rootSpanOptions = { name: requestName, url: requestName, - traceContext: getStringifiedTraceContext(call.metadata), + traceContext: getTraceContext(call.metadata), skipFrames: SKIP_FRAMES }; return api.runInRootSpan(rootSpanOptions, (rootSpan) => { @@ -410,7 +401,7 @@ function patchServer(server: ServerModule, api: Tracer) { const rootSpanOptions = { name: requestName, url: requestName, - traceContext: getStringifiedTraceContext(stream.metadata), + traceContext: getTraceContext(stream.metadata), skipFrames: SKIP_FRAMES } as RootSpanOptions; return api.runInRootSpan(rootSpanOptions, (rootSpan) => { @@ -472,7 +463,7 @@ function patchServer(server: ServerModule, api: Tracer) { const rootSpanOptions = { name: requestName, url: requestName, - traceContext: getStringifiedTraceContext(stream.metadata), + traceContext: getTraceContext(stream.metadata), skipFrames: SKIP_FRAMES } as RootSpanOptions; return api.runInRootSpan(rootSpanOptions, (rootSpan) => { @@ -532,7 +523,7 @@ function patchServer(server: ServerModule, api: Tracer) { const rootSpanOptions = { name: requestName, url: requestName, - traceContext: getStringifiedTraceContext(stream.metadata), + traceContext: getTraceContext(stream.metadata), skipFrames: SKIP_FRAMES } as RootSpanOptions; return api.runInRootSpan(rootSpanOptions, (rootSpan) => { diff --git a/src/plugins/plugin-hapi.ts b/src/plugins/plugin-hapi.ts index af2343a2b..bf46329eb 100644 --- a/src/plugins/plugin-hapi.ts +++ b/src/plugins/plugin-hapi.ts @@ -34,34 +34,26 @@ type Hapi17Request = hapi_17.Request&{ _execute: Hapi17RequestExecutePrivate; }; -function getFirstHeader(req: IncomingMessage, key: string): string|null { - let headerValue = req.headers[key] || null; - if (headerValue && typeof headerValue !== 'string') { - headerValue = headerValue[0]; - } - return headerValue; -} - function instrument( api: PluginTypes.Tracer, request: hapi_16.Request|hapi_17.Request, continueCb: () => T): T { const req = request.raw.req; const res = request.raw.res; const originalEnd = res.end; - const options: PluginTypes.RootSpanOptions = { + const options = { name: req.url ? (urlParse(req.url).pathname || '') : '', url: req.url, method: req.method, - traceContext: getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), + traceContext: api.propagation.extract(key => req.headers[key]), skipFrames: 2 }; return api.runInRootSpan(options, (root) => { // Set response trace context. - const responseTraceContext = api.getResponseTraceContext( - options.traceContext || null, api.isRealSpan(root)); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext, api.isRealSpan(root)); if (responseTraceContext) { - res.setHeader( - api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + api.propagation.inject( + (k, v) => res.setHeader(k, v), responseTraceContext); } if (!api.isRealSpan(root)) { diff --git a/src/plugins/plugin-http.ts b/src/plugins/plugin-http.ts index 25da9afe5..9d090b501 100644 --- a/src/plugins/plugin-http.ts +++ b/src/plugins/plugin-http.ts @@ -142,9 +142,11 @@ function makeRequestTrace( // headers. options = Object.assign({}, options) as ClientRequestArgs; options.headers = Object.assign({}, options.headers); + const headers = options.headers; // Inject the trace context header. - options.headers[api.constants.TRACE_CONTEXT_HEADER_NAME] = - span.getTraceContext(); + api.propagation.inject((key, value) => { + headers[key] = value; + }, span.getTraceContext()); } const req = request(options, (res) => { @@ -188,19 +190,20 @@ function makeRequestTrace( // Inject the trace context header, but only if it wasn't already injected // earlier. if (!traceHeaderPreinjected) { - try { - req.setHeader( - api.constants.TRACE_CONTEXT_HEADER_NAME, span.getTraceContext()); - } catch (e) { - if (e.code === ERR_HTTP_HEADERS_SENT || - e.message === ERR_HTTP_HEADERS_SENT_MSG) { - // Swallow the error. - // This would happen in the pathological case where the Expect header - // exists but is not detected by hasExpectHeader. - } else { - throw e; + api.propagation.inject((key, value) => { + try { + req.setHeader(key, value); + } catch (e) { + if (e.code === ERR_HTTP_HEADERS_SENT || + e.message === ERR_HTTP_HEADERS_SENT_MSG) { + // Swallow the error. + // This would happen in the pathological case where the Expect + // header exists but is not detected by hasExpectHeader. + } else { + throw e; + } } - } + }, span.getTraceContext()); } return req; }; diff --git a/src/plugins/plugin-http2.ts b/src/plugins/plugin-http2.ts index 71ebe5927..9a7f59447 100644 --- a/src/plugins/plugin-http2.ts +++ b/src/plugins/plugin-http2.ts @@ -97,8 +97,8 @@ function makeRequestTrace( api.labels.HTTP_METHOD_LABEL_KEY, extractMethodName(newHeaders)); requestLifecycleSpan.addLabel( api.labels.HTTP_URL_LABEL_KEY, extractUrl(authority, newHeaders)); - newHeaders[api.constants.TRACE_CONTEXT_HEADER_NAME] = - requestLifecycleSpan.getTraceContext(); + api.propagation.inject( + (k, v) => newHeaders[k] = v, requestLifecycleSpan.getTraceContext()); const stream: http2.ClientHttp2Stream = request.call( this, newHeaders, ...Array.prototype.slice.call(arguments, 1)); api.wrapEmitter(stream); diff --git a/src/plugins/plugin-koa.ts b/src/plugins/plugin-koa.ts index 0c8eb8559..d8b11c69f 100644 --- a/src/plugins/plugin-koa.ts +++ b/src/plugins/plugin-koa.ts @@ -40,14 +40,6 @@ type CreateMiddlewareFn = (api: PluginTypes.Tracer) => T; // propagateContext flag. The type of "next" differs between Koa 1 and 2. type GetNextFn = (propagateContext: boolean) => T; -function getFirstHeader(req: IncomingMessage, key: string): string|null { - let headerValue = req.headers[key] || null; - if (headerValue && typeof headerValue !== 'string') { - headerValue = headerValue[0]; - } - return headerValue; -} - function startSpanForRequest( api: PluginTypes.Tracer, ctx: KoaContext, getNext: GetNextFn): T { const req = ctx.req; @@ -57,16 +49,16 @@ function startSpanForRequest( name: req.url ? (urlParse(req.url).pathname || '') : '', url: req.url, method: req.method, - traceContext: getFirstHeader(req, api.constants.TRACE_CONTEXT_HEADER_NAME), + traceContext: api.propagation.extract(key => req.headers[key]), skipFrames: 2 }; return api.runInRootSpan(options, root => { // Set response trace context. - const responseTraceContext = api.getResponseTraceContext( - options.traceContext || null, api.isRealSpan(root)); + const responseTraceContext = + api.getResponseTraceContext(options.traceContext, api.isRealSpan(root)); if (responseTraceContext) { - res.setHeader( - api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + api.propagation.inject( + (k, v) => res.setHeader(k, v), responseTraceContext); } if (!api.isRealSpan(root)) { diff --git a/src/plugins/plugin-restify.ts b/src/plugins/plugin-restify.ts index bb02b5ff5..c17f1ab0f 100644 --- a/src/plugins/plugin-restify.ts +++ b/src/plugins/plugin-restify.ts @@ -52,7 +52,7 @@ function patchRestify(restify: Restify5, api: PluginTypes.Tracer) { name: req.path(), url: req.url, method: req.method, - traceContext: req.header(api.constants.TRACE_CONTEXT_HEADER_NAME), + traceContext: api.propagation.extract(key => req.header(key)), skipFrames: 1 }; @@ -61,8 +61,8 @@ function patchRestify(restify: Restify5, api: PluginTypes.Tracer) { const responseTraceContext = api.getResponseTraceContext( options.traceContext, api.isRealSpan(rootSpan)); if (responseTraceContext) { - res.header( - api.constants.TRACE_CONTEXT_HEADER_NAME, responseTraceContext); + api.propagation.inject( + (k, v) => res.setHeader(k, v), responseTraceContext); } if (!api.isRealSpan(rootSpan)) { diff --git a/src/span-data.ts b/src/span-data.ts index ac74e01a2..c30890c3b 100644 --- a/src/span-data.ts +++ b/src/span-data.ts @@ -86,11 +86,11 @@ export abstract class BaseSpanData implements Span { } getTraceContext() { - return traceUtil.generateTraceContext({ + return { traceId: this.trace.traceId.toString(), spanId: this.span.spanId.toString(), options: 1 // always traced - }); + }; } // tslint:disable-next-line:no-any @@ -195,7 +195,7 @@ function createPhantomSpanData(spanType: T): Span& return Object.freeze(Object.assign( { getTraceContext() { - return ''; + return null; }, // tslint:disable-next-line:no-any addLabel(key: string, value: any) {}, diff --git a/src/trace-api.ts b/src/trace-api.ts index 70de1597e..8020e3abb 100644 --- a/src/trace-api.ts +++ b/src/trace-api.ts @@ -15,14 +15,13 @@ */ import {EventEmitter} from 'events'; -import * as is from 'is'; import * as uuid from 'uuid'; import {cls, RootContext} from './cls'; -import {TracePolicy} from './config'; +import {OpenCensusPropagation, TracePolicy} from './config'; import {Constants, SpanType} from './constants'; import {Logger} from './logger'; -import {Func, RootSpan, RootSpanOptions, Span, SpanOptions, Tracer} from './plugin-types'; +import {Func, Propagation, RootSpan, RootSpanOptions, Span, SpanOptions, Tracer} from './plugin-types'; import {RootSpanData, UNCORRELATED_CHILD_SPAN, UNCORRELATED_ROOT_SPAN, UNTRACED_CHILD_SPAN, UNTRACED_ROOT_SPAN} from './span-data'; import {TraceLabels} from './trace-labels'; import {traceWriter} from './trace-writer'; @@ -41,11 +40,12 @@ export interface StackdriverTracerConfig { } /** - * Type guard that returns whether an object is a string or not. + * A collection of externally-instantiated objects used by StackdriverTracer. */ -// tslint:disable-next-line:no-any -function isString(obj: any): obj is string { - return is.string(obj); +export interface StackdriverTracerComponents { + logger: Logger; + tracePolicy: TracePolicy; + propagation: OpenCensusPropagation; } /** @@ -57,11 +57,49 @@ export class StackdriverTracer implements Tracer { readonly labels = TraceLabels; readonly spanTypes = SpanType; readonly traceContextUtils = { - encodeAsString: util.generateTraceContext, - decodeFromString: util.parseContextFromHeader, 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; @@ -69,6 +107,8 @@ export class StackdriverTracer implements Tracer { 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. @@ -86,13 +126,17 @@ export class StackdriverTracer implements Tracer { * beforehand. * @param config An object specifying how this instance should * be configured. - * @param logger A logger object. + * @param components An collection of externally-instantiated objects used + * by this instance. * @private */ - enable(config: StackdriverTracerConfig, policy: TracePolicy, logger: Logger) { - this.logger = logger; + enable( + config: StackdriverTracerConfig, + components: StackdriverTracerComponents) { this.config = config; - this.policy = policy; + this.logger = components.logger; + this.policy = components.tracePolicy; + this.headerPropagation = components.propagation; this.enabled = true; } @@ -146,20 +190,22 @@ export class StackdriverTracer implements Tracer { return fn(UNCORRELATED_ROOT_SPAN); } - // Attempt to read incoming trace context. - const parseContext = (stringifiedTraceContext?: string|null) => { - const parsedContext = isString(stringifiedTraceContext) ? - util.parseContextFromHeader(stringifiedTraceContext) : - null; - if (parsedContext) { - if (parsedContext.options === undefined) { - // If there are no incoming option flags, default to 0x1. - parsedContext.options = 1; - } - } - return parsedContext as Required| null; - }; - const traceContext = parseContext(options.traceContext); + // 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; + } + return { + traceId: traceContext.traceId, + spanId: traceContext.spanId, + options: 1 + }; + }; + const traceContext = canonicalizeTraceContext(options.traceContext); // Consult the trace policy. const shouldTrace = this.policy!.shouldTrace({ @@ -208,8 +254,7 @@ export class StackdriverTracer implements Tracer { getCurrentContextId(): string|null { // In v3, this will be deprecated for getCurrentRootSpan. const traceContext = this.getCurrentRootSpan().getTraceContext(); - const parsedTraceContext = util.parseContextFromHeader(traceContext); - return parsedTraceContext ? parsedTraceContext.traceId : null; + return traceContext ? traceContext.traceId : null; } getProjectId(): Promise { @@ -315,18 +360,16 @@ export class StackdriverTracer implements Tracer { return span.type === SpanType.ROOT || span.type === SpanType.CHILD; } - getResponseTraceContext(incomingTraceContext: string|null, isTraced: boolean): - string { + getResponseTraceContext( + incomingTraceContext: util.TraceContext|null, isTraced: boolean) { if (!this.isActive() || !incomingTraceContext) { - return ''; - } - - const traceContext = util.parseContextFromHeader(incomingTraceContext); - if (!traceContext) { - return ''; + return null; } - traceContext.options = (traceContext.options || 0) & (isTraced ? 1 : 0); - return util.generateTraceContext(traceContext); + return { + traceId: incomingTraceContext.traceId, + spanId: incomingTraceContext.spanId, + options: (incomingTraceContext.options || 0) & (isTraced ? 1 : 0) + }; } wrap(fn: Func): Func { diff --git a/src/trace-plugin-loader.ts b/src/trace-plugin-loader.ts index e8c168ffc..d9dd01b8c 100644 --- a/src/trace-plugin-loader.ts +++ b/src/trace-plugin-loader.ts @@ -22,7 +22,7 @@ import * as semver from 'semver'; import {TracePolicy} from './config'; import {Logger} from './logger'; import {Intercept, Monkeypatch, Plugin} from './plugin-types'; -import {StackdriverTracer, StackdriverTracerConfig} from './trace-api'; +import {StackdriverTracer, StackdriverTracerComponents, StackdriverTracerConfig} from './trace-api'; import {Singleton} from './util'; /** @@ -90,7 +90,7 @@ export class ModulePluginWrapper implements PluginWrapper { // A logger. private readonly logger: Logger; // A trace policy to apply to created StackdriverTracer instances. - private readonly tracePolicy: TracePolicy; + private readonly components: StackdriverTracerComponents; // Display-friendly name of the module being patched by this plugin. private readonly name: string; // The path to the plugin. @@ -111,9 +111,9 @@ export class ModulePluginWrapper implements PluginWrapper { constructor( options: ModulePluginWrapperOptions, private readonly traceConfig: StackdriverTracerConfig, - components: PluginLoaderComponents) { + components: StackdriverTracerComponents) { this.logger = components.logger; - this.tracePolicy = components.tracePolicy; + this.components = components; this.name = options.name; this.path = options.path; } @@ -204,7 +204,7 @@ export class ModulePluginWrapper implements PluginWrapper { private createTraceAgentInstance(file: string) { const traceApi = new StackdriverTracer(file); - traceApi.enable(this.traceConfig, this.tracePolicy, this.logger); + traceApi.enable(this.traceConfig, this.components); this.traceApiInstances.push(traceApi); return traceApi; } @@ -222,7 +222,7 @@ export class CorePluginWrapper implements PluginWrapper { constructor( config: CorePluginWrapperOptions, traceConfig: StackdriverTracerConfig, - components: PluginLoaderComponents) { + components: StackdriverTracerComponents) { this.logger = components.logger; this.children = config.children.map( config => new ModulePluginWrapper(config, traceConfig, components)); @@ -267,11 +267,6 @@ export enum PluginLoaderState { DEACTIVATED } -export interface PluginLoaderComponents { - logger: Logger; - tracePolicy: TracePolicy; -} - /** * A class providing functionality to hook into module loading and apply * plugins to enable tracing. @@ -295,7 +290,8 @@ export class PluginLoader { * @param config The configuration for this instance. * @param logger The logger to use. */ - constructor(config: PluginLoaderConfig, components: PluginLoaderComponents) { + constructor( + config: PluginLoaderConfig, components: StackdriverTracerComponents) { this.logger = components.logger; const nonCoreModules: string[] = []; // Initialize ALL of the PluginWrapper objects here. diff --git a/src/tracing.ts b/src/tracing.ts index 372a1eb76..0b95a4978 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import {v1 as stackdriverPropagation} from '@opencensus/propagation-stackdriver'; import * as path from 'path'; import {cls, TraceCLSConfig} from './cls'; -import {TracePolicy} from './config'; +import {OpenCensusPropagation, TracePolicy} from './config'; import {LEVELS, Logger} from './logger'; import {StackdriverTracer} from './trace-api'; import {pluginLoader, PluginLoaderConfig} from './trace-plugin-loader'; @@ -30,7 +31,8 @@ export type TopLevelConfig = { writerConfig: TraceWriterConfig; pluginLoaderConfig: PluginLoaderConfig; tracePolicyConfig: TracePolicyConfig; - overrides: {tracePolicy?: TracePolicy;}; + overrides: + {tracePolicy?: TracePolicy; propagation?: OpenCensusPropagation;}; }|{ enabled: false; }; @@ -120,12 +122,14 @@ export class Tracing implements Component { const tracePolicy = this.config.overrides.tracePolicy || new BuiltinTracePolicy(this.config.tracePolicyConfig); + const propagation = + this.config.overrides.propagation || stackdriverPropagation; + + const tracerComponents = {logger: this.logger, tracePolicy, propagation}; this.traceAgent.enable( - this.config.pluginLoaderConfig.tracerConfig, tracePolicy, this.logger); - pluginLoader - .create( - this.config.pluginLoaderConfig, {logger: this.logger, tracePolicy}) + this.config.pluginLoaderConfig.tracerConfig, tracerComponents); + pluginLoader.create(this.config.pluginLoaderConfig, tracerComponents) .activate(); if (typeof this.config.writerConfig.projectId !== 'string' && diff --git a/src/util.ts b/src/util.ts index 72a2de8ad..cf793ac3a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -19,6 +19,8 @@ import * as sourceMapSupport from 'source-map-support'; const {hexToDec, decToHex}: {[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(); diff --git a/test/plugins/common.ts b/test/plugins/common.ts index 41b8a80c9..6d1aa4948 100644 --- a/test/plugins/common.ts +++ b/test/plugins/common.ts @@ -16,6 +16,7 @@ 'use strict'; import '../override-gcp-metadata'; +import {v1 as stackdriverPropagation} from '@opencensus/propagation-stackdriver'; import { cls, TraceCLS } from '../../src/cls'; import { StackdriverTracer } from '../../src/trace-api'; import { traceWriter } from '../../src/trace-writer'; @@ -65,7 +66,11 @@ shimmer.wrap(trace, 'start', function(original) { return function() { var result = original.apply(this, arguments); testTraceAgent = new StackdriverTracer('test'); - testTraceAgent.enable(getBaseConfig(), alwaysTrace(), new TestLogger()); + testTraceAgent.enable(getBaseConfig(), { + tracePolicy: alwaysTrace(), + logger: new TestLogger(), + propagation: stackdriverPropagation + }); return result; }; }); diff --git a/test/plugins/test-trace-grpc.ts b/test/plugins/test-trace-grpc.ts index 495e62150..fb963be92 100644 --- a/test/plugins/test-trace-grpc.ts +++ b/test/plugins/test-trace-grpc.ts @@ -475,7 +475,11 @@ describeInterop('grpc', fixture => { it('should support distributed trace context', function(done) { function makeLink(fn, meta, next) { return function() { - agent.runInRootSpan({ name: '', traceContext: `${COMMON_TRACE_ID}/0;o=1` }, function(span) { + agent.runInRootSpan({ name: '', traceContext: { + traceId: COMMON_TRACE_ID, + spanId: '0', + options: 1 + }}, function(span) { assert.strictEqual(span.type, agent.spanTypes.ROOT); fn(client, grpc, meta, function() { span.endSpan(); diff --git a/test/test-config-plugins.ts b/test/test-config-plugins.ts index d183d67ad..5497d4a6a 100644 --- a/test/test-config-plugins.ts +++ b/test/test-config-plugins.ts @@ -17,8 +17,8 @@ import * as assert from 'assert'; import {defaultConfig} from '../src/config'; -import {Logger} from '../src/logger'; -import {PluginLoader, PluginLoaderComponents, PluginLoaderConfig} from '../src/trace-plugin-loader'; +import {StackdriverTracerComponents} from '../src/trace-api'; +import {PluginLoader, PluginLoaderConfig} from '../src/trace-plugin-loader'; import * as testTraceModule from './trace'; @@ -28,7 +28,7 @@ describe('Configuration: Plugins', () => { class ConfigTestPluginLoader extends PluginLoader { constructor( - config: PluginLoaderConfig, components: PluginLoaderComponents) { + config: PluginLoaderConfig, components: StackdriverTracerComponents) { super(config, components); plugins = config.plugins; } diff --git a/test/test-index.ts b/test/test-index.ts index dd6355b46..0b1aabff4 100644 --- a/test/test-index.ts +++ b/test/test-index.ts @@ -43,7 +43,10 @@ describe('index.js', function() { await disabledAgent.getProjectId().then( () => Promise.reject(new Error()), () => Promise.resolve()); assert.strictEqual(disabledAgent.createChildSpan({ name: '' }).type, SpanType.UNTRACED); - assert.strictEqual(disabledAgent.getResponseTraceContext('', false), ''); + assert.strictEqual(disabledAgent.getResponseTraceContext({ + traceId: '1', + spanId: '1' + }, false), null); const fn = () => {}; assert.strictEqual(disabledAgent.wrap(fn), fn); // TODO(kjin): Figure out how to test wrapEmitter diff --git a/test/test-plugin-loader.ts b/test/test-plugin-loader.ts index 5ca32fba0..cbeca30f2 100644 --- a/test/test-plugin-loader.ts +++ b/test/test-plugin-loader.ts @@ -17,11 +17,12 @@ import * as assert from 'assert'; import * as path from 'path'; +import {OpenCensusPropagation} from '../src/config'; import {PluginLoader, PluginLoaderState, PluginWrapper} from '../src/trace-plugin-loader'; import {alwaysTrace} from '../src/tracing-policy'; import {TestLogger} from './logger'; -import {getBaseConfig} from './utils'; +import {getBaseConfig, NoPropagation} from './utils'; export interface SimplePluginLoaderConfig { // An object which contains paths to files that should be loaded as plugins @@ -41,7 +42,7 @@ describe('Trace Plugin Loader', () => { const makePluginLoader = (config: SimplePluginLoaderConfig) => { return new PluginLoader( Object.assign({tracerConfig: getBaseConfig()}, config), - {tracePolicy: alwaysTrace(), logger}); + {tracePolicy: alwaysTrace(), logger, propagation: new NoPropagation()}); }; before(() => { diff --git a/test/test-span-data.ts b/test/test-span-data.ts index 83a122790..1031e6cc9 100644 --- a/test/test-span-data.ts +++ b/test/test-span-data.ts @@ -158,11 +158,13 @@ describe('SpanData', () => { }); }); - it('exposes a method to provide serialized trace context', () => { + it('exposes a method to provide trace context', () => { const spanData = new CommonSpanData(trace, 'name', '0', 0); - assert.deepStrictEqual( - spanData.getTraceContext(), - `${spanData.trace.traceId}/${spanData.span.spanId};o=1`); + assert.deepStrictEqual(spanData.getTraceContext(), { + traceId: spanData.trace.traceId, + spanId: spanData.span.spanId, + options: 1 + }); }); it('captures stack traces', () => { diff --git a/test/test-trace-api.ts b/test/test-trace-api.ts index 5a20ddeab..796d62f4e 100644 --- a/test/test-trace-api.ts +++ b/test/test-trace-api.ts @@ -17,26 +17,32 @@ import * as assert from 'assert'; import {cls, TraceCLS, TraceCLSMechanism} from '../src/cls'; -import {defaultConfig, RequestDetails, TracePolicy} from '../src/config'; +import {defaultConfig, GetHeaderFunction as HeaderGetter, OpenCensusPropagation, RequestDetails, SetHeaderFunction as HeaderSetter, TracePolicy} from '../src/config'; import {SpanType} from '../src/constants'; -import {StackdriverTracer, StackdriverTracerConfig} from '../src/trace-api'; +import {StackdriverTracer, StackdriverTracerComponents, StackdriverTracerConfig} from '../src/trace-api'; import {traceWriter} from '../src/trace-writer'; -import {alwaysTrace, TraceContextHeaderBehavior} from '../src/tracing-policy'; -import {FORCE_NEW} from '../src/util'; +import {alwaysTrace} from '../src/tracing-policy'; +import {FORCE_NEW, TraceContext} from '../src/util'; import {TestLogger} from './logger'; import * as testTraceModule from './trace'; -import {getBaseConfig} from './utils'; +import {getBaseConfig, NoPropagation} from './utils'; describe('Trace Interface', () => { const logger = new TestLogger(); function createTraceAgent( config?: Partial, - policy?: TracePolicy): StackdriverTracer { + components?: Partial): StackdriverTracer { const result = new StackdriverTracer('test'); result.enable( - Object.assign(getBaseConfig(), config), policy || alwaysTrace(), - logger); + Object.assign(getBaseConfig(), config), + Object.assign( + { + tracePolicy: alwaysTrace(), + logger, + propagation: new NoPropagation() + }, + components)); return result; } @@ -202,11 +208,15 @@ describe('Trace Interface', () => { } } const tracePolicy = new CaptureOptionsTracePolicy(); - const traceAPI = createTraceAgent({}, tracePolicy); + const traceAPI = createTraceAgent({}, {tracePolicy}); // All params present { - const rootSpanOptions = - {name: 'root', url: 'foo', method: 'bar', traceContext: '1/2;o=1'}; + const rootSpanOptions = { + name: 'root', + url: 'foo', + method: 'bar', + traceContext: {traceId: '1', spanId: '2', options: 1} + }; const beforeRootSpan = Date.now(); traceAPI.runInRootSpan(rootSpanOptions, (rootSpan) => { assert.strictEqual(rootSpan.type, SpanType.UNTRACED); @@ -221,14 +231,13 @@ describe('Trace Interface', () => { assert.ok(shouldTraceParam.timestamp <= afterRootSpan); assert.ok(shouldTraceParam.timestamp <= afterRootSpan); assert.deepStrictEqual( - shouldTraceParam.traceContext, - {traceId: '1', spanId: '2', options: 1}); + shouldTraceParam.traceContext, rootSpanOptions.traceContext); assert.strictEqual(shouldTraceParam.options, rootSpanOptions); } tracePolicy.capturedShouldTraceParam = null; // Limited params present { - const rootSpanOptions = {name: 'root', traceContext: 'unparseable'}; + const rootSpanOptions = {name: 'root'}; traceAPI.runInRootSpan(rootSpanOptions, (rootSpan) => { assert.strictEqual(rootSpan.type, SpanType.UNTRACED); rootSpan.endSpan(); @@ -242,6 +251,28 @@ describe('Trace Interface', () => { } }); + it('should expose methods for trace context header propagation', () => { + class TestPropagation implements OpenCensusPropagation { + extract({getHeader}: HeaderGetter) { + return {traceId: getHeader('a') as string, spanId: '0', options: 1}; + } + inject({setHeader}: HeaderSetter, traceContext: TraceContext) { + setHeader(traceContext.traceId, 'y'); + } + } + const propagation = new TestPropagation(); + const tracer = createTraceAgent({}, {propagation}); + const result = tracer.propagation.extract(s => `${s}${s}`); + assert.deepStrictEqual(result, {traceId: 'aa', spanId: '0', options: 1}); + let setHeaderCalled = false; + tracer.propagation.inject((key: string, value: string) => { + assert.strictEqual(key, 'x'); + assert.strictEqual(value, 'y'); + setHeaderCalled = true; + }, {traceId: 'x', spanId: '0', options: 1}); + assert.ok(setHeaderCalled); + }); + it('should respect enhancedDatabaseReporting options field', () => { [true, false].forEach((enhancedDatabaseReporting) => { const traceAPI = createTraceAgent({ @@ -257,7 +288,11 @@ describe('Trace Interface', () => { // Propagate from trace context header { createTraceAgent().runInRootSpan( - {name: 'root1', traceContext: '123456/667;o=1'}, (rootSpan) => { + { + name: 'root1', + traceContext: {traceId: '123456', spanId: '667', options: 1} + }, + (rootSpan) => { rootSpan.endSpan(); }); const foundTrace = @@ -268,7 +303,7 @@ describe('Trace Interface', () => { } // Generate a trace context {createTraceAgent().runInRootSpan( - {name: 'root2', traceContext: 'unparseable'}, + {name: 'root2'}, (rootSpan) => { rootSpan.endSpan(); }); @@ -283,7 +318,8 @@ describe('Trace Interface', () => { it('should trace if no option flags are provided', () => { createTraceAgent({enhancedDatabaseReporting: false}) .runInRootSpan( - {name: 'root', traceContext: '123456/667'}, (rootSpan) => { + {name: 'root', traceContext: {traceId: '123456', spanId: '667'}}, + (rootSpan) => { rootSpan.endSpan(); }); const foundTrace = @@ -295,25 +331,25 @@ describe('Trace Interface', () => { it('should behave as expected', () => { const fakeTraceId = 'ffeeddccbbaa99887766554433221100'; const traceApi = createTraceAgent(); - const tracedContext = fakeTraceId + '/0;o=1'; - const untracedContext = fakeTraceId + '/0;o=0'; - const unspecifiedContext = fakeTraceId + '/0'; - assert.strictEqual( + const tracedContext = {traceId: fakeTraceId, spanId: '0', options: 1}; + const untracedContext = {traceId: fakeTraceId, spanId: '0', options: 0}; + const unspecifiedContext = {traceId: fakeTraceId, spanId: '0'}; + assert.deepStrictEqual( traceApi.getResponseTraceContext(tracedContext, true), tracedContext); - assert.strictEqual( + assert.deepStrictEqual( traceApi.getResponseTraceContext(tracedContext, false), untracedContext); - assert.strictEqual( + assert.deepStrictEqual( traceApi.getResponseTraceContext(untracedContext, true), untracedContext); - assert.strictEqual( + assert.deepStrictEqual( traceApi.getResponseTraceContext(untracedContext, false), untracedContext); - assert.strictEqual( + assert.deepStrictEqual( traceApi.getResponseTraceContext(unspecifiedContext, true), untracedContext); - assert.strictEqual( + assert.deepStrictEqual( traceApi.getResponseTraceContext(unspecifiedContext, false), untracedContext); }); diff --git a/test/utils.ts b/test/utils.ts index 6cbdc9bf5..6083c9a6c 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -21,12 +21,12 @@ import * as fs from 'fs'; import * as semver from 'semver'; import {cls} from '../src/cls'; +import {OpenCensusPropagation} from '../src/config'; import {SpanType} from '../src/constants'; import {Span} from '../src/plugin-types'; import {ChildSpanData, RootSpanData} from '../src/span-data'; import {TraceSpan} from '../src/trace'; import {StackdriverTracerConfig} from '../src/trace-api'; -import {alwaysTrace, TraceContextHeaderBehavior} from '../src/tracing-policy'; /** * Constants @@ -40,6 +40,16 @@ export const ASSERT_SPAN_TIME_TOLERANCE_MS = 5; export const SERVER_KEY = fs.readFileSync(`${__dirname}/fixtures/key.pem`); export const SERVER_CERT = fs.readFileSync(`${__dirname}/fixtures/cert.pem`); +/** + * Misc. Implementations + */ +export class NoPropagation implements OpenCensusPropagation { + extract() { + return null; + } + inject() {} +} + /** * Helper Functions */