Skip to content

Commit d6be505

Browse files
authored
feat(aws): enhance service representation for serverless (#14055)
### What does this PR do? <!-- A brief description of the change being made with this pull request. --> Rollout of span naming changes to align tracer with serverless product to create streamlined Service Representation for Serverless Key Changes: - Apply all changes ONLY in a Serverless scenario - Apply explicit `peer.service` tag equal to the hostname based on aws service type and region grouping all spans underneath one explicitly defined inferred service - Extract hostname within the region event for accuracy - Remove `_dd.base_service` for serverless spans since the config is derived incorrectly in those situations ### Motivation <!-- What inspired you to submit this pull request? --> Improve Service Map for Serverless ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 162d587 commit d6be505

File tree

7 files changed

+286
-2
lines changed

7 files changed

+286
-2
lines changed

ddtrace/_trace/utils_botocore/span_tags.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,52 @@
1212
from ddtrace.ext import aws
1313
from ddtrace.ext import http
1414
from ddtrace.internal.constants import COMPONENT
15+
from ddtrace.internal.serverless import in_aws_lambda
1516
from ddtrace.internal.utils.formats import deep_getattr
1617

1718

1819
_PAYLOAD_TAGGER = AWSPayloadTagging()
1920

21+
SERVICE_MAP = {
22+
"eventbridge": "events",
23+
"events": "events",
24+
"sqs": "sqs",
25+
"sns": "sns",
26+
"kinesis": "kinesis",
27+
"dynamodb": "dynamodb",
28+
"dynamodbdocument": "dynamodb",
29+
}
30+
31+
32+
# Helper to build AWS hostname from service, region and parameters
33+
def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, Any]] = None) -> Optional[str]:
34+
"""Return hostname for given AWS service according to Datadog peer hostname rules.
35+
36+
Only returns hostnames for specific AWS services:
37+
- eventbridge/events -> events.<region>.amazonaws.com
38+
- sqs -> sqs.<region>.amazonaws.com
39+
- sns -> sns.<region>.amazonaws.com
40+
- kinesis -> kinesis.<region>.amazonaws.com
41+
- dynamodb -> dynamodb.<region>.amazonaws.com
42+
- s3 -> <bucket>.s3.<region>.amazonaws.com (if Bucket param present)
43+
s3.<region>.amazonaws.com (otherwise)
44+
45+
Other services return ``None``.
46+
"""
47+
48+
if not region:
49+
return None
50+
51+
aws_service = service.lower()
52+
53+
if aws_service == "s3":
54+
bucket = params.get("Bucket") if params else None
55+
return f"{bucket}.s3.{region}.amazonaws.com" if bucket else f"s3.{region}.amazonaws.com"
56+
57+
mapped = SERVICE_MAP.get(aws_service)
58+
59+
return f"{mapped}.{region}.amazonaws.com" if mapped else None
60+
2061

2162
def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params, endpoint_name, operation):
2263
span.set_tag_str(COMPONENT, config.botocore.integration_name)
@@ -51,6 +92,13 @@ def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params,
5192
span.set_tag_str("aws.region", region_name)
5293
span.set_tag_str("region", region_name)
5394

95+
# Derive peer hostname only in serverless environments to avoid
96+
# unnecessary tag noise in traditional hosts/containers.
97+
if in_aws_lambda():
98+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
99+
if hostname:
100+
span.set_tag_str("peer.service", hostname)
101+
54102

55103
def set_botocore_response_metadata_tags(
56104
span: Span, result: Dict[str, Any], is_error_code_fn: Optional[Callable] = None

ddtrace/contrib/internal/aiobotocore/patch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import wrapt
66

77
from ddtrace import config
8+
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
89
from ddtrace.constants import _SPAN_MEASURED_KEY
910
from ddtrace.constants import SPAN_KIND
1011
from ddtrace.contrib.internal.trace_utils import ext_service
@@ -16,6 +17,7 @@
1617
from ddtrace.internal.constants import COMPONENT
1718
from ddtrace.internal.schema import schematize_cloud_api_operation
1819
from ddtrace.internal.schema import schematize_service_name
20+
from ddtrace.internal.serverless import in_aws_lambda
1921
from ddtrace.internal.utils import ArgumentError
2022
from ddtrace.internal.utils import get_argument_value
2123
from ddtrace.internal.utils.formats import asbool
@@ -145,6 +147,12 @@ async def _wrapped_api_call(original_func, instance, args, kwargs):
145147

146148
region_name = deep_getattr(instance, "meta.region_name")
147149

150+
if in_aws_lambda():
151+
# Derive the peer hostname now that we have both service and region.
152+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
153+
if hostname:
154+
span.set_tag_str("peer.service", hostname)
155+
148156
meta = {
149157
"aws.agent": "aiobotocore",
150158
"aws.operation": operation,

ddtrace/contrib/internal/boto/patch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import wrapt
88

99
from ddtrace import config
10+
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
1011
from ddtrace.constants import _SPAN_MEASURED_KEY
1112
from ddtrace.constants import SPAN_KIND
1213
from ddtrace.ext import SpanKind
@@ -16,6 +17,7 @@
1617
from ddtrace.internal.constants import COMPONENT
1718
from ddtrace.internal.schema import schematize_cloud_api_operation
1819
from ddtrace.internal.schema import schematize_service_name
20+
from ddtrace.internal.serverless import in_aws_lambda
1921
from ddtrace.internal.utils import get_argument_value
2022
from ddtrace.internal.utils.formats import asbool
2123
from ddtrace.internal.utils.wrappers import unwrap
@@ -122,6 +124,12 @@ def patched_query_request(original_func, instance, args, kwargs):
122124
meta[aws.REGION] = region_name
123125
meta[aws.AWSREGION] = region_name
124126

127+
if in_aws_lambda():
128+
# Derive the peer hostname now that we have both service and region.
129+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
130+
if hostname:
131+
meta["peer.service"] = hostname
132+
125133
span.set_tags(meta)
126134

127135
# Original func returns a boto.connection.HTTPResponse object
@@ -183,6 +191,12 @@ def patched_auth_request(original_func, instance, args, kwargs):
183191
meta[aws.REGION] = region_name
184192
meta[aws.AWSREGION] = region_name
185193

194+
if in_aws_lambda():
195+
# Derive the peer hostname
196+
hostname = _derive_peer_hostname(endpoint_name, region_name, None)
197+
if hostname:
198+
meta["peer.service"] = hostname
199+
186200
span.set_tags(meta)
187201

188202
# Original func returns a boto.connection.HTTPResponse object

ddtrace/internal/schema/processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ddtrace._trace.processor import TraceProcessor
22
from ddtrace.constants import _BASE_SERVICE_KEY
3+
from ddtrace.internal.serverless import in_aws_lambda
34
from ddtrace.settings._config import config
45

56
from . import schematize_service_name
@@ -8,10 +9,13 @@
89
class BaseServiceProcessor(TraceProcessor):
910
def __init__(self):
1011
self._global_service = schematize_service_name((config.service or "").lower())
12+
self._in_aws_lambda = in_aws_lambda()
1113

1214
def process_trace(self, trace):
13-
if not trace:
14-
return
15+
# AWS Lambda spans receive unhelpful base_service value of runtime
16+
# Remove base_service to prevent service overrides in Lambda spans
17+
if not trace or self._in_aws_lambda:
18+
return trace
1519

1620
traces_to_process = filter(
1721
lambda x: x.service and x.service.lower() != self._global_service,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features:
3+
- |
4+
aws: Set peer.service explictly to improve the accuracy of serverless
5+
service representation. Base_service defaults to unhelpful value "runtime"
6+
in serverless spans. Remove base_service to prevent unwanted service
7+
overrides in Lambda spans.

tests/contrib/aiobotocore/test.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,102 @@ async def test_response_context_manager(tracer):
553553
assert span.name == "s3.command"
554554
assert span.get_tag("component") == "aiobotocore"
555555
assert span.get_tag("span.kind") == "client"
556+
557+
558+
# Peer service tests
559+
@pytest.mark.asyncio
560+
async def test_sqs_client_peer_service_in_lambda(tracer, monkeypatch):
561+
"""Test that peer.service tag is set for SQS when running in AWS Lambda"""
562+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
563+
564+
async with aiobotocore_client("sqs", tracer) as sqs:
565+
await sqs.list_queues()
566+
567+
traces = tracer.pop_traces()
568+
assert len(traces) == 1
569+
assert len(traces[0]) == 1
570+
span = traces[0][0]
571+
# Should have peer.service set to sqs hostname
572+
assert span.get_tag("peer.service") == "sqs.us-west-2.amazonaws.com"
573+
574+
575+
@pytest.mark.asyncio
576+
async def test_s3_client_peer_service_in_lambda(tracer, monkeypatch):
577+
"""Test that peer.service tag is set for S3 when running in AWS Lambda"""
578+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
579+
bucket_name = f"{time.time()}bucket".replace(".", "")
580+
581+
async with aiobotocore_client("s3", tracer) as s3:
582+
# Test with bucket parameter
583+
await s3.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": "us-west-2"})
584+
585+
traces = tracer.pop_traces()
586+
assert len(traces) == 1
587+
assert len(traces[0]) == 1
588+
span = traces[0][0]
589+
# Should have peer.service set to bucket-specific hostname
590+
assert span.get_tag("peer.service") == f"{bucket_name}.s3.us-west-2.amazonaws.com"
591+
592+
593+
@pytest.mark.asyncio
594+
async def test_dynamodb_client_peer_service_in_lambda(tracer, monkeypatch):
595+
"""Test that peer.service tag is set for DynamoDB when running in AWS Lambda"""
596+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
597+
598+
async with aiobotocore_client("dynamodb", tracer) as dynamodb:
599+
await dynamodb.list_tables()
600+
601+
traces = tracer.pop_traces()
602+
assert len(traces) == 1
603+
assert len(traces[0]) == 1
604+
span = traces[0][0]
605+
# Should have peer.service set to dynamodb hostname
606+
assert span.get_tag("peer.service") == "dynamodb.us-west-2.amazonaws.com"
607+
608+
609+
@pytest.mark.asyncio
610+
async def test_kinesis_client_peer_service_in_lambda(tracer, monkeypatch):
611+
"""Test that peer.service tag is set for Kinesis when running in AWS Lambda"""
612+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
613+
614+
async with aiobotocore_client("kinesis", tracer) as kinesis:
615+
await kinesis.list_streams()
616+
617+
traces = tracer.pop_traces()
618+
assert len(traces) == 1
619+
assert len(traces[0]) == 1
620+
span = traces[0][0]
621+
# Should have peer.service set to kinesis hostname
622+
assert span.get_tag("peer.service") == "kinesis.us-west-2.amazonaws.com"
623+
624+
625+
@pytest.mark.asyncio
626+
async def test_sns_client_peer_service_in_lambda(tracer, monkeypatch):
627+
"""Test that peer.service tag is set for SNS when running in AWS Lambda"""
628+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
629+
630+
async with aiobotocore_client("sns", tracer) as sns:
631+
await sns.list_topics()
632+
633+
traces = tracer.pop_traces()
634+
assert len(traces) == 1
635+
assert len(traces[0]) == 1
636+
span = traces[0][0]
637+
# Should have peer.service set to sns hostname
638+
assert span.get_tag("peer.service") == "sns.us-west-2.amazonaws.com"
639+
640+
641+
@pytest.mark.asyncio
642+
async def test_eventbridge_client_peer_service_in_lambda(tracer, monkeypatch):
643+
"""Test that peer.service tag is set for EventBridge when running in AWS Lambda"""
644+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
645+
646+
async with aiobotocore_client("events", tracer) as events:
647+
await events.list_rules()
648+
649+
traces = tracer.pop_traces()
650+
assert len(traces) == 1
651+
assert len(traces[0]) == 1
652+
span = traces[0][0]
653+
# Should have peer.service set to events hostname
654+
assert span.get_tag("peer.service") == "events.us-west-2.amazonaws.com"

tests/contrib/botocore/test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4247,3 +4247,107 @@ def test_aws_payload_tagging_kinesis(self):
42474247
with self.tracer.trace("kinesis.manual_span"):
42484248
client.create_stream(StreamName=stream_name, ShardCount=1)
42494249
client.put_records(StreamName=stream_name, Records=data)
4250+
4251+
# Peer service tests
4252+
@mock_sqs
4253+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4254+
def test_sqs_client_peer_service_in_lambda(self):
4255+
"""Test that peer.service tag is set for SQS when running in AWS Lambda"""
4256+
sqs = self.session.create_client("sqs", region_name="us-east-1")
4257+
pin = Pin(service=self.TEST_SERVICE)
4258+
pin._tracer = self.tracer
4259+
pin.onto(sqs)
4260+
4261+
sqs.list_queues()
4262+
spans = self.get_spans()
4263+
assert spans
4264+
assert len(spans) == 1
4265+
span = spans[0]
4266+
# Should have peer.service set to sqs hostname
4267+
assert span.get_tag("peer.service") == "sqs.us-east-1.amazonaws.com"
4268+
4269+
@mock_s3
4270+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4271+
def test_s3_client_peer_service_in_lambda(self):
4272+
"""Test that peer.service tag is set for S3 when running in AWS Lambda"""
4273+
s3 = self.session.create_client("s3", region_name="us-east-1")
4274+
pin = Pin(service=self.TEST_SERVICE)
4275+
pin._tracer = self.tracer
4276+
pin.onto(s3)
4277+
4278+
# Test with bucket parameter
4279+
s3.create_bucket(Bucket="test-bucket")
4280+
spans = self.get_spans()
4281+
assert spans
4282+
assert len(spans) == 1
4283+
span = spans[0]
4284+
# Should have peer.service set to bucket-specific hostname
4285+
assert span.get_tag("peer.service") == "test-bucket.s3.us-east-1.amazonaws.com"
4286+
4287+
@mock_dynamodb
4288+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4289+
def test_dynamodb_client_peer_service_in_lambda(self):
4290+
"""Test that peer.service tag is set for DynamoDB when running in AWS Lambda"""
4291+
dynamodb = self.session.create_client("dynamodb", region_name="us-west-2")
4292+
pin = Pin(service=self.TEST_SERVICE)
4293+
pin._tracer = self.tracer
4294+
pin.onto(dynamodb)
4295+
4296+
dynamodb.list_tables()
4297+
spans = self.get_spans()
4298+
assert spans
4299+
assert len(spans) == 1
4300+
span = spans[0]
4301+
# Should have peer.service set to dynamodb hostname
4302+
assert span.get_tag("peer.service") == "dynamodb.us-west-2.amazonaws.com"
4303+
4304+
@mock_kinesis
4305+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4306+
def test_kinesis_client_peer_service_in_lambda(self):
4307+
"""Test that peer.service tag is set for Kinesis when running in AWS Lambda"""
4308+
kinesis = self.session.create_client("kinesis", region_name="us-east-1")
4309+
pin = Pin(service=self.TEST_SERVICE)
4310+
pin._tracer = self.tracer
4311+
pin.onto(kinesis)
4312+
4313+
kinesis.list_streams()
4314+
spans = self.get_spans()
4315+
assert spans
4316+
assert len(spans) == 1
4317+
span = spans[0]
4318+
# Should have peer.service set to kinesis hostname
4319+
assert span.get_tag("peer.service") == "kinesis.us-east-1.amazonaws.com"
4320+
4321+
@mock_sns
4322+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4323+
def test_sns_client_peer_service_in_lambda(self):
4324+
"""Test that peer.service tag is set for SNS when running in AWS Lambda"""
4325+
sns = self.session.create_client("sns", region_name="us-west-2")
4326+
pin = Pin(service=self.TEST_SERVICE)
4327+
pin._tracer = self.tracer
4328+
pin.onto(sns)
4329+
4330+
sns.list_topics()
4331+
spans = self.get_spans()
4332+
assert spans
4333+
assert len(spans) == 1
4334+
span = spans[0]
4335+
# Should have peer.service set to sns hostname
4336+
assert span.get_tag("peer.service") == "sns.us-west-2.amazonaws.com"
4337+
4338+
@mock_events
4339+
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
4340+
def test_eventbridge_client_peer_service_in_lambda(self):
4341+
"""Test that peer.service tag is set for EventBridge when running in AWS Lambda"""
4342+
events = self.session.create_client("events", region_name="us-east-1")
4343+
pin = Pin(service=self.TEST_SERVICE)
4344+
pin._tracer = self.tracer
4345+
pin.onto(events)
4346+
4347+
events.list_rules()
4348+
spans = self.get_spans()
4349+
assert spans
4350+
assert len(spans) == 1
4351+
span = spans[0]
4352+
# Should have peer.service set to events hostname
4353+
assert span.get_tag("peer.service") == "events.us-east-1.amazonaws.com"

0 commit comments

Comments
 (0)