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 13 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 @@ -196,6 +196,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 LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dev,eslint-plugin-mocha,MIT,Copyright 2014 Mathias Schreck
dev,eslint-plugin-n,MIT,Copyright 2015 Toru Nagashima
dev,eslint-plugin-promise,ISC,jden and other contributors
dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson
dev,find-process,MIT,Copyright (c) 2016 Zoujie
dev,get-port,MIT,Copyright Sindre Sorhus
dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors
dev,graphql,MIT,Copyright 2015 Facebook Inc.
Expand All @@ -65,6 +66,7 @@ dev,sinon,BSD-3-Clause,Copyright 2010-2017 Christian Johansen
dev,sinon-chai,WTFPL and BSD-2-Clause,Copyright 2004 Sam Hocevar 2012–2017 Domenic Denicola
dev,tap,ISC,Copyright 2011-2022 Isaac Z. Schlueter and Contributors
dev,tiktoken,MIT,Copyright (c) 2022 OpenAI, Shantanu Jain
dev,wait-on,MIT,Copyright (c) 2015 Jeff Barczewski
file,aws-lambda-nodejs-runtime-interface-client,Apache 2.0,Copyright 2019 Amazon.com Inc. or its affiliates. All Rights Reserved.
file,profile.proto,Apache license 2.0,Copyright 2016 Google Inc.
file,is-git-url,MIT,Copyright (c) 2017 Jon Schlinkert.
2 changes: 2 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 @@ -102,6 +103,7 @@ tracer.use('pg', {
* [amqp10](./interfaces/export_.plugins.amqp10.html)
* [amqplib](./interfaces/export_.plugins.amqplib.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 @@ -283,6 +283,7 @@ tracer.use('amqp10');
tracer.use('amqplib');
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
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ interface Plugins {
"amqplib": tracer.plugins.amqplib;
"apollo": tracer.plugins.apollo;
"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 @@ -1229,6 +1230,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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.4.0",
"express": "^4.18.2",
"find-process": "^1.4.7",
"get-port": "^3.2.0",
"glob": "^7.1.6",
"graphql": "0.13.2",
Expand All @@ -143,6 +144,7 @@
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0",
"tap": "^16.3.7",
"tiktoken": "^1.0.15"
"tiktoken": "^1.0.15",
"wait-on": "^8.0.1"
}
}
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
84 changes: 84 additions & 0 deletions packages/datadog-plugin-azure-functions/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict'

const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
const { storage } = require('../../datadog-core')
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 = extractPath(httpRequest.url)
const req = {
method: httpRequest.method,
headers: Object.fromEntries(httpRequest.headers.entries()),
url: path
}

const context = web.patch(req)
context.span = ctx.currentStore.span
context.config = this.config
context.paths = [path]

// Use status for status code if available. Otherwise if no status is provided assume an internal server error
context.res = { statusCode: result.hasOwnProperty('status') ? result.status : 500 }

web.finishSpan(context)
}

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

function extractPath (url) {
const regex = /https?:\/\/[^/]+(\/.*$)/
const match = url.match(regex)
return match ? match[1] : ''
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using the built-in URL parsers instead of rolling one ourselves:

extractPath('http://www.google.com/asdf?asdf')
// '/asdf?asdf'
url.parse('http://www.google.com/asdf?asdf').path // if search query is needed
// '/asdf?asdf'
(new URL('http://www.google.com/asdf?asdf')).pathname // if search query isn't needed
// '/asdf'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!


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

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

const {
FakeAgent,
hookFile,
createSandbox,
curlAndAssertMessage
} = require('../../../../integration-tests/helpers')
const { spawn } = require('child_process')
const { assert } = require('chai')
const findProcess = require('find-process')
const waitOn = require('wait-on')

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 () => {
const azureFuncProc = await findProcess('name', 'func', true)
const azureFuncProcPid = azureFuncProc[0]?.pid ?? null
azureFuncProcPid !== null && process.kill(azureFuncProcPid, 'SIGKILL')

proc && proc.kill()
await agent.stop()
})

it('is instrumented', async () => {
const envArgs = {
PATH: `${sandbox.folder}/node_modules/.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) => {
waitOn({
resources: ['http-get://127.0.0.1:7071'],
timeout: 5000
}).then(() => {
resolve(proc)
}).catch(err => {
reject(new Error(`Error while waiting for process to start: ${err.message}`))
})

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())
})

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": ""
}
}
Loading
Loading