Skip to content

Commit 4b86dd6

Browse files
committed
Populated context with TraceContext.
1 parent 1a0d68b commit 4b86dd6

File tree

5 files changed

+191
-102
lines changed

5 files changed

+191
-102
lines changed

src/Context.ts

+100-98
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,121 @@
11
import { FunctionInfo } from './FunctionInfo';
2-
import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions } from './converters';
2+
import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions, fromRpcTraceContext } from './converters';
33
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
44
import { Request, RequestProperties } from './http/Request';
55
import { Response } from './http/Response';
66
import LogLevel = rpc.RpcLog.Level;
77
import LogCategory = rpc.RpcLog.RpcLogCategory;
8-
import { Context, ExecutionContext, Logger, BindingDefinition, HttpRequest } from './public/Interfaces'
8+
import { Context, ExecutionContext, Logger, BindingDefinition, HttpRequest, TraceContext } from './public/Interfaces'
99

1010
export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
11-
let context = new InvocationContext(info, request, logCallback, callback);
12-
13-
let bindings: Dict<any> = {};
14-
let inputs: InputTypes[] = [];
15-
let httpInput: RequestProperties | undefined;
16-
for (let binding of <rpc.IParameterBinding[]>request.inputData) {
17-
if (binding.data && binding.name) {
18-
let input: InputTypes;
19-
if (binding.data && binding.data.http) {
20-
input = httpInput = fromRpcHttp(binding.data.http);
21-
} else {
22-
input = fromTypedData(binding.data);
23-
}
24-
bindings[binding.name] = input;
25-
inputs.push(input);
11+
let context = new InvocationContext(info, request, logCallback, callback);
12+
13+
let bindings: Dict<any> = {};
14+
let inputs: InputTypes[] = [];
15+
let httpInput: RequestProperties | undefined;
16+
for (let binding of <rpc.IParameterBinding[]>request.inputData) {
17+
if (binding.data && binding.name) {
18+
let input: InputTypes;
19+
if (binding.data && binding.data.http) {
20+
input = httpInput = fromRpcHttp(binding.data.http);
21+
} else {
22+
input = fromTypedData(binding.data);
23+
}
24+
bindings[binding.name] = input;
25+
inputs.push(input);
26+
}
27+
}
28+
29+
context.bindings = bindings;
30+
if (httpInput) {
31+
context.req = new Request(httpInput);
32+
context.res = new Response(context.done);
33+
}
34+
return {
35+
context: <Context>context,
36+
inputs: inputs
2637
}
27-
}
28-
29-
context.bindings = bindings;
30-
if (httpInput) {
31-
context.req = new Request(httpInput);
32-
context.res = new Response(context.done);
33-
}
34-
return {
35-
context: <Context>context,
36-
inputs: inputs
37-
}
3838
}
3939

4040
class InvocationContext implements Context {
41-
invocationId: string;
42-
executionContext: ExecutionContext;
43-
bindings: Dict<any>;
44-
bindingData: Dict<any>;
45-
bindingDefinitions: BindingDefinition[];
46-
log: Logger;
47-
req?: Request;
48-
res?: Response;
49-
done: DoneCallback;
50-
51-
constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
52-
this.invocationId = <string>request.invocationId;
53-
const executionContext = {
54-
invocationId: this.invocationId,
55-
functionName: <string>info.name,
56-
functionDirectory: <string>info.directory
57-
};
58-
this.executionContext = executionContext;
59-
this.bindings = {};
60-
let _done = false;
61-
let _promise = false;
62-
63-
// Log message that is tied to function invocation
64-
this.log = Object.assign(
65-
<ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
66-
{
67-
error: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args),
68-
warn: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args),
69-
info: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
70-
verbose: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Trace, executionContext, ...args)
71-
}
72-
);
73-
74-
this.bindingData = getNormalizedBindingData(request);
75-
this.bindingDefinitions = getBindingDefinitions(info);
76-
77-
// isPromise is a hidden parameter that we set to true in the event of a returned promise
78-
this.done = (err?: any, result?: any, isPromise?: boolean) => {
79-
_promise = isPromise === true;
80-
if (_done) {
81-
if (_promise) {
82-
logCallback(LogLevel.Error, LogCategory.User, "Error: Choose either to return a promise or call 'done'. Do not use both in your script.");
83-
} else {
84-
logCallback(LogLevel.Error, LogCategory.User, "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'.");
85-
}
86-
return;
87-
}
88-
_done = true;
89-
90-
// Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object
91-
if (info.httpOutputName && this.res && this.bindings[info.httpOutputName] === undefined) {
92-
this.bindings[info.httpOutputName] = this.res;
93-
}
94-
95-
callback(err, {
96-
return: result,
97-
bindings: this.bindings
98-
});
99-
};
100-
}
41+
invocationId: string;
42+
executionContext: ExecutionContext;
43+
bindings: Dict<any>;
44+
bindingData: Dict<any>;
45+
traceContext: TraceContext;
46+
bindingDefinitions: BindingDefinition[];
47+
log: Logger;
48+
req?: Request;
49+
res?: Response;
50+
done: DoneCallback;
51+
52+
constructor(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
53+
this.invocationId = <string>request.invocationId;
54+
this.traceContext = fromRpcTraceContext(request.traceContext);
55+
const executionContext = {
56+
invocationId: this.invocationId,
57+
functionName: <string>info.name,
58+
functionDirectory: <string>info.directory
59+
};
60+
this.executionContext = executionContext;
61+
this.bindings = {};
62+
let _done = false;
63+
let _promise = false;
64+
65+
// Log message that is tied to function invocation
66+
this.log = Object.assign(
67+
<ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
68+
{
69+
error: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args),
70+
warn: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args),
71+
info: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args),
72+
verbose: <ILog>(...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Trace, executionContext, ...args)
73+
}
74+
);
75+
76+
this.bindingData = getNormalizedBindingData(request);
77+
this.bindingDefinitions = getBindingDefinitions(info);
78+
79+
// isPromise is a hidden parameter that we set to true in the event of a returned promise
80+
this.done = (err?: any, result?: any, isPromise?: boolean) => {
81+
_promise = isPromise === true;
82+
if (_done) {
83+
if (_promise) {
84+
logCallback(LogLevel.Error, LogCategory.User, "Error: Choose either to return a promise or call 'done'. Do not use both in your script.");
85+
} else {
86+
logCallback(LogLevel.Error, LogCategory.User, "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'.");
87+
}
88+
return;
89+
}
90+
_done = true;
91+
92+
// Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object
93+
if (info.httpOutputName && this.res && this.bindings[info.httpOutputName] === undefined) {
94+
this.bindings[info.httpOutputName] = this.res;
95+
}
96+
97+
callback(err, {
98+
return: result,
99+
bindings: this.bindings
100+
});
101+
};
102+
}
101103
}
102104

103105
// Emit warning if trying to log after function execution is done.
104106
function logWithAsyncCheck(done: boolean, log: LogCallback, level: LogLevel, executionContext: ExecutionContext, ...args: any[]) {
105-
if (done) {
106-
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. ";
107-
badAsyncMsg += `Function name: ${executionContext.functionName}. Invocation Id: ${executionContext.invocationId}. `;
108-
badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `;
109-
log(LogLevel.Warning, LogCategory.System, badAsyncMsg);
110-
}
111-
return log(level, LogCategory.User, ...args);
107+
if (done) {
108+
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. ";
109+
badAsyncMsg += `Function name: ${executionContext.functionName}. Invocation Id: ${executionContext.invocationId}. `;
110+
badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `;
111+
log(LogLevel.Warning, LogCategory.System, badAsyncMsg);
112+
}
113+
return log(level, LogCategory.User, ...args);
112114
}
113115

114116
export interface InvocationResult {
115-
return: any;
116-
bindings: Dict<any>;
117+
return: any;
118+
bindings: Dict<any>;
117119
}
118120

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

125127
export interface Dict<T> {
126-
[key: string]: T
128+
[key: string]: T
127129
}
128130

129131
// Allowed input types

src/converters/RpcConverters.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
INullableTimestamp
77
} from '../../azure-functions-language-worker-protobuf/src/rpc';
88
import { InternalException } from "../utils/InternalException";
9+
import { TraceContext } from '../public/Interfaces';
910

1011
/**
1112
* Converts 'ITypedData' input from the RPC layer to JavaScript types.
@@ -30,6 +31,24 @@ export function fromTypedData(typedData?: rpc.ITypedData, convertStringToJson: b
3031
}
3132
}
3233

34+
/**
35+
* Converts 'IRpcTraceContext' input from RPC layer to dictionary of key value pairs.
36+
* @param traceContext IRpcTraceContext object containing the activityId, tracestate and attributes.
37+
*/
38+
export function fromRpcTraceContext(traceContext: rpc.IRpcTraceContext | null | undefined): TraceContext
39+
{
40+
if (traceContext)
41+
{
42+
return <TraceContext>{
43+
traceparent: traceContext.traceParent,
44+
tracestate: traceContext.traceState,
45+
attributes: traceContext.attributes
46+
};
47+
}
48+
49+
return <TraceContext>{};
50+
}
51+
3352
/**
3453
* Converts JavaScript type data to 'ITypedData' to be sent through the RPC layer
3554
* TypedData can be string, json, or bytes
@@ -162,4 +181,4 @@ export function toNullableTimestamp(dateTime: Date | number | undefined, propert
162181
}
163182
}
164183
return undefined;
165-
}
184+
}

src/public/Interfaces.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export interface Context {
3434
* Trigger metadata and function invocation data.
3535
*/
3636
bindingData: { [key: string]: any };
37+
/**
38+
* TraceContext information to enable distributed tracing scenarios.
39+
*/
40+
traceContext: TraceContext;
3741
/**
3842
* Bindings your function uses, as defined in function.json.
3943
*/
@@ -155,6 +159,22 @@ export interface ExecutionContext {
155159
functionDirectory: string;
156160
}
157161

162+
/**
163+
* TraceContext information to enable distributed tracing scenarios.
164+
*/
165+
export interface TraceContext {
166+
/** Describes the position of the incoming request in its trace graph in a portable, fixed-length format. */
167+
traceparent: string | null | undefined;
168+
169+
/** Extends traceparent with vendor-specific data. */
170+
tracestate: string | null | undefined;
171+
172+
/** Holds additional properties being sent as part of request telemetry. */
173+
attributes: {
174+
[k: string]: string
175+
} | null | undefined;
176+
}
177+
158178
export interface BindingDefinition {
159179
/**
160180
* The name of your binding, as defined in function.json.
@@ -163,7 +183,7 @@ export interface BindingDefinition {
163183
/**
164184
* The type of your binding, as defined in function.json.
165185
*/
166-
type: string,
186+
type: string,
167187
/**
168188
* The direction of your binding, as defined in function.json.
169189
*/

test/RpcConvertersTests.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { toNullableBool, toNullableString, toNullableDouble, toNullableTimestamp } from '../src/converters';
1+
import { toNullableBool, toNullableString, toNullableDouble, toNullableTimestamp, fromRpcTraceContext } from '../src/converters';
22
import { expect } from 'chai';
3-
import * as sinon from 'sinon';
43
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
54
import 'mocha';
65

@@ -22,6 +21,38 @@ describe('Rpc Converters', () => {
2221
}).to.throw("A 'boolean' type was expected instead of a 'string' type. Cannot parse value of 'test'.")
2322
});
2423

24+
it('Converts IRpcTraceContext to tracecontext', () => {
25+
let traceparentvalue = "tracep";
26+
let tracestatevalue = "traces";
27+
let attributesvalue = {"traceparent": "traceparent", "tracestate": "tracestate"};
28+
29+
let input = <rpc.IRpcTraceContext>({
30+
traceParent: traceparentvalue,
31+
traceState: tracestatevalue,
32+
attributes: attributesvalue
33+
});
34+
35+
let traceContext = fromRpcTraceContext(input);
36+
37+
expect(traceparentvalue).to.equal(traceContext.traceparent);
38+
expect(tracestatevalue).to.equal(traceContext.tracestate);
39+
expect(attributesvalue).to.equal(traceContext.attributes);
40+
});
41+
42+
it('Converts null traceContext to empty values', () => {
43+
let traceContext = fromRpcTraceContext(null);
44+
expect(traceContext.traceparent).to.be.undefined;
45+
expect(traceContext.tracestate).to.be.undefined;
46+
expect(traceContext.attributes).to.be.undefined;
47+
});
48+
49+
it('Converts undefined traceContext to empty values', () => {
50+
let traceContext = fromRpcTraceContext(undefined);
51+
expect(traceContext.traceparent).to.be.undefined;
52+
expect(traceContext.tracestate).to.be.undefined;
53+
expect(traceContext.attributes).to.be.undefined;
54+
});
55+
2556
it('does not converts null to NullableBool', () => {
2657
let nullable = toNullableBool(<any>null, "test");
2758
expect(nullable && nullable.value).to.be.undefined;

types/public/Interfaces.d.ts

+17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface Context {
3737
bindingData: {
3838
[key: string]: any;
3939
};
40+
/**
41+
* TraceContext information to enable distributed tracing scenarios.
42+
*/
43+
traceContext: TraceContext;
4044
/**
4145
* Bindings your function uses, as defined in function.json.
4246
*/
@@ -152,6 +156,19 @@ export interface ExecutionContext {
152156
*/
153157
functionDirectory: string;
154158
}
159+
/**
160+
* TraceContext information to enable distributed tracing scenarios.
161+
*/
162+
export interface TraceContext {
163+
/** Describes the position of the incoming request in its trace graph in a portable, fixed-length format. */
164+
traceparent: string | null | undefined;
165+
/** Extends traceparent with vendor-specific data. */
166+
tracestate: string | null | undefined;
167+
/** Holds additional properties being sent as part of request telemetry. */
168+
attributes: {
169+
[k: string]: string;
170+
} | null | undefined;
171+
}
155172
export interface BindingDefinition {
156173
/**
157174
* The name of your binding, as defined in function.json.

0 commit comments

Comments
 (0)