Skip to content

Commit

Permalink
Add support for Functions programming model v4 (#1181)
Browse files Browse the repository at this point in the history
Co-authored-by: Hector Hernandez <[email protected]>
  • Loading branch information
ejizba and hectorhdzg authored Sep 6, 2023
1 parent feb5241 commit 4034bb0
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 214 deletions.
178 changes: 118 additions & 60 deletions AutoCollection/AzureFunctionsHook.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Disposable, PostInvocationContext, PreInvocationContext } from "@azure/functions-core";
import { Context, HttpRequest, HttpResponse } from "@azure/functions";
import Logging = require("../Library/Logging");
import TelemetryClient = require("../Library/TelemetryClient");
import { CorrelationContext, CorrelationContextManager } from "./CorrelationContextManager";
import * as sharedFuncTypes from "../Library/Functions";
import * as v3 from "@azure/functions";

/** Node.js Azure Functions handle incoming HTTP requests before Application Insights SDK is available,
* this code generate incoming request telemetry and generate correlation context to be used
Expand All @@ -14,27 +15,45 @@ export class AzureFunctionsHook {
private _autoGenerateIncomingRequests: boolean;
private _preInvocationHook: Disposable;
private _postInvocationHook: Disposable;
private _cachedModelHelper: FuncModelV3Helper | FuncModelV4Helper | null | undefined;

constructor(client: TelemetryClient) {
this._client = client;
this._autoGenerateIncomingRequests = false;
try {
this._functionsCoreModule = require("@azure/functions-core");
// Only v3 of Azure Functions library is supported right now. See matrix of versions here:
// https://github.com/Azure/azure-functions-nodejs-library
const funcProgModel = this._functionsCoreModule.getProgrammingModel();
if (funcProgModel.name === "@azure/functions" && funcProgModel.version.startsWith("3.")) {
this._addPreInvocationHook();
this._addPostInvocationHook();
} else {
Logging.warn(`AzureFunctionsHook does not support model "${funcProgModel.name}" version "${funcProgModel.version}"`);
}
this._addPreInvocationHook();
this._addPostInvocationHook();
}
catch (error) {
Logging.info("AzureFunctionsHook failed to load, not running in Azure Functions");
}
}

/**
* NOTE: The programming model can be changed any time until the first invocation
* For that reason, we delay setting the model helper until the first hook is called, but we can cache it after that
*/
private _getFuncModelHelper(): FuncModelV3Helper | FuncModelV4Helper | null {
if (this._cachedModelHelper === undefined) {
const funcProgModel = this._functionsCoreModule.getProgrammingModel();
if (funcProgModel.name === "@azure/functions") {
if (funcProgModel.version.startsWith("3.")) {
this._cachedModelHelper = new FuncModelV3Helper();
} else if (funcProgModel.version.startsWith("4.")) {
this._cachedModelHelper = new FuncModelV4Helper();
}
}

if (!this._cachedModelHelper) {
this._cachedModelHelper = null;
Logging.warn(`AzureFunctionsHook does not support model "${funcProgModel.name}" version "${funcProgModel.version}"`);
}
}

return this._cachedModelHelper;
}

public enable(isEnabled: boolean) {
this._autoGenerateIncomingRequests = isEnabled;
}
Expand All @@ -48,23 +67,28 @@ export class AzureFunctionsHook {
private _addPreInvocationHook() {
if (!this._preInvocationHook) {
this._preInvocationHook = this._functionsCoreModule.registerHook("preInvocation", async (preInvocationContext: PreInvocationContext) => {
const ctx: Context = <Context>preInvocationContext.invocationContext;
try {
// Start an AI Correlation Context using the provided Function context
let extractedContext = CorrelationContextManager.startOperation(ctx);
if (extractedContext) { // Will be null if CorrelationContextManager is not enabled, we should not try to propagate context in that case
extractedContext.customProperties.setProperty("InvocationId", ctx.invocationId);
if (ctx.traceContext.attributes) {
extractedContext.customProperties.setProperty("ProcessId", ctx.traceContext.attributes["ProcessId"]);
extractedContext.customProperties.setProperty("LogLevel", ctx.traceContext.attributes["LogLevel"]);
extractedContext.customProperties.setProperty("Category", ctx.traceContext.attributes["Category"]);
extractedContext.customProperties.setProperty("HostInstanceId", ctx.traceContext.attributes["HostInstanceId"]);
extractedContext.customProperties.setProperty("AzFuncLiveLogsSessionId", ctx.traceContext.attributes["#AzFuncLiveLogsSessionId"]);
}
preInvocationContext.functionCallback = CorrelationContextManager.wrapCallback(preInvocationContext.functionCallback, extractedContext);
if (this._isHttpTrigger(ctx) && this._autoGenerateIncomingRequests) {
preInvocationContext.hookData.appInsightsExtractedContext = extractedContext;
preInvocationContext.hookData.appInsightsStartTime = Date.now(); // Start trackRequest timer
const modelHelper = this._getFuncModelHelper();
if (modelHelper) {
const sharedContext = <sharedFuncTypes.Context>preInvocationContext.invocationContext;
// Start an AI Correlation Context using the provided Function context
let extractedContext = CorrelationContextManager.startOperation(sharedContext);
if (extractedContext) { // Will be null if CorrelationContextManager is not enabled, we should not try to propagate context in that case
extractedContext.customProperties.setProperty("InvocationId", sharedContext.invocationId);

const traceContext = sharedContext.traceContext;
if (traceContext.attributes) {
extractedContext.customProperties.setProperty("ProcessId", traceContext.attributes["ProcessId"]);
extractedContext.customProperties.setProperty("LogLevel", traceContext.attributes["LogLevel"]);
extractedContext.customProperties.setProperty("Category", traceContext.attributes["Category"]);
extractedContext.customProperties.setProperty("HostInstanceId", traceContext.attributes["HostInstanceId"]);
extractedContext.customProperties.setProperty("AzFuncLiveLogsSessionId", traceContext.attributes["#AzFuncLiveLogsSessionId"]);
}
preInvocationContext.functionCallback = CorrelationContextManager.wrapCallback(preInvocationContext.functionCallback, extractedContext);
if (modelHelper.isHttpTrigger(preInvocationContext) && this._autoGenerateIncomingRequests) {
preInvocationContext.hookData.appInsightsExtractedContext = extractedContext;
preInvocationContext.hookData.appInsightsStartTime = Date.now(); // Start trackRequest timer
}
}
}
}
Expand All @@ -80,21 +104,22 @@ export class AzureFunctionsHook {
if (!this._postInvocationHook) {
this._postInvocationHook = this._functionsCoreModule.registerHook("postInvocation", async (postInvocationContext: PostInvocationContext) => {
try {
if (this._autoGenerateIncomingRequests) {
const ctx: Context = <Context>postInvocationContext.invocationContext;
if (this._isHttpTrigger(ctx)) {
const request: HttpRequest = postInvocationContext.inputs[0];
if (request) {
const startTime: number = postInvocationContext.hookData.appInsightsStartTime || Date.now();
const response = this._getAzureFunctionResponse(postInvocationContext, ctx);
const extractedContext: CorrelationContext | undefined = postInvocationContext.hookData.appInsightsExtractedContext;
if (!extractedContext) {
this._createIncomingRequestTelemetry(request, response, startTime, null);
}
else {
CorrelationContextManager.runWithContext(extractedContext, () => {
this._createIncomingRequestTelemetry(request, response, startTime, extractedContext.operation.parentId);
});
const modelHelper = this._getFuncModelHelper();
if (modelHelper) {
if (this._autoGenerateIncomingRequests) {
if (modelHelper.isHttpTrigger(postInvocationContext)) {
const request = <sharedFuncTypes.HttpRequest>postInvocationContext.inputs[0];
if (request) {
const startTime: number = postInvocationContext.hookData.appInsightsStartTime || Date.now();
const extractedContext: CorrelationContext | undefined = postInvocationContext.hookData.appInsightsExtractedContext;
if (!extractedContext) {
this._createIncomingRequestTelemetry(request, postInvocationContext, startTime, null);
}
else {
CorrelationContextManager.runWithContext(extractedContext, () => {
this._createIncomingRequestTelemetry(request, postInvocationContext, startTime, extractedContext.operation.parentId);
});
}
}
}
}
Expand All @@ -107,10 +132,11 @@ export class AzureFunctionsHook {
}
}

private _createIncomingRequestTelemetry(request: HttpRequest, response: HttpResponse, startTime: number, parentId: string) {
private _createIncomingRequestTelemetry(request: sharedFuncTypes.HttpRequest, hookContext: PostInvocationContext, startTime: number, parentId: string) {
const values = this._getFuncModelHelper().getStatusCodes(hookContext);
let statusCode: string | number = 200; //Default
if (response) {
for (const value of [response.statusCode, response.status]) {
if (values) {
for (const value of values) {
if (typeof value === "number" && Number.isInteger(value)) {
statusCode = value;
break;
Expand All @@ -128,7 +154,7 @@ export class AzureFunctionsHook {
this._client.trackRequest({
name: request.method + " " + request.url,
resultCode: statusCode,
success: typeof(statusCode) === "number" ? (0 < statusCode) && (statusCode < 400) : undefined,
success: typeof (statusCode) === "number" ? (0 < statusCode) && (statusCode < 400) : undefined,
url: request.url,
time: new Date(startTime),
duration: Date.now() - startTime,
Expand All @@ -137,21 +163,6 @@ export class AzureFunctionsHook {
this._client.flush();
}

private _getAzureFunctionResponse(postInvocationContext: PostInvocationContext, ctx: Context): HttpResponse {
const httpOutputBinding = ctx.bindingDefinitions.find(b => b.direction === "out" && b.type.toLowerCase() === "http");
if (httpOutputBinding?.name === "$return") {
return postInvocationContext.result;
} else if (httpOutputBinding && ctx.bindings && ctx.bindings[httpOutputBinding.name] !== undefined) {
return ctx.bindings[httpOutputBinding.name];
} else {
return ctx.res;
}
}

private _isHttpTrigger(ctx: Context) {
return ctx.bindingDefinitions.find(b => b.type?.toLowerCase() === "httptrigger");
}

private _removeInvocationHooks() {
if (this._preInvocationHook) {
this._preInvocationHook.dispose();
Expand All @@ -163,3 +174,50 @@ export class AzureFunctionsHook {
}
}
}

class FuncModelV3Helper {
private _getInvocationContext(hookContext: PreInvocationContext | PostInvocationContext): v3.Context {
return <v3.Context>hookContext.invocationContext;
}

public getStatusCodes(hookContext: PostInvocationContext): unknown[] | undefined {
const ctx = this._getInvocationContext(hookContext);

let response: v3.HttpResponse | undefined;
const httpOutputBinding = ctx.bindingDefinitions.find(b => b.direction === "out" && b.type.toLowerCase() === "http");
if (httpOutputBinding?.name === "$return") {
response = hookContext.result;
} else if (httpOutputBinding && ctx.bindings && ctx.bindings[httpOutputBinding.name] !== undefined) {
response = ctx.bindings[httpOutputBinding.name];
} else {
response = ctx.res;
}

return response ? [response.statusCode, response.status] : undefined;
}

public isHttpTrigger(hookContext: PreInvocationContext | PostInvocationContext): boolean {
const ctx = this._getInvocationContext(hookContext);
return !!ctx.bindingDefinitions.find(b => b.type?.toLowerCase() === "httptrigger");
}
}

/**
* V4 is only supported on Node.js v18+
* Unfortunately that means we can't use the "@azure/functions" types for v4 or we break the build on Node <v18
*/
class FuncModelV4Helper {
private _getInvocationContext(hookContext: PreInvocationContext | PostInvocationContext): any {
return hookContext.invocationContext;
}

public getStatusCodes(hookContext: PostInvocationContext): unknown[] | undefined {
let response = hookContext.result;
return response ? [response.status] : undefined;
}

public isHttpTrigger(hookContext: PreInvocationContext | PostInvocationContext) {
const ctx = this._getInvocationContext(hookContext);
return ctx.options.trigger.type.toLowerCase() === "httptrigger";
}
}
18 changes: 9 additions & 9 deletions AutoCollection/CorrelationContextManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import events = require("events");
import Logging = require("../Library/Logging");
import * as DiagChannel from "./diagnostic-channel/initialization";
import * as azureFunctionsTypes from "../Library/Functions";
import * as sharedFuncTypes from "../Library/Functions";

// Don't reference modules from these directly. Use only for types.
import * as cls from "cls-hooked";
Expand Down Expand Up @@ -198,13 +198,13 @@ export class CorrelationContextManager {
* Create new correlation context.
*/
public static startOperation(
input: azureFunctionsTypes.Context | (http.IncomingMessage | azureFunctionsTypes.HttpRequest) | SpanContext | Span,
request?: azureFunctionsTypes.HttpRequest | string)
input: sharedFuncTypes.Context | (http.IncomingMessage | sharedFuncTypes.HttpRequest) | SpanContext | Span,
request?: sharedFuncTypes.HttpRequest | string)
: CorrelationContext | null {
const traceContext = input && (input as azureFunctionsTypes.Context).traceContext || null;
const traceContext = input && (input as sharedFuncTypes.Context).traceContext || null;
const span = input && (input as Span).spanContext ? input as Span : null;
const spanContext = input && (input as SpanContext).traceId ? input as SpanContext : null;
const headers = input && (input as http.IncomingMessage | azureFunctionsTypes.HttpRequest).headers;
const headers = input && (input as http.IncomingMessage | sharedFuncTypes.HttpRequest).headers;

// OpenTelemetry Span
if (span) {
Expand All @@ -224,7 +224,7 @@ export class CorrelationContextManager {
let tracestate = null;
operationName = traceContext.attributes["OperationName"] || operationName;
if (request) {
let azureFnRequest = request as azureFunctionsTypes.HttpRequest;
let azureFnRequest = request as sharedFuncTypes.HttpRequest;
if (azureFnRequest.headers) {
if (azureFnRequest.headers.traceparent) {
traceparent = new Traceparent(azureFnRequest.headers.traceparent);
Expand All @@ -237,10 +237,10 @@ export class CorrelationContextManager {
}
}
if (!traceparent) {
traceparent = new Traceparent(traceContext.traceparent);
traceparent = new Traceparent(traceContext.traceParent || traceContext.traceparent);
}
if (!tracestate) {
tracestate = new Tracestate(traceContext.tracestate);
tracestate = new Tracestate(traceContext.traceState || traceContext.tracestate);
}

let correlationContextHeader = undefined;
Expand All @@ -265,7 +265,7 @@ export class CorrelationContextManager {
if (headers) {
const traceparent = new Traceparent(headers.traceparent ? headers.traceparent.toString() : null);
const tracestate = new Tracestate(headers.tracestate ? headers.tracestate.toString() : null);
const parser = new HttpRequestParser(input as http.IncomingMessage | azureFunctionsTypes.HttpRequest);
const parser = new HttpRequestParser(input as http.IncomingMessage | sharedFuncTypes.HttpRequest);
const correlationContext = CorrelationContextManager.generateContextObject(
traceparent.traceId,
traceparent.parentId,
Expand Down
Loading

0 comments on commit 4034bb0

Please sign in to comment.