Skip to content

Commit

Permalink
Automatically handle request/dependency correlation in Azure Functions (
Browse files Browse the repository at this point in the history
microsoft#1044)

* Automatically handle request/dependency correlation in Azure Functions

* Update

* Adding tests
  • Loading branch information
hectorhdzg authored Dec 5, 2022
1 parent 39213a3 commit 9909cbd
Show file tree
Hide file tree
Showing 15 changed files with 312 additions and 145 deletions.
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"files.exclude": {
"**/*.js": { "when": "$(basename).ts"},
"**/*.js.map": true,
"**/*.d.ts": true,
"out/": true
},
"typescript.tsdk": "./node_modules/typescript/lib",
Expand Down
76 changes: 76 additions & 0 deletions AutoCollection/AzureFunctionsHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Context, HttpRequest } from "../Library/Functions";
import Logging = require("../Library/Logging");
import TelemetryClient = require("../Library/TelemetryClient");
import { CorrelationContextManager } from "./CorrelationContextManager";


/** 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
* by outgoing requests and other telemetry, we rely on hooks provided by Azure Functions
*/
export class AutoCollectAzureFunctions {
private _client: TelemetryClient;
private _functionsCoreModule: any;
private _preInvocationHook: any;

constructor(client: TelemetryClient) {
this._client = client;
try {
this._functionsCoreModule = require('@azure/functions-core');
}
catch (error) {
Logging.info("AutoCollectAzureFunctions failed to load, not running in Azure Functions");
}
}

public enable(isEnabled: boolean) {
if (this._functionsCoreModule) {
if (isEnabled) {
this._addPreInvocationHook();
} else {
this._removePreInvocationHook();
}
}
}

public dispose() {
this.enable(false);
this._functionsCoreModule = undefined;
}

private _addPreInvocationHook() {
// Only add hook once
if (!this._preInvocationHook) {
this._preInvocationHook = this._functionsCoreModule.registerHook('preInvocation', (context: any) => {
const originalCallback = context.functionCallback;
context.functionCallback = async (context: Context, req: HttpRequest) => {
const startTime = Date.now(); // Start trackRequest timer
// Start an AI Correlation Context using the provided Function context
const correlationContext = CorrelationContextManager.startOperation(context, req);
if (correlationContext) {
CorrelationContextManager.wrapCallback(async () => {
originalCallback(context, req);
this._client.trackRequest({
name: context?.req?.method + " " + context.req?.url,
resultCode: context?.res?.status,
success: true,
url: (req as HttpRequest)?.url,
time: new Date(startTime),
duration: Date.now() - startTime,
id: correlationContext.operation?.parentId,
});
this._client.flush();
}, correlationContext)();
}
};
});
}
}

private _removePreInvocationHook() {
if (this._preInvocationHook) {
this._preInvocationHook.dispose();
this._preInvocationHook = undefined;
}
}
}
1 change: 0 additions & 1 deletion AutoCollection/CorrelationContextManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import events = require("events");
import Logging = require("../Library/Logging");

import * as DiagChannel from "./diagnostic-channel/initialization";
import * as azureFunctionsTypes from "../Library/Functions";

Expand Down
5 changes: 3 additions & 2 deletions AutoCollection/HttpRequestParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Tracestate = require("../Library/Tracestate");
import Traceparent = require("../Library/Traceparent");
import { HttpRequest } from "../Library/Functions";


/**
* Helper class to read data from the request/response objects and convert them into the telemetry contract
*/
Expand Down Expand Up @@ -145,7 +146,7 @@ class HttpRequestParser extends RequestParser {
}

public getOperationName(tags: { [key: string]: string }) {
if(tags[HttpRequestParser.keys.operationName]){
if (tags[HttpRequestParser.keys.operationName]) {
return tags[HttpRequestParser.keys.operationName];
}
let pathName = "";
Expand Down Expand Up @@ -202,7 +203,7 @@ class HttpRequestParser extends RequestParser {
}
catch (ex) {
// Ignore errors
}
}
var absoluteUrl = url.format({
protocol: protocol,
host: request.headers.host,
Expand Down
30 changes: 17 additions & 13 deletions Declarations/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export interface IBaseConfig {
* @deprecated, please use enableWebInstrumentation instead
* Enable web snippet auto html injection, default to false, this config is NOT exposed in documentation after version 2.3.5
*/
enableAutoWebSnippetInjection?: boolean;
enableAutoWebSnippetInjection?: boolean;
/**
* @deprecated, Please use webInstrumentationConnectionString instead
* Application Insights resource connection string for web snippet, this config is NOT exposed in documentation after version 2.3.5
Expand All @@ -149,10 +149,14 @@ export interface IBaseConfig {
* Enable web instrumentation and automatic monitoring, default to false
*/
enableWebInstrumentation: boolean;
/**
* Application Insights resource connection string for web instrumentation and automatic monitoring
* Note: if no VALID connection string is provided here, web instrumentation will use the connection string during initializing Nodejs SDK
/**
* Enable automatic incoming request tracking and correct correlation when using Azure Functions
*/
enableAutoCollectAzureFunctions: boolean;
/**
* Application Insights resource connection string for web instrumentation and automatic monitoring
* Note: if no VALID connection string is provided here, web instrumentation will use the connection string during initializing Nodejs SDK
*/
webInstrumentationConnectionString?: string;
/**
* Application Insights web Instrumentation config
Expand All @@ -163,12 +167,12 @@ export interface IBaseConfig {
* see more Application Insights web Instrumentation config details at: https://github.com/microsoft/ApplicationInsights-JS#configuration
*/
webInstrumentationConfig?: IWebInstrumentationConfig[];
/**
* Application Insights web Instrumentation CDN url
* NOTE: this config can be changed from env variable: APPLICATIONINSIGHTS_WEB_INSTRUMENTATION_SOURCE or Json Config: webInstrumentationSrc
* If no resouce is provided here, default CDN endpoint: https://js.monitor.azure.com/scripts/b/ai will be used
* see more details at: https://github.com/microsoft/ApplicationInsights-JS
*/
/**
* Application Insights web Instrumentation CDN url
* NOTE: this config can be changed from env variable: APPLICATIONINSIGHTS_WEB_INSTRUMENTATION_SOURCE or Json Config: webInstrumentationSrc
* If no resouce is provided here, default CDN endpoint: https://js.monitor.azure.com/scripts/b/ai will be used
* see more details at: https://github.com/microsoft/ApplicationInsights-JS
*/
webInstrumentationSrc?: string;
}

Expand All @@ -178,9 +182,9 @@ export interface IWebInstrumentationConfig {
* see more Application Insights web Instrumentation config details at: https://github.com/microsoft/ApplicationInsights-JS#configuration
*/
name: string;
/**
* value provided to replace the default config value above
*/
/**
* value provided to replace the default config value above
*/
value: string | boolean | number;
}

Expand Down
2 changes: 2 additions & 0 deletions Library/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Config implements IConfig {
public enableAutoCollectRequests: boolean;
public enableAutoCollectDependencies: boolean;
public enableAutoDependencyCorrelation: boolean;
public enableAutoCollectAzureFunctions: boolean;
public enableSendLiveMetrics: boolean;
public enableUseDiskRetryCaching: boolean;
public enableUseAsyncHooks: boolean;
Expand Down Expand Up @@ -169,6 +170,7 @@ class Config implements IConfig {
this.distributedTracingMode = jsonConfig.distributedTracingMode;
this.enableAutoCollectConsole = jsonConfig.enableAutoCollectConsole;
this.enableAutoCollectDependencies = jsonConfig.enableAutoCollectDependencies;
this.enableAutoCollectAzureFunctions = jsonConfig.enableAutoCollectAzureFunctions;
this.enableAutoCollectExceptions = jsonConfig.enableAutoCollectExceptions;
this.enableAutoCollectExtendedMetrics = jsonConfig.enableAutoCollectExtendedMetrics;
this.enableAutoCollectExternalLoggers = jsonConfig.enableAutoCollectExternalLoggers;
Expand Down
13 changes: 12 additions & 1 deletion Library/Functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
* to your function from the Azure Functions runtime on function invocation.
*/
export interface Context {
traceContext: TraceContext
traceContext: TraceContext;

/**
* HTTP request object. Provided to your function when using HTTP Bindings.
*/
req?: HttpRequest;
/**
* HTTP response object. Provided to your function when using HTTP Bindings.
*/
res?: {
[key: string]: any;
};
}

/**
Expand Down
2 changes: 2 additions & 0 deletions Library/JsonConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class JsonConfig implements IJsonConfig {
public enableAutoCollectRequests: boolean;
public enableAutoCollectDependencies: boolean;
public enableAutoDependencyCorrelation: boolean;
public enableAutoCollectAzureFunctions: boolean;
public enableUseAsyncHooks: boolean;
public enableUseDiskRetryCaching: boolean;
public enableResendInterval: number;
Expand Down Expand Up @@ -205,6 +206,7 @@ export class JsonConfig implements IJsonConfig {
this.enableAutoCollectRequests = jsonConfig.enableAutoCollectRequests;
this.enableAutoCollectDependencies = jsonConfig.enableAutoCollectDependencies;
this.enableAutoDependencyCorrelation = jsonConfig.enableAutoDependencyCorrelation;
this.enableAutoCollectAzureFunctions = jsonConfig.enableAutoCollectAzureFunctions;
this.enableUseAsyncHooks = jsonConfig.enableUseAsyncHooks;
this.enableUseDiskRetryCaching = jsonConfig.enableUseDiskRetryCaching;
this.enableResendInterval = jsonConfig.enableResendInterval;
Expand Down
51 changes: 51 additions & 0 deletions Tests/AutoCollection/AzureFunctionsHook.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert = require("assert");
import sinon = require("sinon");
import { TelemetryClient } from "../../applicationinsights";

import { AutoCollectAzureFunctions } from "../../AutoCollection/AzureFunctionsHook";

const testModule = {
registerHook(type: string, hook: any) {

}
};

describe("AutoCollection/AutoCollectAzureFunctions", () => {

it("constructor", () => {
let client = new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
let auto = new AutoCollectAzureFunctions(client);
assert.equal(auto["_functionsCoreModule"], undefined, "Module is not available so it should be undefined unless running in AzFn env");
});

it("enable", () => {
let client = new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
let auto = new AutoCollectAzureFunctions(client);
auto["_functionsCoreModule"] = testModule;
const addStub = sinon.stub(auto, "_addPreInvocationHook");
const removeStub = sinon.stub(auto, "_removePreInvocationHook");
auto.enable(true);
assert.ok(removeStub.notCalled);
assert.ok(addStub.calledOnce);
});

it("disable", () => {
let client = new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
let auto = new AutoCollectAzureFunctions(client);
auto["_functionsCoreModule"] = testModule;
const addStub = sinon.stub(auto, "_addPreInvocationHook");
const removeStub = sinon.stub(auto, "_removePreInvocationHook");
auto.enable(false);
assert.ok(removeStub.calledOnce);
assert.ok(addStub.notCalled);
});

it("_addPreInvocationHook", () => {
let client = new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
let auto = new AutoCollectAzureFunctions(client);
const registerHook = sinon.stub(testModule, "registerHook");
auto["_functionsCoreModule"] = testModule;
auto["_addPreInvocationHook"]();
assert.ok(registerHook.calledOnce);
});
});
31 changes: 14 additions & 17 deletions Tests/AutoCollection/CorrelationContextManager.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,6 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
const request = {
method: "GET",
url: "/search",
connection: {
encrypted: false
},
headers: {
host: "bing.com",
traceparent: functionContext.traceparent,
Expand Down Expand Up @@ -327,7 +324,7 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
it("should return null if the ContextManager is disabled (inside context)", (done) => {
CorrelationContextManager.enable();

CorrelationContextManager.runWithContext(testContext, ()=>{
CorrelationContextManager.runWithContext(testContext, () => {
CorrelationContextManager.disable();
assert.equal(CorrelationContextManager.getCurrentContext(), null);
done();
Expand All @@ -336,16 +333,16 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
it("should return null if in a context", (done) => {
CorrelationContextManager.enable();

CorrelationContextManager.runWithContext(testContext, ()=>{
CorrelationContextManager.runWithContext(testContext, () => {
assert.equal(CorrelationContextManager.getCurrentContext(), null);
done();
});
});
it("should return null if called by an asynchronous callback in a context", (done) => {
CorrelationContextManager.enable();

CorrelationContextManager.runWithContext(testContext, ()=>{
process.nextTick(()=>{
CorrelationContextManager.runWithContext(testContext, () => {
process.nextTick(() => {
assert.equal(CorrelationContextManager.getCurrentContext(), null);
done();
});
Expand All @@ -354,19 +351,19 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
it("should return null to asynchronous callbacks occuring in parallel", (done) => {
CorrelationContextManager.enable();

CorrelationContextManager.runWithContext(testContext, ()=>{
process.nextTick(()=>{
CorrelationContextManager.runWithContext(testContext, () => {
process.nextTick(() => {
assert.equal(CorrelationContextManager.getCurrentContext(), null);
});
});

CorrelationContextManager.runWithContext(testContext2, ()=>{
process.nextTick(()=>{
CorrelationContextManager.runWithContext(testContext2, () => {
process.nextTick(() => {
assert.equal(CorrelationContextManager.getCurrentContext(), null);
});
});

setTimeout(()=>done(), 10);
setTimeout(() => done(), 10);
});
});

Expand Down Expand Up @@ -401,21 +398,21 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
it("should not return a function that restores a null context at call-time into the supplied function if enabled", (done) => {
CorrelationContextManager.enable();

var sharedFn = ()=> {
var sharedFn = () => {
assert.equal(CorrelationContextManager.getCurrentContext(), null);
};

CorrelationContextManager.runWithContext(testContext, ()=>{
CorrelationContextManager.runWithContext(testContext, () => {
sharedFn = CorrelationContextManager.wrapCallback(sharedFn);
});

CorrelationContextManager.runWithContext(testContext2, ()=>{
setTimeout(()=>{
CorrelationContextManager.runWithContext(testContext2, () => {
setTimeout(() => {
sharedFn();
}, 8);
});

setTimeout(()=>done(), 10);
setTimeout(() => done(), 10);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions Tests/Library/Config.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("Library/Config", () => {
assert.equal(config.enableAutoCollectRequests, false);
assert.equal(config.enableAutoCollectDependencies, false);
assert.equal(config.enableAutoDependencyCorrelation, false);
assert.equal(config.enableAutoCollectAzureFunctions, false);
assert.equal(config.enableUseAsyncHooks, false);
assert.equal(config.disableStatsbeat, false);
assert.equal(config.enableAutoCollectExtendedMetrics, false);
Expand Down
1 change: 1 addition & 0 deletions Tests/Library/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"enableAutoCollectRequests": false,
"enableAutoCollectDependencies": false,
"enableAutoDependencyCorrelation": false,
"enableAutoCollectAzureFunctions": false,
"enableUseAsyncHooks": false,
"disableStatsbeat": false,
"noHttpAgentKeepAlive": false,
Expand Down
1 change: 1 addition & 0 deletions Tests/Library/jsonConfig.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe("Json Config", () => {
assert.equal(config.enableAutoCollectRequests, false);
assert.equal(config.enableAutoCollectDependencies, false);
assert.equal(config.enableAutoDependencyCorrelation, false);
assert.equal(config.enableAutoCollectAzureFunctions, false);
assert.equal(config.enableUseAsyncHooks, false);
assert.equal(config.disableStatsbeat, false);
assert.equal(config.enableAutoCollectExtendedMetrics, false);
Expand Down
Loading

0 comments on commit 9909cbd

Please sign in to comment.