Skip to content

Commit 3bf17b0

Browse files
authored
feat: Support Azure functions background trigger types (#3028)
1 parent 1349cae commit 3bf17b0

File tree

5 files changed

+400
-100
lines changed

5 files changed

+400
-100
lines changed

lib/instrumentation/@azure/functions.js

Lines changed: 175 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77

88
const defaultLogger = require('../../logger').child({ component: 'azure-functions' })
99
const urltils = require('../../util/urltils')
10-
const recordWeb = require('../../metrics/recorders/http')
1110
const headerProcessing = require('../../header-processing')
1211
const synthetics = require('../../synthetics')
1312

14-
const DESTS = require('../../config/attribute-filter').DESTINATIONS
13+
const backgroundRecorder = require('../../metrics/recorders/other.js')
14+
const recordWeb = require('../../metrics/recorders/http')
15+
16+
const {
17+
DESTINATIONS: DESTS,
18+
TYPES
19+
} = require('../../transaction/index.js')
1520

1621
const {
1722
WEBSITE_OWNER_NAME,
@@ -23,8 +28,12 @@ const RESOURCE_GROUP_NAME = WEBSITE_RESOURCE_GROUP ?? WEBSITE_OWNER_NAME?.split(
2328
const AZURE_FUNCTION_APP_NAME = WEBSITE_SITE_NAME
2429

2530
let coldStart = true
31+
let _agent
32+
let _logger
2633

2734
module.exports = function initialize(agent, azureFunctions, _moduleName, shim, { logger = defaultLogger } = {}) {
35+
_agent = agent
36+
_logger = logger
2837
if (!SUBSCRIPTION_ID || !RESOURCE_GROUP_NAME || !AZURE_FUNCTION_APP_NAME) {
2938
logger.warn(
3039
{
@@ -38,98 +47,163 @@ module.exports = function initialize(agent, azureFunctions, _moduleName, shim, {
3847
return
3948
}
4049

41-
const methods = ['http', 'get', 'put', 'post', 'patch', 'deleteRequest']
42-
shim.wrap(azureFunctions.app, methods, function wrapAzureHttpMethods(shim, appMethod) {
43-
return async function wrappedAzureHttpMethod(...args) {
44-
// If the app doesn't need an options object, the user can pass the
45-
// handler function as the second argument
46-
// (e.g. `app.get('name', handler)`).
47-
// See https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#registering-a-function
48-
let handler
49-
if (typeof args[1] === 'function') {
50-
handler = args[1]
51-
args[1] = { handler }
50+
const httpMethods = ['http', 'get', 'put', 'post', 'patch', 'deleteRequest']
51+
shim.wrap(azureFunctions.app, httpMethods, wrapAzureHttpMethods)
52+
53+
const backgroundMethods = [
54+
'cosmosDB',
55+
'eventGrid',
56+
'eventHub',
57+
'mySql',
58+
'serviceBusQueue',
59+
'serviceBusTopic',
60+
'sql',
61+
'storageBlob',
62+
'storageQueue',
63+
'timer',
64+
'warmup',
65+
'webPubSub'
66+
]
67+
shim.wrap(azureFunctions.app, backgroundMethods, wrapAzureBackgroundMethods)
68+
}
69+
70+
function wrapAzureHttpMethods(shim, appMethod) {
71+
return async function wrappedAzureHttpMethod(...args) {
72+
// If the app doesn't need an options object, the user can pass the
73+
// handler function as the second argument
74+
// (e.g. `app.get('name', handler)`).
75+
// See https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#registering-a-function
76+
let handler
77+
if (typeof args[1] === 'function') {
78+
handler = args[1]
79+
args[1] = { handler }
80+
} else {
81+
handler = args[1].handler
82+
}
83+
84+
const tracer = shim.tracer
85+
args[1].handler = tracer.transactionProxy(async function wrappedHandler(...args) {
86+
const [request, context] = args
87+
const ctx = tracer.getContext()
88+
const tx = tracer.getTransaction()
89+
90+
// Set the transaction name according to our spec (category + function name).
91+
tx.setPartialName(`AzureFunction/${context.functionName}`)
92+
93+
const url = new URL(request.url)
94+
const segment = tracer.createSegment({
95+
name: request.url,
96+
recorder: recordWeb,
97+
parent: ctx.segment,
98+
transaction: tx
99+
})
100+
segment.start()
101+
102+
const transport = url.protocol === 'https:' ? 'HTTPS' : 'HTTP'
103+
tx.type = TYPES.WEB
104+
tx.baseSegment = segment
105+
tx.parsedUrl = url
106+
tx.url = urltils.obfuscatePath(_agent.config, url.pathname)
107+
tx.verb = request.method
108+
if (url.port === '') {
109+
tx.port = transport === 'HTTPS' ? '443' : '80'
52110
} else {
53-
handler = args[1].handler
111+
tx.port = url.port
112+
}
113+
114+
tx.trace.attributes.addAttribute(
115+
DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
116+
'request.uri',
117+
tx.url
118+
)
119+
segment.addSpanAttribute('request.uri', tx.url)
120+
if (request.method != null) {
121+
segment.addSpanAttribute('request.method', request.method)
54122
}
123+
addAttributes({ transaction: tx, functionContext: context })
55124

56-
const tracer = shim.tracer
57-
args[1].handler = tracer.transactionProxy(async function wrappedHandler(...args) {
58-
const [request, context] = args
59-
const ctx = tracer.getContext()
60-
const tx = tracer.getTransaction()
61-
62-
// Set the transaction name according to our spec (category + function name).
63-
tx.setPartialName(`AzureFunction/${context.functionName}`)
64-
65-
const url = new URL(request.url)
66-
const segment = tracer.createSegment({
67-
name: request.url,
68-
recorder: recordWeb,
69-
parent: ctx.segment,
70-
transaction: tx
71-
})
72-
segment.start()
73-
74-
const transport = url.protocol === 'https:' ? 'HTTPS' : 'HTTP'
75-
tx.type = 'web'
76-
tx.baseSegment = segment
77-
tx.parsedUrl = url
78-
tx.url = urltils.obfuscatePath(agent.config, url.pathname)
79-
tx.verb = request.method
80-
if (url.port === '') {
81-
tx.port = transport === 'HTTPS' ? '443' : '80'
82-
} else {
83-
tx.port = url.port
84-
}
125+
const queueTimeStamp = headerProcessing.getQueueTime(_logger, request.headers)
126+
if (queueTimeStamp) {
127+
tx.queueTime = Date.now() - queueTimeStamp
128+
}
85129

130+
synthetics.assignHeadersToTransaction(_agent.config, tx, request.headers)
131+
if (_agent.config.distributed_tracing.enabled === true) {
132+
tx.acceptDistributedTraceHeaders(transport, request.headers)
133+
}
134+
135+
const newContext = ctx.enterSegment({ segment })
136+
const boundHandler = tracer.bindFunction(handler, newContext)
137+
138+
const result = await boundHandler(...args)
139+
// Responses should have a shape as described at:
140+
// https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response
141+
if (result.status) {
86142
tx.trace.attributes.addAttribute(
87-
DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
88-
'request.uri',
89-
tx.url
143+
DESTS.TRANS_COMMON,
144+
'http.statusCode',
145+
result.status
90146
)
91-
segment.addSpanAttribute('request.uri', tx.url)
92-
if (request.method != null) {
93-
segment.addSpanAttribute('request.method', request.method)
94-
}
95-
addAttributes({ transaction: tx, functionContext: context })
96-
97-
const queueTimeStamp = headerProcessing.getQueueTime(logger, request.headers)
98-
if (queueTimeStamp) {
99-
tx.queueTime = Date.now() - queueTimeStamp
100-
}
101-
102-
synthetics.assignHeadersToTransaction(agent.config, tx, request.headers)
103-
if (agent.config.distributed_tracing.enabled === true) {
104-
tx.acceptDistributedTraceHeaders(transport, request.headers)
105-
}
106-
107-
const newContext = ctx.enterSegment({ segment })
108-
const boundHandler = tracer.bindFunction(handler, newContext)
109-
110-
const result = await boundHandler(...args)
111-
// Responses should have a shape as described at:
112-
// https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response
113-
if (result.status) {
114-
tx.trace.attributes.addAttribute(
115-
DESTS.TRANS_COMMON,
116-
'http.statusCode',
117-
result.status
118-
)
119-
}
120-
121-
if (coldStart === true) {
122-
tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
123-
coldStart = false
124-
}
125-
126-
tx.end()
127-
return result
128-
})
147+
}
148+
149+
if (coldStart === true) {
150+
tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
151+
coldStart = false
152+
}
153+
154+
tx.end()
155+
return result
156+
})
157+
158+
await appMethod(...args)
159+
}
160+
}
129161

130-
await appMethod(...args)
162+
function wrapAzureBackgroundMethods(shim, appMethod) {
163+
return async function wrappedAzureBackgroundMethod(...args) {
164+
let handler
165+
if (typeof args[1] === 'function') {
166+
handler = args[1]
167+
args[1] = { handler }
168+
} else {
169+
handler = args[1].handler
131170
}
132-
})
171+
172+
const tracer = shim.tracer
173+
args[1].handler = tracer.transactionProxy(async function wrappedHandler(...args) {
174+
const [, context] = args
175+
const ctx = tracer.getContext()
176+
const tx = tracer.getTransaction()
177+
178+
tx.setPartialName(`AzureFunction/${context.functionName}`)
179+
180+
const segment = tracer.createSegment({
181+
name: `${appMethod}-trigger`,
182+
recorder: backgroundRecorder,
183+
parent: ctx.segment,
184+
transaction: tx
185+
})
186+
segment.start()
187+
188+
tx.type = TYPES.BG
189+
tx.baseSegment = segment
190+
addAttributes({ transaction: tx, functionContext: context })
191+
192+
const newContext = ctx.enterSegment({ segment })
193+
const boundHandler = tracer.bindFunction(handler, newContext)
194+
195+
const result = await boundHandler(...args)
196+
if (coldStart === true) {
197+
tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
198+
coldStart = false
199+
}
200+
201+
tx.end()
202+
return result
203+
})
204+
205+
await appMethod(...args)
206+
}
133207
}
134208

135209
/**
@@ -178,6 +252,7 @@ function mapTriggerType({ functionContext }) {
178252

179253
// Input types are found at:
180254
// https://github.com/Azure/azure-functions-nodejs-library/blob/138c021/src/trigger.ts
255+
// https://learn.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings?tabs=isolated-process%2Cnode-v4%2Cpython-v2&pivots=programming-language-javascript#supported-bindings
181256
switch (input) {
182257
case 'httpTrigger': {
183258
return 'http'
@@ -187,16 +262,25 @@ function mapTriggerType({ functionContext }) {
187262
return 'timer'
188263
}
189264

265+
case 'blobTrigger':
190266
case 'cosmosDBTrigger':
191-
case 'sqlTrigger':
192-
case 'mysqlTrigger': {
267+
case 'daprBindingTrigger':
268+
case 'mysqlTrigger':
269+
case 'queueTrigger':
270+
case 'sqlTrigger': {
193271
return 'datasource'
194272
}
195273

196-
case 'queueTrigger':
197-
case 'serviceBusTrigger':
198-
case 'eventHubTrigger':
274+
case 'daprTopicTrigger':
199275
case 'eventGridTrigger':
276+
case 'eventHubTrigger':
277+
case 'kafkaTrigger':
278+
case 'rabbitMQTrigger':
279+
case 'redisListTrigger':
280+
case 'redisPubSubTrigger':
281+
case 'redisStreamTrigger':
282+
case 'serviceBusTrigger':
283+
case 'signalRTrigger':
200284
case 'webPubSubTrigger': {
201285
return 'pubsub'
202286
}

test/unit/instrumentation/@azure/functions.test.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,27 @@ test('mapTriggerType maps recognized keys', t => {
102102
bootstrapModule({ t })
103103
const { mapTriggerType } = t.nr.initialize.internals
104104
const testData = [
105-
['httpTrigger', 'http'],
106-
['timerTrigger', 'timer'],
105+
['blobTrigger', 'datasource'],
107106
['cosmosDBTrigger', 'datasource'],
108-
['sqlTrigger', 'datasource'],
107+
['daprBindingTrigger', 'datasource'],
108+
['daprServiceInvocationTrigger', 'other'],
109+
['daprTopicTrigger', 'pubsub'],
110+
['eventGridTrigger', 'pubsub'],
111+
['eventHubTrigger', 'pubsub'],
112+
['httpTrigger', 'http'],
113+
['kafkaTrigger', 'pubsub'],
109114
['mysqlTrigger', 'datasource'],
110-
['queueTrigger', 'pubsub'],
115+
['not-recognized', 'other'],
116+
['queueTrigger', 'datasource'],
117+
['rabbitMQTrigger', 'pubsub'],
118+
['redisListTrigger', 'pubsub'],
119+
['redisPubSubTrigger', 'pubsub'],
120+
['redisStreamTrigger', 'pubsub'],
111121
['serviceBusTrigger', 'pubsub'],
112-
['eventHubTrigger', 'pubsub'],
113-
['eventGridTrigger', 'pubsub'],
122+
['signalRTrigger', 'pubsub'],
123+
['sqlTrigger', 'datasource'],
124+
['timerTrigger', 'timer'],
114125
['webPubSubTrigger', 'pubsub'],
115-
['not-recognized', 'other']
116126
]
117127

118128
for (const [input, expected] of testData) {

0 commit comments

Comments
 (0)