Skip to content

Commit f6e8a28

Browse files
committed
feat(tracer): update tags for proxy inferred spans
1 parent 01850f2 commit f6e8a28

File tree

20 files changed

+199
-53
lines changed

20 files changed

+199
-53
lines changed

ddtrace/_trace/_inferred_proxy.py

Lines changed: 118 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from dataclasses import dataclass
12
import logging
3+
from typing import Callable
24
from typing import Dict
3-
from typing import Union
5+
from typing import Optional
46

57
from ddtrace import config
68
from ddtrace._trace.span import Span
9+
from ddtrace.ext import SpanKind
710
from ddtrace.ext import SpanTypes
811
from ddtrace.ext import http
912
from ddtrace.internal.constants import COMPONENT
@@ -13,18 +16,61 @@
1316

1417
log = logging.getLogger(__name__)
1518

19+
20+
@dataclass
21+
class ProxyHeaderContext:
22+
system_name: str
23+
request_time: str
24+
method: Optional[str]
25+
path: Optional[str]
26+
resource_path: Optional[str]
27+
domain_name: Optional[str]
28+
stage: Optional[str]
29+
account_id: Optional[str]
30+
api_id: Optional[str]
31+
region: Optional[str]
32+
user: Optional[str]
33+
34+
35+
@dataclass
36+
class ProxyInfo:
37+
span_name: str
38+
component: str
39+
resource_arn_builder: Optional[Callable[[ProxyHeaderContext], Optional[str]]] = None
40+
41+
42+
def _api_gateway_rest_api_arn(proxy_context: ProxyHeaderContext) -> Optional[str]:
43+
if proxy_context.region and proxy_context.api_id:
44+
return f"arn:aws:apigateway:{proxy_context.region}::/restapis/{proxy_context.api_id}"
45+
return None
46+
47+
48+
def _api_gateway_http_api_arn(proxy_context: ProxyHeaderContext) -> Optional[str]:
49+
if proxy_context.region and proxy_context.api_id:
50+
return f"arn:aws:apigateway:{proxy_context.region}::/apis/{proxy_context.api_id}"
51+
return None
52+
53+
54+
supported_proxies: Dict[str, ProxyInfo] = {
55+
"aws-apigateway": ProxyInfo("aws.apigateway", "aws-apigateway", _api_gateway_rest_api_arn),
56+
"aws-httpapi": ProxyInfo("aws.httpapi", "aws-httpapi", _api_gateway_http_api_arn),
57+
}
58+
59+
SUPPORTED_PROXY_SPAN_NAMES = {info.span_name for info in supported_proxies.values()}
60+
1661
# Checking lower case and upper case versions per WSGI spec following ddtrace/propagation/http.py's
1762
# logic to extract http headers
1863
POSSIBLE_PROXY_HEADER_SYSTEM = _possible_header("x-dd-proxy")
1964
POSSIBLE_PROXY_HEADER_START_TIME_MS = _possible_header("x-dd-proxy-request-time-ms")
2065
POSSIBLE_PROXY_HEADER_PATH = _possible_header("x-dd-proxy-path")
66+
POSSIBLE_PROXY_HEADER_RESOURCE_PATH = _possible_header("x-dd-proxy-resource-path")
2167
POSSIBLE_PROXY_HEADER_HTTPMETHOD = _possible_header("x-dd-proxy-httpmethod")
2268
POSSIBLE_PROXY_HEADER_DOMAIN = _possible_header("x-dd-proxy-domain-name")
2369
POSSIBLE_PROXY_HEADER_STAGE = _possible_header("x-dd-proxy-stage")
24-
25-
supported_proxies: Dict[str, Dict[str, str]] = {
26-
"aws-apigateway": {"span_name": "aws.apigateway", "component": "aws-apigateway"}
27-
}
70+
POSSIBLE_PROXY_HEADER_ACCOUNT_ID = _possible_header("x-dd-proxy-account-id")
71+
POSSIBLE_PROXY_HEADER_API_ID = _possible_header("x-dd-proxy-api-id")
72+
POSSIBLE_PROXY_HEADER_REGION = _possible_header("x-dd-proxy-region")
73+
POSSIBLE_PROXY_HEADER_USER = _possible_header("x-dd-proxy-user")
2874

2975

3076
def create_inferred_proxy_span_if_headers_exist(ctx, headers, child_of, tracer) -> None:
@@ -38,19 +84,23 @@ def create_inferred_proxy_span_if_headers_exist(ctx, headers, child_of, tracer)
3884
if not proxy_context:
3985
return None
4086

41-
proxy_span_info = supported_proxies[proxy_context["proxy_system_name"]]
87+
proxy_info = supported_proxies[proxy_context.system_name]
88+
89+
method = proxy_context.method
90+
route_or_path = proxy_context.resource_path or proxy_context.path
91+
resource = f"{method or ''} {route_or_path or ''}"
4292

4393
span = tracer.start_span(
44-
proxy_span_info["span_name"],
45-
service=proxy_context.get("domain_name", config._get_service()),
46-
resource=proxy_context["method"] + " " + proxy_context["path"],
94+
proxy_info.span_name,
95+
service=proxy_context.domain_name or config._get_service(),
96+
resource=resource,
4797
span_type=SpanTypes.WEB,
4898
activate=True,
4999
child_of=child_of,
50100
)
51-
span.start_ns = int(proxy_context["request_time"]) * 1000000
101+
span.start_ns = int(proxy_context.request_time) * 1000000
52102

53-
set_inferred_proxy_span_tags(span, proxy_context)
103+
set_inferred_proxy_span_tags(span, proxy_context, proxy_info)
54104

55105
# we need a callback to finish the api gateway span, this callback will be added to the child spans finish callbacks
56106
def finish_callback(_):
@@ -62,24 +112,56 @@ def finish_callback(_):
62112
ctx.set_item("headers", headers)
63113

64114

65-
def set_inferred_proxy_span_tags(span, proxy_context) -> Span:
66-
span._set_tag_str(COMPONENT, supported_proxies[proxy_context["proxy_system_name"]]["component"])
115+
def set_inferred_proxy_span_tags(span: Span, proxy_context: ProxyHeaderContext, proxy_info: ProxyInfo) -> Span:
116+
span._set_tag_str(COMPONENT, proxy_info.component)
117+
span._set_tag_str("span.kind", SpanKind.SERVER)
118+
119+
span._set_tag_str(http.URL, f"https://{proxy_context.domain_name or ''}{proxy_context.path or ''}")
120+
121+
if proxy_context.method:
122+
span._set_tag_str(http.METHOD, proxy_context.method)
123+
124+
if proxy_context.resource_path:
125+
span._set_tag_str(http.ROUTE, proxy_context.resource_path)
126+
127+
if proxy_context.stage:
128+
span._set_tag_str("stage", proxy_context.stage)
129+
130+
if proxy_context.account_id:
131+
span._set_tag_str("account_id", proxy_context.account_id)
67132

68-
span._set_tag_str(http.METHOD, proxy_context["method"])
69-
span._set_tag_str(http.URL, f"{proxy_context['domain_name']}{proxy_context['path']}")
70-
span._set_tag_str("stage", proxy_context["stage"])
133+
if proxy_context.api_id:
134+
span._set_tag_str("apiid", proxy_context.api_id)
135+
136+
if proxy_context.region:
137+
span._set_tag_str("region", proxy_context.region)
138+
139+
if proxy_context.user:
140+
span._set_tag_str("user", proxy_context.user)
141+
142+
if proxy_info.resource_arn_builder:
143+
resource_arn = proxy_info.resource_arn_builder(proxy_context)
144+
if resource_arn:
145+
span._set_tag_str("dd_resource_key", resource_arn)
71146

72147
span.set_metric("_dd.inferred_span", 1)
73148
return span
74149

75150

76-
def extract_inferred_proxy_context(headers) -> Union[None, Dict[str, str]]:
77-
proxy_header_system = str(_extract_header_value(POSSIBLE_PROXY_HEADER_SYSTEM, headers))
78-
proxy_header_start_time_ms = str(_extract_header_value(POSSIBLE_PROXY_HEADER_START_TIME_MS, headers))
79-
proxy_header_path = str(_extract_header_value(POSSIBLE_PROXY_HEADER_PATH, headers))
80-
proxy_header_httpmethod = str(_extract_header_value(POSSIBLE_PROXY_HEADER_HTTPMETHOD, headers))
81-
proxy_header_domain = str(_extract_header_value(POSSIBLE_PROXY_HEADER_DOMAIN, headers))
82-
proxy_header_stage = str(_extract_header_value(POSSIBLE_PROXY_HEADER_STAGE, headers))
151+
def extract_inferred_proxy_context(headers) -> Optional[ProxyHeaderContext]:
152+
proxy_header_system = _extract_header_value(POSSIBLE_PROXY_HEADER_SYSTEM, headers)
153+
proxy_header_start_time_ms = _extract_header_value(POSSIBLE_PROXY_HEADER_START_TIME_MS, headers)
154+
proxy_header_path = _extract_header_value(POSSIBLE_PROXY_HEADER_PATH, headers)
155+
proxy_header_resource_path = _extract_header_value(POSSIBLE_PROXY_HEADER_RESOURCE_PATH, headers)
156+
157+
proxy_header_httpmethod = _extract_header_value(POSSIBLE_PROXY_HEADER_HTTPMETHOD, headers)
158+
proxy_header_domain = _extract_header_value(POSSIBLE_PROXY_HEADER_DOMAIN, headers)
159+
proxy_header_stage = _extract_header_value(POSSIBLE_PROXY_HEADER_STAGE, headers)
160+
161+
proxy_header_account_id = _extract_header_value(POSSIBLE_PROXY_HEADER_ACCOUNT_ID, headers)
162+
proxy_header_api_id = _extract_header_value(POSSIBLE_PROXY_HEADER_API_ID, headers)
163+
proxy_header_region = _extract_header_value(POSSIBLE_PROXY_HEADER_REGION, headers)
164+
proxy_header_user = _extract_header_value(POSSIBLE_PROXY_HEADER_USER, headers)
83165

84166
# Exit if start time header is not present
85167
if proxy_header_start_time_ms is None:
@@ -92,14 +174,19 @@ def extract_inferred_proxy_context(headers) -> Union[None, Dict[str, str]]:
92174
)
93175
return None
94176

95-
return {
96-
"request_time": proxy_header_start_time_ms,
97-
"method": proxy_header_httpmethod,
98-
"path": proxy_header_path,
99-
"stage": proxy_header_stage,
100-
"domain_name": proxy_header_domain,
101-
"proxy_system_name": proxy_header_system,
102-
}
177+
return ProxyHeaderContext(
178+
proxy_header_system,
179+
proxy_header_start_time_ms,
180+
proxy_header_httpmethod,
181+
proxy_header_path,
182+
proxy_header_resource_path,
183+
proxy_header_domain,
184+
proxy_header_stage,
185+
proxy_header_account_id,
186+
proxy_header_api_id,
187+
proxy_header_region,
188+
proxy_header_user,
189+
)
103190

104191

105192
def normalize_headers(headers) -> Dict[str, str]:

ddtrace/_trace/trace_handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import ddtrace
1616
from ddtrace import config
17+
from ddtrace._trace._inferred_proxy import SUPPORTED_PROXY_SPAN_NAMES
1718
from ddtrace._trace._inferred_proxy import create_inferred_proxy_span_if_headers_exist
1819
from ddtrace._trace._span_link import SpanLinkKind as _SpanLinkKind
1920
from ddtrace._trace._span_pointer import _SpanPointerDescription
@@ -244,7 +245,7 @@ def _on_web_framework_finish_request(
244245

245246

246247
def _set_inferred_proxy_tags(span, status_code):
247-
if span._parent and span._parent.name == "aws.apigateway":
248+
if span._parent and span._parent.name in SUPPORTED_PROXY_SPAN_NAMES:
248249
inferred_span = span._parent
249250
status_code = status_code if status_code else span.get_tag("http.status_code")
250251
if status_code:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
tracing: Ensures inferred proxy spans for AWS API Gateway HTTP APIs are created when the
5+
``x-dd-proxy`` header reports ``aws-httpapi``.
6+
AAP: Update inferred proxy span tags to ensure that inferred services are discovered by the App and API Protection API Catalog.

tests/contrib/aiohttp/test_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ async def test_inferred_spans_api_gateway(app_tracer, aiohttp_client, test_app,
619619
api_gateway_resource="GET /",
620620
method="GET",
621621
status_code=str(test_app["status_code"]),
622-
url="local/",
622+
url="https://local/",
623623
start=1736973768,
624624
is_distributed=test_headers["type"] == "distributed",
625625
distributed_trace_id=1,

tests/contrib/asgi/test_asgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ async def test_inferred_spans_api_gateway_default(scope, tracer, test_spans, app
808808
api_gateway_resource="GET /",
809809
method="GET",
810810
status_code=app_type["status_code"],
811-
url="local/",
811+
url="https://local/",
812812
start=1736973768,
813813
is_distributed=headers == distributed_headers,
814814
distributed_trace_id=1,

tests/contrib/bottle/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ def handled_error_endpoint():
558558
api_gateway_resource="GET /",
559559
method="GET",
560560
status_code=str(test_endpoint["status"]),
561-
url="local/",
561+
url="https://local/",
562562
start=1736973768,
563563
is_distributed=False,
564564
distributed_trace_id=1,

tests/contrib/bottle/test_distributed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def default_endpoint():
169169
api_gateway_resource="GET /",
170170
method="GET",
171171
status_code=200,
172-
url="local/",
172+
url="https://local/",
173173
start=1736973768,
174174
is_distributed=True,
175175
distributed_trace_id=1,

tests/contrib/cherrypy/test_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def test_inferred_spans_api_gateway_default(self):
542542
api_gateway_resource="GET /",
543543
method="GET",
544544
status_code=test_endpoint["status"],
545-
url="local/",
545+
url="https://local/",
546546
start=1736973768,
547547
is_distributed=test_headers == distributed_headers,
548548
distributed_trace_id=1,

tests/contrib/django/test_django.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,7 +1952,7 @@ def test_inferred_spans_api_gateway_default(client, test_spans):
19521952
api_gateway_resource="GET /",
19531953
method="GET",
19541954
status_code="200",
1955-
url="local/",
1955+
url="https://local/",
19561956
start=1736973768.0,
19571957
)
19581958

@@ -1975,7 +1975,7 @@ def test_inferred_spans_api_gateway_default(client, test_spans):
19751975
api_gateway_resource="GET /",
19761976
method="GET",
19771977
status_code="500",
1978-
url="local/",
1978+
url="https://local/",
19791979
start=1736973768.0,
19801980
)
19811981

@@ -2035,7 +2035,7 @@ def test_inferred_spans_api_gateway_distributed_tracing(client, test_spans):
20352035
api_gateway_resource="GET /",
20362036
method="GET",
20372037
status_code="200",
2038-
url="local/",
2038+
url="https://local/",
20392039
start=1736973768.0,
20402040
is_distributed=True,
20412041
distributed_trace_id=1,

tests/contrib/djangorestframework/test_djangorestframework.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_inferred_spans_api_gateway_default(client, test_spans, test_endpoint, i
107107
api_gateway_resource="GET /",
108108
method="GET",
109109
status_code=test_endpoint["status_code"],
110-
url="local/",
110+
url="https://local/",
111111
start=1736973768,
112112
is_distributed=headers == distributed_headers,
113113
distributed_trace_id=1,

0 commit comments

Comments
 (0)