Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Plugin for @azure/functions #4716

Merged
merged 25 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
53c0632
adds azure functions plugin
duncanpharvey Sep 18, 2024
a82c482
adds azure_functions plugin to API documentation
duncanpharvey Sep 23, 2024
45db12c
add typescript test for azure functions plugin
duncanpharvey Sep 24, 2024
6bd7123
adds integration test for azure-functions plugin
duncanpharvey Sep 27, 2024
f9749c7
add licenses for added dev packages
duncanpharvey Sep 27, 2024
b8b2316
add azure-functions plugin to github workflow
duncanpharvey Sep 27, 2024
1aa235f
use pipe for azure-functions integration test child process
duncanpharvey Sep 27, 2024
500103c
update azure-functions integration test api route
duncanpharvey Sep 27, 2024
ba88022
Merge branch 'master' into duncan-harvey/azure-functions-integration
duncanpharvey Sep 27, 2024
195082c
refactor azure-functions integration test
duncanpharvey Sep 27, 2024
d5a3c8d
add azure func command to path
duncanpharvey Sep 27, 2024
f1cf495
remove yarn.lock file from azure-functions integration test
duncanpharvey Sep 27, 2024
6b405c7
allow span kind to be server for azure functions
duncanpharvey Oct 1, 2024
201d9c0
Update index.d.ts
duncanpharvey Oct 4, 2024
766e740
add serverless util
duncanpharvey Oct 7, 2024
bcd230e
use built in url parser
duncanpharvey Oct 7, 2024
119da9f
remove serverless logic from web util
duncanpharvey Oct 7, 2024
8a81f3c
remove wait-on dependency
duncanpharvey Oct 7, 2024
3c004c5
remove find-process dependency
duncanpharvey Oct 8, 2024
a088120
Revert "remove find-process dependency"
duncanpharvey Oct 8, 2024
4a6c8a6
call func start directly and remove find-process dependency
duncanpharvey Oct 9, 2024
91a2dd9
simplify serverless util
duncanpharvey Oct 9, 2024
6967f7d
Revert "simplify serverless util"
duncanpharvey Oct 9, 2024
21c85af
simplify serverless util
duncanpharvey Oct 10, 2024
c3f9565
Merge branch 'master' into duncan-harvey/azure-functions-integration
duncanpharvey Oct 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/plugins/upstream

azure-functions:
runs-on: ubuntu-latest
env:
PLUGINS: azure-functions
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/plugins/test

bluebird:
runs-on: ubuntu-latest
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,4 @@ packages/dd-trace/test/appsec/next/*/package.json
packages/dd-trace/test/appsec/next/*/node_modules
packages/dd-trace/test/appsec/next/*/yarn.lock
!packages/dd-trace/**/telemetry/logs
packages/datadog-plugin-azure-functions/test/integration-test/fixtures/node_modules
rochdev marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tracer.use('pg', {
<h5 id="aws-sdk"></h5>
<h5 id="aws-sdk-tags"></h5>
<h5 id="aws-sdk-config"></h5>
<h5 id="azure-functions"></h5>
<h5 id="bunyan"></h5>
<h5 id="couchbase"></h5>
<h5 id="cucumber"></h5>
Expand Down Expand Up @@ -105,6 +106,7 @@ tracer.use('pg', {
* [amqplib](./interfaces/export_.plugins.amqplib.html)
* [avsc](./interfaces/export_.plugins.avsc.html)
* [aws-sdk](./interfaces/export_.plugins.aws_sdk.html)
* [azure-functions](./interfaces/export_.plugins.azure_functions.html)
* [bluebird](./interfaces/export_.plugins.bluebird.html)
* [couchbase](./interfaces/export_.plugins.couchbase.html)
* [cucumber](./interfaces/export_.plugins.cucumber.html)
Expand Down
1 change: 1 addition & 0 deletions docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ tracer.use('amqplib');
tracer.use('avsc');
tracer.use('aws-sdk');
tracer.use('aws-sdk', awsSdkOptions);
tracer.use('azure-functions');
tracer.use('bunyan');
tracer.use('couchbase');
tracer.use('cassandra-driver');
Expand Down
1 change: 1 addition & 0 deletions ext/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
declare const types: {
HTTP: 'http'
SERVERLESS: 'serverless'
WEB: 'web'
}

Expand Down
1 change: 1 addition & 0 deletions ext/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

module.exports = {
HTTP: 'http',
SERVERLESS: 'serverless',
WEB: 'web'
}
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ interface Plugins {
"apollo": tracer.plugins.apollo;
"avsc": tracer.plugins.avsc;
"aws-sdk": tracer.plugins.aws_sdk;
"azure-functions": tracer.plugins.azure_functions;
"bunyan": tracer.plugins.bunyan;
"cassandra-driver": tracer.plugins.cassandra_driver;
"child_process": tracer.plugins.child_process;
Expand Down Expand Up @@ -1237,6 +1238,12 @@ declare namespace tracer {
[key: string]: boolean | Object | undefined;
}

/**
* This plugin automatically instruments the
* @azure/functions module.
*/
interface azure_functions extends Instrumentation {}

/**
* This plugin patches the [bunyan](https://github.com/trentm/node-bunyan)
* to automatically inject trace identifiers in log records when the
Expand Down
1 change: 1 addition & 0 deletions integration-tests/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ function assertUUID (actual, msg = 'not a valid UUID') {

module.exports = {
FakeAgent,
hookFile,
assertObjectContains,
assertUUID,
spawnProc,
Expand Down
48 changes: 48 additions & 0 deletions packages/datadog-instrumentations/src/azure-functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

const {
addHook
} = require('./helpers/instrument')
const shimmer = require('../../datadog-shimmer')
const dc = require('dc-polyfill')

const azureFunctionsChannel = dc.tracingChannel('datadog:azure-functions:invoke')

addHook({ name: '@azure/functions', versions: ['>=4'] }, azureFunction => {
const { app } = azureFunction

shimmer.wrap(app, 'deleteRequest', wrapHandler)
shimmer.wrap(app, 'http', wrapHandler)
shimmer.wrap(app, 'get', wrapHandler)
shimmer.wrap(app, 'patch', wrapHandler)
shimmer.wrap(app, 'post', wrapHandler)
shimmer.wrap(app, 'put', wrapHandler)

return azureFunction
})

// The http methods are overloaded so we need to check which type of argument was passed in order to wrap the handler
// The arguments are either an object with a handler property or the handler function itself
function wrapHandler (method) {
return function (name, arg) {
if (typeof arg === 'object' && arg.hasOwnProperty('handler')) {
const options = arg
shimmer.wrap(options, 'handler', handler => traceHandler(handler, name, method.name))
} else if (typeof arg === 'function') {
const handler = arg
arguments[1] = shimmer.wrapFunction(handler, handler => traceHandler(handler, name, method.name))
}
return method.apply(this, arguments)
}
}

function traceHandler (handler, functionName, methodName) {
return function (...args) {
const httpRequest = args[0]
const invocationContext = args[1]
return azureFunctionsChannel.tracePromise(
handler,
{ functionName, httpRequest, invocationContext, methodName },
this, ...args)
}
}
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
'@apollo/gateway': () => require('../apollo'),
'apollo-server-core': () => require('../apollo-server-core'),
'@aws-sdk/smithy-client': () => require('../aws-sdk'),
'@azure/functions': () => require('../azure-functions'),
'@cucumber/cucumber': () => require('../cucumber'),
'@playwright/test': () => require('../playwright'),
'@elastic/elasticsearch': () => require('../elasticsearch'),
Expand Down
77 changes: 77 additions & 0 deletions packages/datadog-plugin-azure-functions/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict'

const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
const { storage } = require('../../datadog-core')
const serverless = require('../../dd-trace/src/plugins/util/serverless')
const web = require('../../dd-trace/src/plugins/util/web')

const triggerMap = {
deleteRequest: 'Http',
http: 'Http',
get: 'Http',
patch: 'Http',
post: 'Http',
put: 'Http'
}

class AzureFunctionsPlugin extends TracingPlugin {
static get id () { return 'azure-functions' }
static get operation () { return 'invoke' }
static get kind () { return 'server' }
static get type () { return 'serverless' }

static get prefix () { return 'tracing:datadog:azure-functions:invoke' }

bindStart (ctx) {
const { functionName, methodName } = ctx
const store = storage.getStore()

const span = this.startSpan(this.operationName(), {
service: this.serviceName(),
type: 'serverless',
meta: {
'aas.function.name': functionName,
'aas.function.trigger': mapTriggerTag(methodName)
}
}, false)

ctx.span = span
ctx.parentStore = store
ctx.currentStore = { ...store, span }

return ctx.currentStore
}

error (ctx) {
this.addError(ctx.error)
ctx.currentStore.span.setTag('error.message', ctx.error)
}

asyncEnd (ctx) {
const { httpRequest, result = {} } = ctx
const path = (new URL(httpRequest.url)).pathname
const req = {
method: httpRequest.method,
headers: Object.fromEntries(httpRequest.headers.entries()),
url: path
}

const context = web.patch(req)
context.config = this.config
context.paths = [path]
context.res = { statusCode: result.status }
context.span = ctx.currentStore.span

serverless.finishSpan(context)
}

configure (config) {
return super.configure(web.normalizeConfig(config))
}
}

function mapTriggerTag (methodName) {
return triggerMap[methodName] || 'Unknown'
}

module.exports = AzureFunctionsPlugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict'

const {
FakeAgent,
hookFile,
createSandbox,
curlAndAssertMessage
} = require('../../../../integration-tests/helpers')
const { spawn } = require('child_process')
const { assert } = require('chai')

describe('esm', () => {
let agent
let proc
let sandbox

withVersions('azure-functions', '@azure/functions', version => {
before(async function () {
this.timeout(50000)
sandbox = await createSandbox([`@azure/functions@${version}`, 'azure-functions-core-tools@4'], false,
['./packages/datadog-plugin-azure-functions/test/integration-test/fixtures/*'])
})

after(async function () {
this.timeout(50000)
await sandbox.remove()
})

beforeEach(async () => {
agent = await new FakeAgent().start()
})

afterEach(async () => {
proc && proc.kill('SIGINT')
await agent.stop()
})

it('is instrumented', async () => {
const envArgs = {
PATH: `${sandbox.folder}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`
}
proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'func', ['start'], agent.port, undefined, envArgs)

return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/httptest', ({ headers, payload }) => {
assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`)
assert.isArray(payload)
assert.strictEqual(payload.length, 1)
assert.isArray(payload[0])
assert.strictEqual(payload[0].length, 1)
assert.propertyVal(payload[0][0], 'name', 'azure-functions.invoke')
})
}).timeout(50000)
})
})

async function spawnPluginIntegrationTestProc (cwd, command, args, agentPort, stdioHandler, additionalEnvArgs = {}) {
let env = {
NODE_OPTIONS: `--loader=${hookFile}`,
DD_TRACE_AGENT_PORT: agentPort
}
env = { ...env, ...additionalEnvArgs }
return spawnProc(command, args, {
cwd,
env
}, stdioHandler)
}

function spawnProc (command, args, options = {}, stdioHandler, stderrHandler) {
const proc = spawn(command, args, { ...options, stdio: 'pipe' })
return new Promise((resolve, reject) => {
proc
.on('error', reject)
.on('exit', code => {
if (code !== 0) {
reject(new Error(`Process exited with status code ${code}.`))
}
resolve()
})

proc.stdout.on('data', data => {
if (stdioHandler) {
stdioHandler(data)
}
// eslint-disable-next-line no-console
if (!options.silent) console.log(data.toString())

if (data.toString().includes('http://localhost:7071/api/httptest')) {
resolve(proc)
}
})

proc.stderr.on('data', data => {
if (stderrHandler) {
stderrHandler(data)
}
// eslint-disable-next-line no-console
if (!options.silent) console.error(data.toString())
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"AzureWebJobsStorage": ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "azure-function-node-integration-test",
"version": "1.0.0",
"description": "",
"main": "src/functions/server.mjs",
"scripts": {
"start": "func start"
},
"dependencies": {
"@azure/functions": "^4.0.0"
},
"devDependencies": {
"azure-functions-core-tools": "^4.x"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'dd-trace/init.js'
import { app } from '@azure/functions'

async function handlerFunction (request, context) {
return {
status: 200,
body: 'Hello Datadog!'
}
}

app.http('httptest', {
methods: ['GET'],
authLevel: 'anonymous',
handler: handlerFunction
})
Loading
Loading