Skip to content

Commit 29ff735

Browse files
wconti27wantsui
andauthored
feat(tracing): AWS API Gateway Inferred Span Support (#4837)
* Add support for inferred spans to be created for proxies. Initially supports AWS API Gateway and creates a span when the required headers are attached on the received request. --------- Co-authored-by: wantsui <wan.tsui@datadoghq.com>
1 parent b81d9d8 commit 29ff735

File tree

4 files changed

+423
-10
lines changed

4 files changed

+423
-10
lines changed

packages/dd-trace/src/config.js

+4
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ class Config {
513513
this._setValue(defaults, 'isTestDynamicInstrumentationEnabled', false)
514514
this._setValue(defaults, 'logInjection', false)
515515
this._setValue(defaults, 'lookup', undefined)
516+
this._setValue(defaults, 'inferredProxyServicesEnabled', false)
516517
this._setValue(defaults, 'memcachedCommandEnabled', false)
517518
this._setValue(defaults, 'openAiLogsEnabled', false)
518519
this._setValue(defaults, 'openaiSpanCharLimit', 128)
@@ -675,6 +676,7 @@ class Config {
675676
DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH,
676677
DD_TRACING_ENABLED,
677678
DD_VERSION,
679+
DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED,
678680
OTEL_METRICS_EXPORTER,
679681
OTEL_PROPAGATORS,
680682
OTEL_RESOURCE_ATTRIBUTES,
@@ -862,6 +864,7 @@ class Config {
862864
: !!OTEL_PROPAGATORS)
863865
this._setBoolean(env, 'tracing', DD_TRACING_ENABLED)
864866
this._setString(env, 'version', DD_VERSION || tags.version)
867+
this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED)
865868
}
866869

867870
_applyOptions (options) {
@@ -980,6 +983,7 @@ class Config {
980983
this._setBoolean(opts, 'traceId128BitGenerationEnabled', options.traceId128BitGenerationEnabled)
981984
this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled)
982985
this._setString(opts, 'version', options.version || tags.version)
986+
this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled)
983987

984988
// For LLMObs, we want the environment variable to take precedence over the options.
985989
// This is reliant on environment config being set before options.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const log = require('../../log')
2+
const tags = require('../../../../../ext/tags')
3+
4+
const RESOURCE_NAME = tags.RESOURCE_NAME
5+
const HTTP_ROUTE = tags.HTTP_ROUTE
6+
const SPAN_KIND = tags.SPAN_KIND
7+
const SPAN_TYPE = tags.SPAN_TYPE
8+
const HTTP_URL = tags.HTTP_URL
9+
const HTTP_METHOD = tags.HTTP_METHOD
10+
11+
const PROXY_HEADER_SYSTEM = 'x-dd-proxy'
12+
const PROXY_HEADER_START_TIME_MS = 'x-dd-proxy-request-time-ms'
13+
const PROXY_HEADER_PATH = 'x-dd-proxy-path'
14+
const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod'
15+
const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name'
16+
const PROXY_HEADER_STAGE = 'x-dd-proxy-stage'
17+
18+
const supportedProxies = {
19+
'aws-apigateway': {
20+
spanName: 'aws.apigateway',
21+
component: 'aws-apigateway'
22+
}
23+
}
24+
25+
function createInferredProxySpan (headers, childOf, tracer, context) {
26+
if (!headers) {
27+
return null
28+
}
29+
30+
if (!tracer._config?.inferredProxyServicesEnabled) {
31+
return null
32+
}
33+
34+
const proxyContext = extractInferredProxyContext(headers)
35+
36+
if (!proxyContext) {
37+
return null
38+
}
39+
40+
const proxySpanInfo = supportedProxies[proxyContext.proxySystemName]
41+
42+
log.debug(`Successfully extracted inferred span info ${proxyContext} for proxy: ${proxyContext.proxySystemName}`)
43+
44+
const span = tracer.startSpan(
45+
proxySpanInfo.spanName,
46+
{
47+
childOf,
48+
type: 'web',
49+
startTime: proxyContext.requestTime,
50+
tags: {
51+
service: proxyContext.domainName || tracer._config.service,
52+
component: proxySpanInfo.component,
53+
[SPAN_KIND]: 'internal',
54+
[SPAN_TYPE]: 'web',
55+
[HTTP_METHOD]: proxyContext.method,
56+
[HTTP_URL]: proxyContext.domainName + proxyContext.path,
57+
[HTTP_ROUTE]: proxyContext.path,
58+
stage: proxyContext.stage
59+
}
60+
}
61+
)
62+
63+
tracer.scope().activate(span)
64+
context.inferredProxySpan = span
65+
childOf = span
66+
67+
log.debug('Successfully created inferred proxy span.')
68+
69+
setInferredProxySpanTags(span, proxyContext)
70+
71+
return childOf
72+
}
73+
74+
function setInferredProxySpanTags (span, proxyContext) {
75+
span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`)
76+
span.setTag('_dd.inferred_span', '1')
77+
return span
78+
}
79+
80+
function extractInferredProxyContext (headers) {
81+
if (!(PROXY_HEADER_START_TIME_MS in headers)) {
82+
return null
83+
}
84+
85+
if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in supportedProxies)) {
86+
log.debug(`Received headers to create inferred proxy span but headers include an unsupported proxy type ${headers}`)
87+
return null
88+
}
89+
90+
return {
91+
requestTime: headers[PROXY_HEADER_START_TIME_MS]
92+
? parseInt(headers[PROXY_HEADER_START_TIME_MS], 10)
93+
: null,
94+
method: headers[PROXY_HEADER_HTTPMETHOD],
95+
path: headers[PROXY_HEADER_PATH],
96+
stage: headers[PROXY_HEADER_STAGE],
97+
domainName: headers[PROXY_HEADER_DOMAIN],
98+
proxySystemName: headers[PROXY_HEADER_SYSTEM]
99+
}
100+
}
101+
102+
function finishInferredProxySpan (context) {
103+
const { req } = context
104+
105+
if (!context.inferredProxySpan) return
106+
107+
if (context.inferredProxySpanFinished && !req.stream) return
108+
109+
// context.config.hooks.request(context.inferredProxySpan, req, res) # TODO: Do we need this??
110+
111+
// Only close the inferred span if one was created
112+
if (context.inferredProxySpan) {
113+
context.inferredProxySpan.finish()
114+
context.inferredProxySpanFinished = true
115+
}
116+
}
117+
118+
module.exports = {
119+
createInferredProxySpan,
120+
finishInferredProxySpan
121+
}

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

+38-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const kinds = require('../../../../../ext/kinds')
1010
const urlFilter = require('./urlfilter')
1111
const { extractIp } = require('./ip_extractor')
1212
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../constants')
13+
const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy')
1314

1415
const WEB = types.WEB
1516
const SERVER = kinds.SERVER
@@ -97,7 +98,7 @@ const web = {
9798
context.span.context()._name = name
9899
span = context.span
99100
} else {
100-
span = web.startChildSpan(tracer, name, req.headers)
101+
span = web.startChildSpan(tracer, name, req)
101102
}
102103

103104
context.tracer = tracer
@@ -253,8 +254,19 @@ const web = {
253254
},
254255

255256
// Extract the parent span from the headers and start a new span as its child
256-
startChildSpan (tracer, name, headers) {
257-
const childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers)
257+
startChildSpan (tracer, name, req) {
258+
const headers = req.headers
259+
const context = contexts.get(req)
260+
let childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers)
261+
262+
// we may have headers signaling a router proxy span should be created (such as for AWS API Gateway)
263+
if (tracer._config?.inferredProxyServicesEnabled) {
264+
const proxySpan = createInferredProxySpan(headers, childOf, tracer, context)
265+
if (proxySpan) {
266+
childOf = proxySpan
267+
}
268+
}
269+
258270
const span = tracer.startSpan(name, { childOf })
259271

260272
return span
@@ -263,13 +275,21 @@ const web = {
263275
// Validate a request's status code and then add error tags if necessary
264276
addStatusError (req, statusCode) {
265277
const context = contexts.get(req)
266-
const span = context.span
267-
const error = context.error
268-
const hasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE]
278+
const { span, inferredProxySpan, error } = context
279+
280+
const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE]
281+
const inferredSpanContext = inferredProxySpan?.context()
282+
const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE]
269283

270-
if (!hasExistingError && !context.config.validateStatus(statusCode)) {
284+
const isValidStatusCode = context.config.validateStatus(statusCode)
285+
286+
if (!spanHasExistingError && !isValidStatusCode) {
271287
span.setTag(ERROR, error || true)
272288
}
289+
290+
if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) {
291+
inferredProxySpan.setTag(ERROR, error || true)
292+
}
273293
},
274294

275295
// Add an error to the request
@@ -316,6 +336,8 @@ const web = {
316336
web.finishMiddleware(context)
317337

318338
web.finishSpan(context)
339+
340+
finishInferredProxySpan(context)
319341
},
320342

321343
obfuscateQs (config, url) {
@@ -426,7 +448,7 @@ function reactivate (req, fn) {
426448
}
427449

428450
function addRequestTags (context, spanType) {
429-
const { req, span, config } = context
451+
const { req, span, inferredProxySpan, config } = context
430452
const url = extractURL(req)
431453

432454
span.addTags({
@@ -443,14 +465,15 @@ function addRequestTags (context, spanType) {
443465

444466
if (clientIp) {
445467
span.setTag(HTTP_CLIENT_IP, clientIp)
468+
inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp)
446469
}
447470
}
448471

449472
addHeaders(context)
450473
}
451474

452475
function addResponseTags (context) {
453-
const { req, res, paths, span } = context
476+
const { req, res, paths, span, inferredProxySpan } = context
454477

455478
if (paths.length > 0) {
456479
span.setTag(HTTP_ROUTE, paths.join(''))
@@ -459,6 +482,9 @@ function addResponseTags (context) {
459482
span.addTags({
460483
[HTTP_STATUS_CODE]: res.statusCode
461484
})
485+
inferredProxySpan?.addTags({
486+
[HTTP_STATUS_CODE]: res.statusCode
487+
})
462488

463489
web.addStatusError(req, res.statusCode)
464490
}
@@ -477,18 +503,20 @@ function addResourceTag (context) {
477503
}
478504

479505
function addHeaders (context) {
480-
const { req, res, config, span } = context
506+
const { req, res, config, span, inferredProxySpan } = context
481507

482508
config.headers.forEach(([key, tag]) => {
483509
const reqHeader = req.headers[key]
484510
const resHeader = res.getHeader(key)
485511

486512
if (reqHeader) {
487513
span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader)
514+
inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader)
488515
}
489516

490517
if (resHeader) {
491518
span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader)
519+
inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader)
492520
}
493521
})
494522
}

0 commit comments

Comments
 (0)