From b8588ed4ea74fc9ef75586ef793b55771ce314f3 Mon Sep 17 00:00:00 2001 From: Yogesh Date: Wed, 11 Sep 2019 18:59:05 -0700 Subject: [PATCH] Populated context with TraceContext. --- src/Context.ts | 198 ++++++++++++++++---------------- src/converters/RpcConverters.ts | 21 +++- src/public/Interfaces.ts | 22 +++- test/RpcConvertersTests.ts | 35 +++++- types/public/Interfaces.d.ts | 17 +++ 5 files changed, 191 insertions(+), 102 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index 3448a036..3ab0cb2e 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -1,119 +1,121 @@ import { FunctionInfo } from './FunctionInfo'; -import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions } from './converters'; +import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions, fromRpcTraceContext } from './converters'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import { Request, RequestProperties } from './http/Request'; import { Response } from './http/Response'; import LogLevel = rpc.RpcLog.Level; import LogCategory = rpc.RpcLog.RpcLogCategory; -import { Context, ExecutionContext, Logger, BindingDefinition, HttpRequest } from './public/Interfaces' +import { Context, ExecutionContext, Logger, BindingDefinition, HttpRequest, TraceContext } from './public/Interfaces' export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) { - let context = new InvocationContext(info, request, logCallback, callback); - - let bindings: Dict = {}; - let inputs: InputTypes[] = []; - let httpInput: RequestProperties | undefined; - for (let binding of request.inputData) { - if (binding.data && binding.name) { - let input: InputTypes; - if (binding.data && binding.data.http) { - input = httpInput = fromRpcHttp(binding.data.http); - } else { - input = fromTypedData(binding.data); - } - bindings[binding.name] = input; - inputs.push(input); + let context = new InvocationContext(info, request, logCallback, callback); + + let bindings: Dict = {}; + let inputs: InputTypes[] = []; + let httpInput: RequestProperties | undefined; + for (let binding of request.inputData) { + if (binding.data && binding.name) { + let input: InputTypes; + if (binding.data && binding.data.http) { + input = httpInput = fromRpcHttp(binding.data.http); + } else { + input = fromTypedData(binding.data); + } + bindings[binding.name] = input; + inputs.push(input); + } + } + + context.bindings = bindings; + if (httpInput) { + context.req = new Request(httpInput); + context.res = new Response(context.done); + } + return { + context: context, + inputs: inputs } - } - - context.bindings = bindings; - if (httpInput) { - context.req = new Request(httpInput); - context.res = new Response(context.done); - } - return { - context: context, - inputs: inputs - } } class InvocationContext implements Context { - invocationId: string; - executionContext: ExecutionContext; - bindings: Dict; - bindingData: Dict; - bindingDefinitions: BindingDefinition[]; - log: Logger; - req?: Request; - res?: Response; - done: DoneCallback; - - constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) { - this.invocationId = request.invocationId; - const executionContext = { - invocationId: this.invocationId, - functionName: info.name, - functionDirectory: info.directory - }; - this.executionContext = executionContext; - this.bindings = {}; - let _done = false; - let _promise = false; - - // Log message that is tied to function invocation - this.log = Object.assign( - (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), - { - error: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args), - warn: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args), - info: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), - verbose: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Trace, executionContext, ...args) - } - ); - - this.bindingData = getNormalizedBindingData(request); - this.bindingDefinitions = getBindingDefinitions(info); - - // isPromise is a hidden parameter that we set to true in the event of a returned promise - this.done = (err?: any, result?: any, isPromise?: boolean) => { - _promise = isPromise === true; - if (_done) { - if (_promise) { - logCallback(LogLevel.Error, LogCategory.User, "Error: Choose either to return a promise or call 'done'. Do not use both in your script."); - } else { - logCallback(LogLevel.Error, LogCategory.User, "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'."); - } - return; - } - _done = true; - - // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object - if (info.httpOutputName && this.res && this.bindings[info.httpOutputName] === undefined) { - this.bindings[info.httpOutputName] = this.res; - } - - callback(err, { - return: result, - bindings: this.bindings - }); - }; - } + invocationId: string; + executionContext: ExecutionContext; + bindings: Dict; + bindingData: Dict; + traceContext: TraceContext; + bindingDefinitions: BindingDefinition[]; + log: Logger; + req?: Request; + res?: Response; + done: DoneCallback; + + constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) { + this.invocationId = request.invocationId; + this.traceContext = fromRpcTraceContext(request.traceContext); + const executionContext = { + invocationId: this.invocationId, + functionName: info.name, + functionDirectory: info.directory + }; + this.executionContext = executionContext; + this.bindings = {}; + let _done = false; + let _promise = false; + + // Log message that is tied to function invocation + this.log = Object.assign( + (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), + { + error: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args), + warn: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args), + info: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), + verbose: (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Trace, executionContext, ...args) + } + ); + + this.bindingData = getNormalizedBindingData(request); + this.bindingDefinitions = getBindingDefinitions(info); + + // isPromise is a hidden parameter that we set to true in the event of a returned promise + this.done = (err?: any, result?: any, isPromise?: boolean) => { + _promise = isPromise === true; + if (_done) { + if (_promise) { + logCallback(LogLevel.Error, LogCategory.User, "Error: Choose either to return a promise or call 'done'. Do not use both in your script."); + } else { + logCallback(LogLevel.Error, LogCategory.User, "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'."); + } + return; + } + _done = true; + + // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object + if (info.httpOutputName && this.res && this.bindings[info.httpOutputName] === undefined) { + this.bindings[info.httpOutputName] = this.res; + } + + callback(err, { + return: result, + bindings: this.bindings + }); + }; + } } // Emit warning if trying to log after function execution is done. function logWithAsyncCheck(done: boolean, log: LogCallback, level: LogLevel, executionContext: ExecutionContext, ...args: any[]) { - if (done) { - let badAsyncMsg = "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. "; - badAsyncMsg += `Function name: ${executionContext.functionName}. Invocation Id: ${executionContext.invocationId}. `; - badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `; - log(LogLevel.Warning, LogCategory.System, badAsyncMsg); - } - return log(level, LogCategory.User, ...args); + if (done) { + let badAsyncMsg = "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. "; + badAsyncMsg += `Function name: ${executionContext.functionName}. Invocation Id: ${executionContext.invocationId}. `; + badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `; + log(LogLevel.Warning, LogCategory.System, badAsyncMsg); + } + return log(level, LogCategory.User, ...args); } export interface InvocationResult { - return: any; - bindings: Dict; + return: any; + bindings: Dict; } export type DoneCallback = (err?: Error | string, result?: any) => void; @@ -123,7 +125,7 @@ export type LogCallback = (level: LogLevel, category: rpc.RpcLog.RpcLogCategory, export type ResultCallback = (err?: any, result?: InvocationResult) => void; export interface Dict { - [key: string]: T + [key: string]: T } // Allowed input types diff --git a/src/converters/RpcConverters.ts b/src/converters/RpcConverters.ts index 09c1db30..64c1fa8d 100644 --- a/src/converters/RpcConverters.ts +++ b/src/converters/RpcConverters.ts @@ -6,6 +6,7 @@ import { INullableTimestamp } from '../../azure-functions-language-worker-protobuf/src/rpc'; import { InternalException } from "../utils/InternalException"; +import { TraceContext } from '../public/Interfaces'; /** * Converts 'ITypedData' input from the RPC layer to JavaScript types. @@ -30,6 +31,24 @@ export function fromTypedData(typedData?: rpc.ITypedData, convertStringToJson: b } } +/** + * Converts 'IRpcTraceContext' input from RPC layer to dictionary of key value pairs. + * @param traceContext IRpcTraceContext object containing the activityId, tracestate and attributes. + */ +export function fromRpcTraceContext(traceContext: rpc.IRpcTraceContext | null | undefined): TraceContext +{ + if (traceContext) + { + return { + traceparent: traceContext.traceParent, + tracestate: traceContext.traceState, + attributes: traceContext.attributes + }; + } + + return {}; +} + /** * Converts JavaScript type data to 'ITypedData' to be sent through the RPC layer * TypedData can be string, json, or bytes @@ -162,4 +181,4 @@ export function toNullableTimestamp(dateTime: Date | number | undefined, propert } } return undefined; -} +} \ No newline at end of file diff --git a/src/public/Interfaces.ts b/src/public/Interfaces.ts index eeed4b09..d1f0d42f 100644 --- a/src/public/Interfaces.ts +++ b/src/public/Interfaces.ts @@ -34,6 +34,10 @@ export interface Context { * Trigger metadata and function invocation data. */ bindingData: { [key: string]: any }; + /** + * TraceContext information to enable distributed tracing scenarios. + */ + traceContext: TraceContext; /** * Bindings your function uses, as defined in function.json. */ @@ -155,6 +159,22 @@ export interface ExecutionContext { functionDirectory: string; } +/** + * TraceContext information to enable distributed tracing scenarios. + */ +export interface TraceContext { + /** Describes the position of the incoming request in its trace graph in a portable, fixed-length format. */ + traceparent: string | null | undefined; + + /** Extends traceparent with vendor-specific data. */ + tracestate: string | null | undefined; + + /** Holds additional properties being sent as part of request telemetry. */ + attributes: { + [k: string]: string + } | null | undefined; +} + export interface BindingDefinition { /** * The name of your binding, as defined in function.json. @@ -163,7 +183,7 @@ export interface BindingDefinition { /** * The type of your binding, as defined in function.json. */ - type: string, + type: string, /** * The direction of your binding, as defined in function.json. */ diff --git a/test/RpcConvertersTests.ts b/test/RpcConvertersTests.ts index e6d9c885..4a62003f 100644 --- a/test/RpcConvertersTests.ts +++ b/test/RpcConvertersTests.ts @@ -1,6 +1,5 @@ -import { toNullableBool, toNullableString, toNullableDouble, toNullableTimestamp } from '../src/converters'; +import { toNullableBool, toNullableString, toNullableDouble, toNullableTimestamp, fromRpcTraceContext } from '../src/converters'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import 'mocha'; @@ -22,6 +21,38 @@ describe('Rpc Converters', () => { }).to.throw("A 'boolean' type was expected instead of a 'string' type. Cannot parse value of 'test'.") }); + it('Converts IRpcTraceContext to tracecontext', () => { + let traceparentvalue = "tracep"; + let tracestatevalue = "traces"; + let attributesvalue = {"traceparent": "traceparent", "tracestate": "tracestate"}; + + let input = ({ + traceParent: traceparentvalue, + traceState: tracestatevalue, + attributes: attributesvalue + }); + + let traceContext = fromRpcTraceContext(input); + + expect(traceparentvalue).to.equal(traceContext.traceparent); + expect(tracestatevalue).to.equal(traceContext.tracestate); + expect(attributesvalue).to.equal(traceContext.attributes); + }); + + it('Converts null traceContext to empty values', () => { + let traceContext = fromRpcTraceContext(null); + expect(traceContext.traceparent).to.be.undefined; + expect(traceContext.tracestate).to.be.undefined; + expect(traceContext.attributes).to.be.undefined; + }); + + it('Converts undefined traceContext to empty values', () => { + let traceContext = fromRpcTraceContext(undefined); + expect(traceContext.traceparent).to.be.undefined; + expect(traceContext.tracestate).to.be.undefined; + expect(traceContext.attributes).to.be.undefined; + }); + it('does not converts null to NullableBool', () => { let nullable = toNullableBool(null, "test"); expect(nullable && nullable.value).to.be.undefined; diff --git a/types/public/Interfaces.d.ts b/types/public/Interfaces.d.ts index 0e529c2d..351ffb18 100644 --- a/types/public/Interfaces.d.ts +++ b/types/public/Interfaces.d.ts @@ -37,6 +37,10 @@ export interface Context { bindingData: { [key: string]: any; }; + /** + * TraceContext information to enable distributed tracing scenarios. + */ + traceContext: TraceContext; /** * Bindings your function uses, as defined in function.json. */ @@ -152,6 +156,19 @@ export interface ExecutionContext { */ functionDirectory: string; } +/** + * TraceContext information to enable distributed tracing scenarios. + */ +export interface TraceContext { + /** Describes the position of the incoming request in its trace graph in a portable, fixed-length format. */ + traceparent: string | null | undefined; + /** Extends traceparent with vendor-specific data. */ + tracestate: string | null | undefined; + /** Holds additional properties being sent as part of request telemetry. */ + attributes: { + [k: string]: string; + } | null | undefined; +} export interface BindingDefinition { /** * The name of your binding, as defined in function.json.