Skip to content

Commit ec08cdd

Browse files
authored
feat(apim): add inferred span support for Azure API Managment (#7496)
* initial * add apim to the inferrered proxy test * rename method * udpate method name in index.js * clean up some comments and remove serverless type * fix addRequestTags type * remove object.fromentries * remove unexpected import * re add object.fromentries * remove unwanted imports * fix bug found by codex * add test for bug caught by codex
1 parent aa68f29 commit ec08cdd

File tree

6 files changed

+336
-292
lines changed

6 files changed

+336
-292
lines changed

packages/datadog-plugin-azure-functions/src/index.js

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict'
22

33
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4-
const serverless = require('../../dd-trace/src/plugins/util/serverless')
54
const web = require('../../dd-trace/src/plugins/util/web')
65

76
const triggerMap = {
@@ -26,19 +25,51 @@ class AzureFunctionsPlugin extends TracingPlugin {
2625
bindStart (ctx) {
2726
const meta = getMetaForTrigger(ctx)
2827
const triggerType = triggerMap[ctx.methodName]
28+
const isHttpTrigger = triggerType === 'Http'
2929
const isMessagingService = (triggerType === 'ServiceBus' || triggerType === 'EventHubs')
30-
const childOf = isMessagingService ? null : extractTraceContext(this._tracer, ctx)
31-
const span = this.startSpan(this.operationName(), {
32-
childOf,
33-
service: this.serviceName(),
34-
type: 'serverless',
35-
meta,
36-
}, ctx)
37-
38-
if (isMessagingService) {
39-
setSpanLinks(triggerType, this.tracer, span, ctx)
40-
}
4130

31+
let span
32+
33+
if (isHttpTrigger) {
34+
const { httpRequest } = ctx
35+
const path = (new URL(httpRequest.url)).pathname
36+
const req = {
37+
method: httpRequest.method,
38+
headers: Object.fromEntries(httpRequest.headers),
39+
url: path,
40+
}
41+
// Patch the request to create web context
42+
const webContext = web.patch(req)
43+
webContext.config = this.config
44+
webContext.tracer = this.tracer
45+
webContext.paths = [path]
46+
// Creates a standard span and an inferred proxy span if headers are present
47+
span = web.startServerlessSpanWithInferredProxy(
48+
this.tracer,
49+
this.config,
50+
this.operationName(),
51+
req,
52+
ctx
53+
)
54+
55+
span._integrationName = 'azure-functions'
56+
span.context()._tags.component = 'azure-functions'
57+
span.addTags(meta)
58+
webContext.span = span
59+
webContext.azureFunctionCtx = ctx
60+
ctx.webContext = webContext
61+
} else {
62+
// For non-HTTP triggers, use standard flow
63+
span = this.startSpan(this.operationName(), {
64+
service: this.serviceName(),
65+
type: 'serverless',
66+
meta,
67+
}, ctx)
68+
69+
if (isMessagingService) {
70+
setSpanLinks(triggerType, this.tracer, span, ctx)
71+
}
72+
}
4273
ctx.span = span
4374
return ctx.currentStore
4475
}
@@ -48,24 +79,16 @@ class AzureFunctionsPlugin extends TracingPlugin {
4879
ctx.currentStore.span.setTag('error.message', ctx.error)
4980
}
5081

51-
asyncEnd (ctx) {
52-
const { httpRequest, methodName, result = {} } = ctx
53-
if (triggerMap[methodName] === 'Http') {
54-
// If the method is an HTTP trigger, we need to patch the request and finish the span
55-
const path = (new URL(httpRequest.url)).pathname
56-
const req = {
57-
method: httpRequest.method,
58-
headers: Object.fromEntries(httpRequest.headers),
59-
url: path,
82+
asyncStart (ctx) {
83+
const { methodName, result = {}, webContext } = ctx
84+
const triggerType = triggerMap[methodName]
85+
86+
// For HTTP triggers, use web utilities to finish all spans (including inferred proxy)
87+
if (triggerType === 'Http') {
88+
if (webContext) {
89+
webContext.res = { statusCode: result.status }
90+
web.finishAll(webContext, 'serverless')
6091
}
61-
const context = web.patch(req)
62-
context.config = this.config
63-
context.paths = [path]
64-
context.res = { statusCode: result.status }
65-
context.span = ctx.currentStore.span
66-
67-
serverless.finishSpan(context)
68-
// Fallback for other trigger types
6992
} else {
7093
super.finish()
7194
}
@@ -80,6 +103,7 @@ function getMetaForTrigger ({ functionName, methodName, invocationContext }) {
80103
let meta = {
81104
'aas.function.name': functionName,
82105
'aas.function.trigger': mapTriggerTag(methodName),
106+
'span.type': 'serverless',
83107
}
84108

85109
if (triggerMap[methodName] === 'ServiceBus') {
@@ -112,14 +136,6 @@ function mapTriggerTag (methodName) {
112136
return triggerMap[methodName] || 'Unknown'
113137
}
114138

115-
function extractTraceContext (tracer, ctx) {
116-
if (triggerMap[ctx.methodName] === 'Http') {
117-
return tracer.extract('http_headers', Object.fromEntries(ctx.httpRequest.headers))
118-
}
119-
// Returning null indicates that the span is a root span
120-
return null
121-
}
122-
123139
// message & messages & batch with cardinality of 1 == applicationProperties
124140
// messages with cardinality of many == applicationPropertiesArray
125141
function setSpanLinks (triggerType, tracer, span, ctx) {

packages/datadog-plugin-azure-functions/test/integration-test/http-test/client.spec.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ describe('esm', () => {
5050
assert.strictEqual(payload.length, 1)
5151
assert.ok(Array.isArray(payload[0]))
5252
assert.strictEqual(payload[0].length, 1)
53-
assert.strictEqual(payload[0][0].name, 'azure.functions.invoke')
53+
54+
const span = payload[0][0]
55+
56+
assert.strictEqual(span.name, 'azure.functions.invoke')
57+
assert.strictEqual(span.meta['_dd.integration'], 'azure-functions')
58+
assert.strictEqual(span.meta.component, 'azure-functions')
59+
assert.strictEqual(span.meta['http.route'], '/api/httptest')
60+
assert.strictEqual(span.resource, 'GET /api/httptest')
5461
})
5562
}).timeout(60_000)
5663

packages/dd-trace/src/plugins/util/inferred_proxy.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ const PROXY_HEADER_PATH = 'x-dd-proxy-path'
1414
const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod'
1515
const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name'
1616
const PROXY_HEADER_STAGE = 'x-dd-proxy-stage'
17+
const PROXY_HEADER_REGION = 'x-dd-proxy-region'
1718

1819
const supportedProxies = {
1920
'aws-apigateway': {
2021
spanName: 'aws.apigateway',
2122
component: 'aws-apigateway',
2223
},
24+
'azure-apim': {
25+
spanName: 'azure.apim',
26+
component: 'azure-apim',
27+
},
2328
}
2429

2530
function createInferredProxySpan (headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) {
@@ -53,6 +58,7 @@ function createInferredProxySpan (headers, childOf, tracer, reqCtx, traceCtx, co
5358
[HTTP_METHOD]: proxyContext.method,
5459
[HTTP_URL]: proxyContext.domainName + proxyContext.path,
5560
stage: proxyContext.stage,
61+
region: proxyContext.region,
5662
},
5763
}, traceCtx, config)
5864

@@ -91,6 +97,7 @@ function extractInferredProxyContext (headers) {
9197
stage: headers[PROXY_HEADER_STAGE],
9298
domainName: headers[PROXY_HEADER_DOMAIN],
9399
proxySystemName: headers[PROXY_HEADER_SYSTEM],
100+
region: headers[PROXY_HEADER_REGION],
94101
}
95102
}
96103

packages/dd-trace/src/plugins/util/serverless.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/dd-trace/src/plugins/util/web.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const web = {
119119
context.span.context()._name = name
120120
span = context.span
121121
} else {
122-
span = web.startChildSpan(tracer, config, name, req, traceCtx)
122+
span = web.startServerlessSpanWithInferredProxy(tracer, config, name, req, traceCtx)
123123
}
124124

125125
context.tracer = tracer
@@ -275,7 +275,7 @@ const web = {
275275
return context.middleware.at(-1)
276276
},
277277

278-
startChildSpan (tracer, config, name, req, traceCtx) {
278+
startServerlessSpanWithInferredProxy (tracer, config, name, req, traceCtx) {
279279
const headers = req.headers
280280
const reqCtx = contexts.get(req)
281281
const { storage } = require('../../../../datadog-core')
@@ -338,12 +338,12 @@ const web = {
338338
}
339339
},
340340

341-
finishSpan (context) {
341+
finishSpan (context, spanType) {
342342
const { req, res } = context
343343

344344
if (context.finished && !req.stream) return
345345

346-
addRequestTags(context, this.TYPE)
346+
addRequestTags(context, spanType)
347347
addResponseTags(context)
348348

349349
context.config.hooks.request(context.span, req, res)
@@ -353,14 +353,14 @@ const web = {
353353
context.finished = true
354354
},
355355

356-
finishAll (context) {
356+
finishAll (context, spanType) {
357357
for (const beforeEnd of context.beforeEnd) {
358358
beforeEnd()
359359
}
360360

361361
web.finishMiddleware(context)
362362

363-
web.finishSpan(context)
363+
web.finishSpan(context, spanType)
364364

365365
finishInferredProxySpan(context)
366366
},
@@ -457,12 +457,13 @@ function reactivate (req, fn) {
457457
function addRequestTags (context, spanType) {
458458
const { req, span, inferredProxySpan, config } = context
459459
const url = extractURL(req)
460+
const type = spanType ?? WEB
460461

461462
span.addTags({
462463
[HTTP_URL]: obfuscateQs(config, url),
463464
[HTTP_METHOD]: req.method,
464465
[SPAN_KIND]: SERVER,
465-
[SPAN_TYPE]: spanType,
466+
[SPAN_TYPE]: type,
466467
[HTTP_USERAGENT]: req.headers['user-agent'],
467468
})
468469

0 commit comments

Comments
 (0)