Skip to content

Commit afd1486

Browse files
authored
[Azure Monitor OpenTelemetry] Add Azure Functions Correlation Hook (Azure#26313)
Functionality for @azure/monitor-opentelemetry package Adding Azure Fn code to handle auto correlation when running in that environment
1 parent dd2e2a6 commit afd1486

File tree

9 files changed

+223
-15
lines changed

9 files changed

+223
-15
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/monitor/monitor-opentelemetry/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"typescript": "~5.0.0"
8787
},
8888
"dependencies": {
89+
"@azure/functions": "^3.2.0",
8990
"@azure/monitor-opentelemetry-exporter": "1.0.0-beta.11",
9091
"@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.3",
9192
"@opentelemetry/api": "^1.4.1",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { Context as AzureFnContext } from "@azure/functions";
5+
import {
6+
context,
7+
propagation,
8+
ROOT_CONTEXT,
9+
Context as OpenTelemetryContext,
10+
} from "@opentelemetry/api";
11+
import { Logger } from "../shared/logging";
12+
13+
export class AzureFunctionsHook {
14+
private _functionsCoreModule: any;
15+
private _preInvocationHook: any;
16+
17+
constructor() {
18+
try {
19+
// TODO: Add types files when publicly available
20+
this._functionsCoreModule = require("@azure/functions-core");
21+
// Only v3 of Azure Functions library is supported right now. See matrix of versions here:
22+
// https://github.com/Azure/azure-functions-nodejs-library
23+
const funcProgModel = this._functionsCoreModule.getProgrammingModel();
24+
if (funcProgModel.name === "@azure/functions" && funcProgModel.version.startsWith("3.")) {
25+
this._addPreInvocationHook();
26+
} else {
27+
Logger.getInstance().debug(
28+
`AzureFunctionsHook does not support model "${funcProgModel.name}" version "${funcProgModel.version}"`
29+
);
30+
}
31+
} catch (error) {
32+
Logger.getInstance().debug(
33+
"@azure/functions-core failed to load, not running in Azure Functions"
34+
);
35+
}
36+
}
37+
38+
public shutdown() {
39+
if (this._preInvocationHook) {
40+
this._preInvocationHook.dispose();
41+
this._preInvocationHook = undefined;
42+
}
43+
this._functionsCoreModule = undefined;
44+
}
45+
46+
private _addPreInvocationHook() {
47+
if (!this._preInvocationHook) {
48+
this._preInvocationHook = this._functionsCoreModule.registerHook(
49+
"preInvocation",
50+
async (preInvocationContext: any) => {
51+
const ctx: AzureFnContext = <AzureFnContext>preInvocationContext.invocationContext;
52+
// Update context to use Azure Functions one
53+
let extractedContext: OpenTelemetryContext | any = null;
54+
try {
55+
if (ctx.traceContext) {
56+
extractedContext = propagation.extract(ROOT_CONTEXT, ctx.traceContext);
57+
}
58+
const currentContext = extractedContext || context.active();
59+
preInvocationContext.functionCallback = context.bind(
60+
currentContext,
61+
preInvocationContext.functionCallback
62+
);
63+
} catch (err) {
64+
Logger.getInstance().error("Failed to propagate context in Azure Functions", err);
65+
}
66+
}
67+
);
68+
}
69+
}
70+
}

sdk/monitor/monitor-opentelemetry/src/traces/handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { AzureMonitorOpenTelemetryConfig } from "../shared/config";
3131
import { MetricHandler } from "../metrics/handler";
3232
import { ignoreOutgoingRequestHook } from "../utils/common";
3333
import { AzureMonitorSpanProcessor } from "./spanProcessor";
34+
import { AzureFunctionsHook } from "./azureFnHook";
3435

3536
/**
3637
* Azure Monitor OpenTelemetry Trace Handler
@@ -50,6 +51,7 @@ export class TraceHandler {
5051
private _redis4Instrumentation?: Instrumentation;
5152
private _config: AzureMonitorOpenTelemetryConfig;
5253
private _metricHandler?: MetricHandler;
54+
private _azureFunctionsHook: AzureFunctionsHook;
5355

5456
/**
5557
* Initializes a new instance of the TraceHandler class.
@@ -83,6 +85,7 @@ export class TraceHandler {
8385
const azureSpanProcessor = new AzureMonitorSpanProcessor(this._metricHandler);
8486
this._tracerProvider.addSpanProcessor(azureSpanProcessor);
8587
}
88+
this._azureFunctionsHook = new AzureFunctionsHook();
8689
this._initializeInstrumentations();
8790
}
8891

@@ -105,6 +108,7 @@ export class TraceHandler {
105108
*/
106109
public async shutdown(): Promise<void> {
107110
await this._tracerProvider.shutdown();
111+
this._azureFunctionsHook.shutdown();
108112
}
109113

110114
/**

sdk/monitor/monitor-opentelemetry/test/internal/unit/logHandler.test.ts renamed to sdk/monitor/monitor-opentelemetry/test/internal/unit/logs/logHandler.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import sinon from "sinon";
66
import { trace, context, isValidTraceId, isValidSpanId } from "@opentelemetry/api";
77
import { LogRecord as APILogRecord } from "@opentelemetry/api-logs";
88
import { ExportResultCode } from "@opentelemetry/core";
9-
import { LogHandler } from "../../../src/logs";
10-
import { MetricHandler } from "../../../src/metrics";
11-
import { TraceHandler } from "../../../src/traces";
12-
import { AzureMonitorOpenTelemetryConfig } from "../../../src/shared";
9+
import { LogHandler } from "../../../../src/logs";
10+
import { MetricHandler } from "../../../../src/metrics";
11+
import { TraceHandler } from "../../../../src/traces";
12+
import { AzureMonitorOpenTelemetryConfig } from "../../../../src/shared";
1313

1414
describe("LogHandler", () => {
1515
let sandbox: sinon.SinonSandbox;

sdk/monitor/monitor-opentelemetry/test/internal/unit/metricHandler.test.ts renamed to sdk/monitor/monitor-opentelemetry/test/internal/unit/metrics/metricHandler.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Licensed under the MIT license.
33

44
import * as assert from "assert";
5-
import { MetricHandler } from "../../../src/metrics";
6-
import { AzureMonitorOpenTelemetryConfig } from "../../../src/shared";
5+
import { MetricHandler } from "../../../../src/metrics";
6+
import { AzureMonitorOpenTelemetryConfig } from "../../../../src/shared";
77

88
describe("MetricHandler", () => {
99
let _config: AzureMonitorOpenTelemetryConfig;

sdk/monitor/monitor-opentelemetry/test/internal/unit/standadMetrics.test.ts renamed to sdk/monitor/monitor-opentelemetry/test/internal/unit/metrics/standadMetrics.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
import { ExportResultCode } from "@opentelemetry/core";
1313
import { LoggerProvider, LogRecord, Logger } from "@opentelemetry/sdk-logs";
1414
import { Resource } from "@opentelemetry/resources";
15-
import { StandardMetrics } from "../../../src/metrics/standardMetrics";
16-
import { AzureMonitorOpenTelemetryConfig } from "../../../src/shared";
15+
import { StandardMetrics } from "../../../../src/metrics/standardMetrics";
16+
import { AzureMonitorOpenTelemetryConfig } from "../../../../src/shared";
1717

1818
describe("#StandardMetricsHandler", () => {
1919
let exportStub: sinon.SinonStub;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import * as assert from "assert";
5+
import * as sinon from "sinon";
6+
import { AzureFunctionsHook } from "../../../../src/traces/azureFnHook";
7+
import { TraceHandler } from "../../../../src/traces";
8+
import { Logger } from "../../../../src/shared/logging";
9+
import { AzureMonitorOpenTelemetryConfig } from "../../../../src/shared";
10+
11+
describe("Library/AzureFunctionsHook", () => {
12+
let sandbox: sinon.SinonSandbox;
13+
14+
before(() => {
15+
sandbox = sinon.createSandbox();
16+
});
17+
18+
afterEach(() => {
19+
sandbox.restore();
20+
});
21+
22+
it("Hook not added if not running in Azure Functions", () => {
23+
const spy = sandbox.spy(Logger.getInstance(), "debug");
24+
let hook = new AzureFunctionsHook();
25+
assert.equal(hook["_functionsCoreModule"], undefined);
26+
assert.ok(spy.called);
27+
assert.equal(
28+
spy.args[0][0],
29+
"@azure/functions-core failed to load, not running in Azure Functions"
30+
);
31+
});
32+
33+
describe("AutoCollection/AzureFunctionsHook load fake Azure Functions Core", () => {
34+
let originalRequire: any;
35+
36+
before(() => {
37+
var Module = require("module");
38+
originalRequire = Module.prototype.require;
39+
});
40+
41+
afterEach(() => {
42+
var Module = require("module");
43+
Module.prototype.require = originalRequire;
44+
});
45+
46+
it("Hook not added if using not supported programming model", () => {
47+
var Module = require("module");
48+
var preInvocationCalled = false;
49+
Module.prototype.require = function () {
50+
if (arguments[0] === "@azure/functions-core") {
51+
return {
52+
registerHook(name: string) {
53+
if (name === "preInvocation") {
54+
preInvocationCalled = true;
55+
}
56+
},
57+
getProgrammingModel() {
58+
return {
59+
name: "@azure/functions",
60+
version: "2.x",
61+
};
62+
},
63+
};
64+
}
65+
return originalRequire.apply(this, arguments);
66+
};
67+
let azureFnHook = new AzureFunctionsHook();
68+
assert.ok(azureFnHook, "azureFnHook");
69+
assert.ok(!preInvocationCalled, "preInvocationCalled");
70+
});
71+
72+
it("Pre Invokation Hook added if running in Azure Functions and context is propagated", () => {
73+
let Module = require("module");
74+
let preInvocationCalled = false;
75+
let config = new AzureMonitorOpenTelemetryConfig();
76+
config.azureMonitorExporterConfig.connectionString =
77+
"InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;";
78+
let traceHandler = new TraceHandler(config);
79+
80+
Module.prototype.require = function () {
81+
if (arguments[0] === "@azure/functions-core") {
82+
return {
83+
registerHook(name: string, callback: any) {
84+
if (name === "preInvocation") {
85+
preInvocationCalled = true;
86+
let ctx = {
87+
res: { status: 400 },
88+
invocationId: "testinvocationId",
89+
traceContext: {
90+
traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
91+
tracestate: "",
92+
attributes: {},
93+
},
94+
};
95+
let preInvocationContext: any = {
96+
inputs: [],
97+
functionCallback: () => {
98+
let span = traceHandler.getTracer().startSpan("test");
99+
// Context should be propagated here
100+
assert.equal(
101+
(span as any)["_spanContext"]["traceId"],
102+
"0af7651916cd43dd8448eb211c80319c"
103+
);
104+
assert.ok((span as any)["_spanContext"]["spanId"]);
105+
},
106+
hookData: {},
107+
appHookData: {},
108+
invocationContext: ctx,
109+
};
110+
// Azure Functions should call preinvocation callback
111+
callback(preInvocationContext);
112+
// Azure Functions should call customer function callback
113+
preInvocationContext.functionCallback(null, null);
114+
}
115+
},
116+
117+
getProgrammingModel() {
118+
return {
119+
name: "@azure/functions",
120+
version: "3.x",
121+
};
122+
},
123+
};
124+
}
125+
return originalRequire.apply(this, arguments);
126+
};
127+
let azureFnHook = new AzureFunctionsHook();
128+
assert.ok(azureFnHook, "azureFnHook");
129+
assert.ok(preInvocationCalled, "preInvocationCalled");
130+
});
131+
});
132+
});

sdk/monitor/monitor-opentelemetry/test/internal/unit/traceHandler.test.ts renamed to sdk/monitor/monitor-opentelemetry/test/internal/unit/traces/traceHandler.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import * as assert from "assert";
55
import * as sinon from "sinon";
66
import { ExportResultCode } from "@opentelemetry/core";
77

8-
import { TraceHandler } from "../../../src/traces";
9-
import { MetricHandler } from "../../../src/metrics";
10-
import { AzureMonitorOpenTelemetryConfig } from "../../../src/shared";
8+
import { TraceHandler } from "../../../../src/traces";
9+
import { MetricHandler } from "../../../../src/metrics";
10+
import { AzureMonitorOpenTelemetryConfig } from "../../../../src/shared";
1111
import { HttpInstrumentationConfig } from "@opentelemetry/instrumentation-http";
1212
import { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
1313
import { Span } from "@opentelemetry/api";

0 commit comments

Comments
 (0)