Skip to content

Commit 5a113b2

Browse files
Add Plugin for @azure/functions (#4716)
* adds azure functions plugin * adds azure_functions plugin to API documentation * add typescript test for azure functions plugin * adds integration test for azure-functions plugin * add licenses for added dev packages * add azure-functions plugin to github workflow * use pipe for azure-functions integration test child process * update azure-functions integration test api route * refactor azure-functions integration test * add azure func command to path * remove yarn.lock file from azure-functions integration test * allow span kind to be server for azure functions * Update index.d.ts Co-authored-by: Roch Devost <roch.devost@datadoghq.com> * add serverless util * use built in url parser * remove serverless logic from web util * remove wait-on dependency * remove find-process dependency * Revert "remove find-process dependency" This reverts commit 3c004c5. * call func start directly and remove find-process dependency * simplify serverless util * Revert "simplify serverless util" This reverts commit 91a2dd9. * simplify serverless util --------- Co-authored-by: Roch Devost <roch.devost@datadoghq.com>
1 parent ce0bdce commit 5a113b2

File tree

24 files changed

+612
-6
lines changed

24 files changed

+612
-6
lines changed

.github/workflows/plugins.yml

+8
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ jobs:
205205
- uses: actions/checkout@v4
206206
- uses: ./.github/actions/plugins/upstream
207207

208+
azure-functions:
209+
runs-on: ubuntu-latest
210+
env:
211+
PLUGINS: azure-functions
212+
steps:
213+
- uses: actions/checkout@v4
214+
- uses: ./.github/actions/plugins/test
215+
208216
bluebird:
209217
runs-on: ubuntu-latest
210218
env:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,4 @@ packages/dd-trace/test/appsec/next/*/package.json
125125
packages/dd-trace/test/appsec/next/*/node_modules
126126
packages/dd-trace/test/appsec/next/*/yarn.lock
127127
!packages/dd-trace/**/telemetry/logs
128+
packages/datadog-plugin-azure-functions/test/integration-test/fixtures/node_modules

docs/API.md

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ tracer.use('pg', {
2828
<h5 id="aws-sdk"></h5>
2929
<h5 id="aws-sdk-tags"></h5>
3030
<h5 id="aws-sdk-config"></h5>
31+
<h5 id="azure-functions"></h5>
3132
<h5 id="bunyan"></h5>
3233
<h5 id="couchbase"></h5>
3334
<h5 id="cucumber"></h5>
@@ -105,6 +106,7 @@ tracer.use('pg', {
105106
* [amqplib](./interfaces/export_.plugins.amqplib.html)
106107
* [avsc](./interfaces/export_.plugins.avsc.html)
107108
* [aws-sdk](./interfaces/export_.plugins.aws_sdk.html)
109+
* [azure-functions](./interfaces/export_.plugins.azure_functions.html)
108110
* [bluebird](./interfaces/export_.plugins.bluebird.html)
109111
* [couchbase](./interfaces/export_.plugins.couchbase.html)
110112
* [cucumber](./interfaces/export_.plugins.cucumber.html)

docs/test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ tracer.use('amqplib');
286286
tracer.use('avsc');
287287
tracer.use('aws-sdk');
288288
tracer.use('aws-sdk', awsSdkOptions);
289+
tracer.use('azure-functions');
289290
tracer.use('bunyan');
290291
tracer.use('couchbase');
291292
tracer.use('cassandra-driver');

ext/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
declare const types: {
22
HTTP: 'http'
3+
SERVERLESS: 'serverless'
34
WEB: 'web'
45
}
56

ext/types.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
module.exports = {
44
HTTP: 'http',
5+
SERVERLESS: 'serverless',
56
WEB: 'web'
67
}

index.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ interface Plugins {
149149
"apollo": tracer.plugins.apollo;
150150
"avsc": tracer.plugins.avsc;
151151
"aws-sdk": tracer.plugins.aws_sdk;
152+
"azure-functions": tracer.plugins.azure_functions;
152153
"bunyan": tracer.plugins.bunyan;
153154
"cassandra-driver": tracer.plugins.cassandra_driver;
154155
"child_process": tracer.plugins.child_process;
@@ -1237,6 +1238,12 @@ declare namespace tracer {
12371238
[key: string]: boolean | Object | undefined;
12381239
}
12391240

1241+
/**
1242+
* This plugin automatically instruments the
1243+
* @azure/functions module.
1244+
*/
1245+
interface azure_functions extends Instrumentation {}
1246+
12401247
/**
12411248
* This plugin patches the [bunyan](https://github.com/trentm/node-bunyan)
12421249
* to automatically inject trace identifiers in log records when the

integration-tests/helpers/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ function assertUUID (actual, msg = 'not a valid UUID') {
358358

359359
module.exports = {
360360
FakeAgent,
361+
hookFile,
361362
assertObjectContains,
362363
assertUUID,
363364
spawnProc,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const {
4+
addHook
5+
} = require('./helpers/instrument')
6+
const shimmer = require('../../datadog-shimmer')
7+
const dc = require('dc-polyfill')
8+
9+
const azureFunctionsChannel = dc.tracingChannel('datadog:azure-functions:invoke')
10+
11+
addHook({ name: '@azure/functions', versions: ['>=4'] }, azureFunction => {
12+
const { app } = azureFunction
13+
14+
shimmer.wrap(app, 'deleteRequest', wrapHandler)
15+
shimmer.wrap(app, 'http', wrapHandler)
16+
shimmer.wrap(app, 'get', wrapHandler)
17+
shimmer.wrap(app, 'patch', wrapHandler)
18+
shimmer.wrap(app, 'post', wrapHandler)
19+
shimmer.wrap(app, 'put', wrapHandler)
20+
21+
return azureFunction
22+
})
23+
24+
// The http methods are overloaded so we need to check which type of argument was passed in order to wrap the handler
25+
// The arguments are either an object with a handler property or the handler function itself
26+
function wrapHandler (method) {
27+
return function (name, arg) {
28+
if (typeof arg === 'object' && arg.hasOwnProperty('handler')) {
29+
const options = arg
30+
shimmer.wrap(options, 'handler', handler => traceHandler(handler, name, method.name))
31+
} else if (typeof arg === 'function') {
32+
const handler = arg
33+
arguments[1] = shimmer.wrapFunction(handler, handler => traceHandler(handler, name, method.name))
34+
}
35+
return method.apply(this, arguments)
36+
}
37+
}
38+
39+
function traceHandler (handler, functionName, methodName) {
40+
return function (...args) {
41+
const httpRequest = args[0]
42+
const invocationContext = args[1]
43+
return azureFunctionsChannel.tracePromise(
44+
handler,
45+
{ functionName, httpRequest, invocationContext, methodName },
46+
this, ...args)
47+
}
48+
}

packages/datadog-instrumentations/src/helpers/hooks.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
'@apollo/gateway': () => require('../apollo'),
66
'apollo-server-core': () => require('../apollo-server-core'),
77
'@aws-sdk/smithy-client': () => require('../aws-sdk'),
8+
'@azure/functions': () => require('../azure-functions'),
89
'@cucumber/cucumber': () => require('../cucumber'),
910
'@playwright/test': () => require('../playwright'),
1011
'@elastic/elasticsearch': () => require('../elasticsearch'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict'
2+
3+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4+
const { storage } = require('../../datadog-core')
5+
const serverless = require('../../dd-trace/src/plugins/util/serverless')
6+
const web = require('../../dd-trace/src/plugins/util/web')
7+
8+
const triggerMap = {
9+
deleteRequest: 'Http',
10+
http: 'Http',
11+
get: 'Http',
12+
patch: 'Http',
13+
post: 'Http',
14+
put: 'Http'
15+
}
16+
17+
class AzureFunctionsPlugin extends TracingPlugin {
18+
static get id () { return 'azure-functions' }
19+
static get operation () { return 'invoke' }
20+
static get kind () { return 'server' }
21+
static get type () { return 'serverless' }
22+
23+
static get prefix () { return 'tracing:datadog:azure-functions:invoke' }
24+
25+
bindStart (ctx) {
26+
const { functionName, methodName } = ctx
27+
const store = storage.getStore()
28+
29+
const span = this.startSpan(this.operationName(), {
30+
service: this.serviceName(),
31+
type: 'serverless',
32+
meta: {
33+
'aas.function.name': functionName,
34+
'aas.function.trigger': mapTriggerTag(methodName)
35+
}
36+
}, false)
37+
38+
ctx.span = span
39+
ctx.parentStore = store
40+
ctx.currentStore = { ...store, span }
41+
42+
return ctx.currentStore
43+
}
44+
45+
error (ctx) {
46+
this.addError(ctx.error)
47+
ctx.currentStore.span.setTag('error.message', ctx.error)
48+
}
49+
50+
asyncEnd (ctx) {
51+
const { httpRequest, result = {} } = ctx
52+
const path = (new URL(httpRequest.url)).pathname
53+
const req = {
54+
method: httpRequest.method,
55+
headers: Object.fromEntries(httpRequest.headers.entries()),
56+
url: path
57+
}
58+
59+
const context = web.patch(req)
60+
context.config = this.config
61+
context.paths = [path]
62+
context.res = { statusCode: result.status }
63+
context.span = ctx.currentStore.span
64+
65+
serverless.finishSpan(context)
66+
}
67+
68+
configure (config) {
69+
return super.configure(web.normalizeConfig(config))
70+
}
71+
}
72+
73+
function mapTriggerTag (methodName) {
74+
return triggerMap[methodName] || 'Unknown'
75+
}
76+
77+
module.exports = AzureFunctionsPlugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict'
2+
3+
const {
4+
FakeAgent,
5+
hookFile,
6+
createSandbox,
7+
curlAndAssertMessage
8+
} = require('../../../../integration-tests/helpers')
9+
const { spawn } = require('child_process')
10+
const { assert } = require('chai')
11+
12+
describe('esm', () => {
13+
let agent
14+
let proc
15+
let sandbox
16+
17+
withVersions('azure-functions', '@azure/functions', version => {
18+
before(async function () {
19+
this.timeout(50000)
20+
sandbox = await createSandbox([`@azure/functions@${version}`, 'azure-functions-core-tools@4'], false,
21+
['./packages/datadog-plugin-azure-functions/test/integration-test/fixtures/*'])
22+
})
23+
24+
after(async function () {
25+
this.timeout(50000)
26+
await sandbox.remove()
27+
})
28+
29+
beforeEach(async () => {
30+
agent = await new FakeAgent().start()
31+
})
32+
33+
afterEach(async () => {
34+
proc && proc.kill('SIGINT')
35+
await agent.stop()
36+
})
37+
38+
it('is instrumented', async () => {
39+
const envArgs = {
40+
PATH: `${sandbox.folder}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`
41+
}
42+
proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'func', ['start'], agent.port, undefined, envArgs)
43+
44+
return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/httptest', ({ headers, payload }) => {
45+
assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`)
46+
assert.isArray(payload)
47+
assert.strictEqual(payload.length, 1)
48+
assert.isArray(payload[0])
49+
assert.strictEqual(payload[0].length, 1)
50+
assert.propertyVal(payload[0][0], 'name', 'azure-functions.invoke')
51+
})
52+
}).timeout(50000)
53+
})
54+
})
55+
56+
async function spawnPluginIntegrationTestProc (cwd, command, args, agentPort, stdioHandler, additionalEnvArgs = {}) {
57+
let env = {
58+
NODE_OPTIONS: `--loader=${hookFile}`,
59+
DD_TRACE_AGENT_PORT: agentPort
60+
}
61+
env = { ...env, ...additionalEnvArgs }
62+
return spawnProc(command, args, {
63+
cwd,
64+
env
65+
}, stdioHandler)
66+
}
67+
68+
function spawnProc (command, args, options = {}, stdioHandler, stderrHandler) {
69+
const proc = spawn(command, args, { ...options, stdio: 'pipe' })
70+
return new Promise((resolve, reject) => {
71+
proc
72+
.on('error', reject)
73+
.on('exit', code => {
74+
if (code !== 0) {
75+
reject(new Error(`Process exited with status code ${code}.`))
76+
}
77+
resolve()
78+
})
79+
80+
proc.stdout.on('data', data => {
81+
if (stdioHandler) {
82+
stdioHandler(data)
83+
}
84+
// eslint-disable-next-line no-console
85+
if (!options.silent) console.log(data.toString())
86+
87+
if (data.toString().includes('http://localhost:7071/api/httptest')) {
88+
resolve(proc)
89+
}
90+
})
91+
92+
proc.stderr.on('data', data => {
93+
if (stderrHandler) {
94+
stderrHandler(data)
95+
}
96+
// eslint-disable-next-line no-console
97+
if (!options.silent) console.error(data.toString())
98+
})
99+
})
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"applicationInsights": {
5+
"samplingSettings": {
6+
"isEnabled": true,
7+
"excludedTypes": "Request"
8+
}
9+
}
10+
},
11+
"extensionBundle": {
12+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
13+
"version": "[4.*, 5.0.0)"
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"IsEncrypted": false,
3+
"Values": {
4+
"FUNCTIONS_WORKER_RUNTIME": "node",
5+
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
6+
"AzureWebJobsStorage": ""
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "azure-function-node-integration-test",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "src/functions/server.mjs",
6+
"scripts": {
7+
"start": "func start"
8+
},
9+
"dependencies": {
10+
"@azure/functions": "^4.0.0"
11+
},
12+
"devDependencies": {
13+
"azure-functions-core-tools": "^4.x"
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'dd-trace/init.js'
2+
import { app } from '@azure/functions'
3+
4+
async function handlerFunction (request, context) {
5+
return {
6+
status: 200,
7+
body: 'Hello Datadog!'
8+
}
9+
}
10+
11+
app.http('httptest', {
12+
methods: ['GET'],
13+
authLevel: 'anonymous',
14+
handler: handlerFunction
15+
})

0 commit comments

Comments
 (0)