Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 100 additions & 98 deletions src/Context.ts
Original file line number Diff line number Diff line change
@@ -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<any> = {};
let inputs: InputTypes[] = [];
let httpInput: RequestProperties | undefined;
for (let binding of <rpc.IParameterBinding[]>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<any> = {};
let inputs: InputTypes[] = [];
let httpInput: RequestProperties | undefined;
for (let binding of <rpc.IParameterBinding[]>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>context,
inputs: inputs
}
}

context.bindings = bindings;
if (httpInput) {
context.req = new Request(httpInput);
context.res = new Response(context.done);
}
return {
context: <Context>context,
inputs: inputs
}
}

class InvocationContext implements Context {
invocationId: string;
executionContext: ExecutionContext;
bindings: Dict<any>;
bindingData: Dict<any>;
bindingDefinitions: BindingDefinition[];
log: Logger;
req?: Request;
res?: Response;
done: DoneCallback;

constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
this.invocationId = <string>request.invocationId;
const executionContext = {
invocationId: this.invocationId,
functionName: <string>info.name,
functionDirectory: <string>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(
<ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
{
error: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args),
warn: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args),
info: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
verbose: <ILog>(...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<any>;
bindingData: Dict<any>;
traceContext: TraceContext;
bindingDefinitions: BindingDefinition[];
log: Logger;
req?: Request;
res?: Response;
done: DoneCallback;

constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
this.invocationId = <string>request.invocationId;
this.traceContext = fromRpcTraceContext(request.traceContext);
const executionContext = {
invocationId: this.invocationId,
functionName: <string>info.name,
functionDirectory: <string>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(
<ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
{
error: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args),
warn: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args),
info: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
verbose: <ILog>(...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<any>;
return: any;
bindings: Dict<any>;
}

export type DoneCallback = (err?: Error | string, result?: any) => void;
Expand All @@ -123,7 +125,7 @@ export type LogCallback = (level: LogLevel, category: rpc.RpcLog.RpcLogCategory,
export type ResultCallback = (err?: any, result?: InvocationResult) => void;

export interface Dict<T> {
[key: string]: T
[key: string]: T
}

// Allowed input types
Expand Down
21 changes: 20 additions & 1 deletion src/converters/RpcConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <TraceContext>{
traceparent: traceContext.traceParent,
tracestate: traceContext.traceState,
attributes: traceContext.attributes
};
}

return <TraceContext>{};
}

/**
* Converts JavaScript type data to 'ITypedData' to be sent through the RPC layer
* TypedData can be string, json, or bytes
Expand Down Expand Up @@ -162,4 +181,4 @@ export function toNullableTimestamp(dateTime: Date | number | undefined, propert
}
}
return undefined;
}
}
22 changes: 21 additions & 1 deletion src/public/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*/
Expand Down
35 changes: 33 additions & 2 deletions test/RpcConvertersTests.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 = <rpc.IRpcTraceContext>({
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(<any>null, "test");
expect(nullable && nullable.value).to.be.undefined;
Expand Down
17 changes: 17 additions & 0 deletions types/public/Interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down