Skip to content

Commit

Permalink
Add Plugin for @azure/functions (#4716)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
duncanpharvey and rochdev authored Oct 10, 2024
1 parent ce0bdce commit 5a113b2
Show file tree
Hide file tree
Showing 24 changed files with 612 additions and 6 deletions.
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
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

0 comments on commit 5a113b2

Please sign in to comment.