Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions ddtrace/_trace/utils_botocore/span_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,55 @@
from ddtrace.ext import aws
from ddtrace.ext import http
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.serverless import in_aws_lambda
from ddtrace.internal.utils.formats import deep_getattr


_PAYLOAD_TAGGER = AWSPayloadTagging()


# Helper to build AWS hostname from service, region and parameters
def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, Any]] = None) -> Optional[str]:
"""Return hostname for given AWS service according to Datadog peer hostname rules.

Only returns hostnames for specific AWS services:
- eventbridge/events -> events.<region>.amazonaws.com
- sqs -> sqs.<region>.amazonaws.com
- sns -> sns.<region>.amazonaws.com
- kinesis -> kinesis.<region>.amazonaws.com
- dynamodb -> dynamodb.<region>.amazonaws.com
- s3 -> <bucket>.s3.<region>.amazonaws.com (if Bucket param present)
s3.<region>.amazonaws.com (otherwise)

Other services return ``None``.
"""

if not region:
return None

aws_service = service.lower()

# Only set peer.service for specific services
if aws_service in {"eventbridge", "events"}:
return f"events.{region}.amazonaws.com"
if aws_service == "sqs":
return f"sqs.{region}.amazonaws.com"
if aws_service == "sns":
return f"sns.{region}.amazonaws.com"
if aws_service == "kinesis":
return f"kinesis.{region}.amazonaws.com"
if aws_service in {"dynamodb", "dynamodbdocument"}:
return f"dynamodb.{region}.amazonaws.com"
if aws_service == "s3":
bucket = params.get("Bucket") if params else None
if bucket:
return f"{bucket}.s3.{region}.amazonaws.com"
return f"s3.{region}.amazonaws.com"

# Return None for all other services
return None


def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params, endpoint_name, operation):
span.set_tag_str(COMPONENT, config.botocore.integration_name)
# set span.kind to the type of request being performed
Expand Down Expand Up @@ -51,6 +94,13 @@ def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params,
span.set_tag_str("aws.region", region_name)
span.set_tag_str("region", region_name)

# Derive peer hostname only in serverless environments to avoid
# unnecessary tag noise in traditional hosts/containers.
if in_aws_lambda():
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
if hostname:
span.set_tag_str("peer.service", hostname)


def set_botocore_response_metadata_tags(
span: Span, result: Dict[str, Any], is_error_code_fn: Optional[Callable] = None
Expand Down
8 changes: 8 additions & 0 deletions ddtrace/contrib/internal/aiobotocore/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import wrapt

from ddtrace import config
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
from ddtrace.constants import _SPAN_MEASURED_KEY
from ddtrace.constants import SPAN_KIND
from ddtrace.contrib.internal.trace_utils import ext_service
Expand All @@ -16,6 +17,7 @@
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.schema import schematize_cloud_api_operation
from ddtrace.internal.schema import schematize_service_name
from ddtrace.internal.serverless import in_aws_lambda
from ddtrace.internal.utils import ArgumentError
from ddtrace.internal.utils import get_argument_value
from ddtrace.internal.utils.formats import asbool
Expand Down Expand Up @@ -145,6 +147,12 @@ async def _wrapped_api_call(original_func, instance, args, kwargs):

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

if in_aws_lambda():
# Derive the peer hostname now that we have both service and region.
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
if hostname:
span.set_tag_str("peer.service", hostname)

meta = {
"aws.agent": "aiobotocore",
"aws.operation": operation,
Expand Down
14 changes: 14 additions & 0 deletions ddtrace/contrib/internal/boto/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import wrapt

from ddtrace import config
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
from ddtrace.constants import _SPAN_MEASURED_KEY
from ddtrace.constants import SPAN_KIND
from ddtrace.ext import SpanKind
Expand All @@ -16,6 +17,7 @@
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.schema import schematize_cloud_api_operation
from ddtrace.internal.schema import schematize_service_name
from ddtrace.internal.serverless import in_aws_lambda
from ddtrace.internal.utils import get_argument_value
from ddtrace.internal.utils.formats import asbool
from ddtrace.internal.utils.wrappers import unwrap
Expand Down Expand Up @@ -122,6 +124,12 @@ def patched_query_request(original_func, instance, args, kwargs):
meta[aws.REGION] = region_name
meta[aws.AWSREGION] = region_name

if in_aws_lambda():
# Derive the peer hostname now that we have both service and region.
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
if hostname:
meta["peer.service"] = hostname

span.set_tags(meta)

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

if in_aws_lambda():
# Derive the peer hostname
hostname = _derive_peer_hostname(endpoint_name, region_name, None)
if hostname:
meta["peer.service"] = hostname

span.set_tags(meta)

# Original func returns a boto.connection.HTTPResponse object
Expand Down
5 changes: 3 additions & 2 deletions ddtrace/internal/schema/processor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ddtrace._trace.processor import TraceProcessor
from ddtrace.constants import _BASE_SERVICE_KEY
from ddtrace.internal.serverless import in_aws_lambda
from ddtrace.settings._config import config

from . import schematize_service_name
Expand All @@ -10,8 +11,8 @@ def __init__(self):
self._global_service = schematize_service_name((config.service or "").lower())

def process_trace(self, trace):
if not trace:
return
if not trace or in_aws_lambda():
return trace

traces_to_process = filter(
lambda x: x.service and x.service.lower() != self._global_service,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features:
- |
aws: set peer.service explictly and remove base_service in serverless
environments to improve the accuracy of serverless service representation.


99 changes: 99 additions & 0 deletions tests/contrib/aiobotocore/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,102 @@ async def test_response_context_manager(tracer):
assert span.name == "s3.command"
assert span.get_tag("component") == "aiobotocore"
assert span.get_tag("span.kind") == "client"


# Peer service tests
@pytest.mark.asyncio
async def test_sqs_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for SQS when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")

async with aiobotocore_client("sqs", tracer) as sqs:
await sqs.list_queues()

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to sqs hostname
assert span.get_tag("peer.service") == "sqs.us-west-2.amazonaws.com"


@pytest.mark.asyncio
async def test_s3_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for S3 when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")
bucket_name = f"{time.time()}bucket".replace(".", "")

async with aiobotocore_client("s3", tracer) as s3:
# Test with bucket parameter
await s3.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": "us-west-2"})

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to bucket-specific hostname
assert span.get_tag("peer.service") == f"{bucket_name}.s3.us-west-2.amazonaws.com"


@pytest.mark.asyncio
async def test_dynamodb_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for DynamoDB when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")

async with aiobotocore_client("dynamodb", tracer) as dynamodb:
await dynamodb.list_tables()

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to dynamodb hostname
assert span.get_tag("peer.service") == "dynamodb.us-west-2.amazonaws.com"


@pytest.mark.asyncio
async def test_kinesis_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for Kinesis when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")

async with aiobotocore_client("kinesis", tracer) as kinesis:
await kinesis.list_streams()

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to kinesis hostname
assert span.get_tag("peer.service") == "kinesis.us-west-2.amazonaws.com"


@pytest.mark.asyncio
async def test_sns_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for SNS when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")

async with aiobotocore_client("sns", tracer) as sns:
await sns.list_topics()

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to sns hostname
assert span.get_tag("peer.service") == "sns.us-west-2.amazonaws.com"


@pytest.mark.asyncio
async def test_eventbridge_client_peer_service_in_lambda(tracer, monkeypatch):
"""Test that peer.service tag is set for EventBridge when running in AWS Lambda"""
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-func")

async with aiobotocore_client("events", tracer) as events:
await events.list_rules()

traces = tracer.pop_traces()
assert len(traces) == 1
assert len(traces[0]) == 1
span = traces[0][0]
# Should have peer.service set to events hostname
assert span.get_tag("peer.service") == "events.us-west-2.amazonaws.com"
104 changes: 104 additions & 0 deletions tests/contrib/botocore/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4247,3 +4247,107 @@ def test_aws_payload_tagging_kinesis(self):
with self.tracer.trace("kinesis.manual_span"):
client.create_stream(StreamName=stream_name, ShardCount=1)
client.put_records(StreamName=stream_name, Records=data)

# Peer service tests
@mock_sqs
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_sqs_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for SQS when running in AWS Lambda"""
sqs = self.session.create_client("sqs", region_name="us-east-1")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(sqs)

sqs.list_queues()
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to sqs hostname
assert span.get_tag("peer.service") == "sqs.us-east-1.amazonaws.com"

@mock_s3
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_s3_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for S3 when running in AWS Lambda"""
s3 = self.session.create_client("s3", region_name="us-east-1")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(s3)

# Test with bucket parameter
s3.create_bucket(Bucket="test-bucket")
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to bucket-specific hostname
assert span.get_tag("peer.service") == "test-bucket.s3.us-east-1.amazonaws.com"

@mock_dynamodb
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_dynamodb_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for DynamoDB when running in AWS Lambda"""
dynamodb = self.session.create_client("dynamodb", region_name="us-west-2")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(dynamodb)

dynamodb.list_tables()
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to dynamodb hostname
assert span.get_tag("peer.service") == "dynamodb.us-west-2.amazonaws.com"

@mock_kinesis
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_kinesis_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for Kinesis when running in AWS Lambda"""
kinesis = self.session.create_client("kinesis", region_name="us-east-1")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(kinesis)

kinesis.list_streams()
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to kinesis hostname
assert span.get_tag("peer.service") == "kinesis.us-east-1.amazonaws.com"

@mock_sns
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_sns_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for SNS when running in AWS Lambda"""
sns = self.session.create_client("sns", region_name="us-west-2")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(sns)

sns.list_topics()
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to sns hostname
assert span.get_tag("peer.service") == "sns.us-west-2.amazonaws.com"

@mock_events
@TracerTestCase.run_in_subprocess(env_overrides=dict(AWS_LAMBDA_FUNCTION_NAME="my-func"))
def test_eventbridge_client_peer_service_in_lambda(self):
"""Test that peer.service tag is set for EventBridge when running in AWS Lambda"""
events = self.session.create_client("events", region_name="us-east-1")
pin = Pin(service=self.TEST_SERVICE)
pin._tracer = self.tracer
pin.onto(events)

events.list_rules()
spans = self.get_spans()
assert spans
assert len(spans) == 1
span = spans[0]
# Should have peer.service set to events hostname
assert span.get_tag("peer.service") == "events.us-east-1.amazonaws.com"
Loading