From b70cdcab67c3d8f9fc4d1959c56da9e24d03cb27 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 17 Jul 2025 18:10:04 -0400 Subject: [PATCH 01/14] add peer.hostname tags --- ddtrace/contrib/internal/botocore/patch.py | 11 +++++ .../internal/botocore/services/kinesis.py | 11 +++++ .../contrib/internal/botocore/services/sqs.py | 11 +++++ ddtrace/contrib/internal/botocore/utils.py | 42 +++++++++++++++++++ ddtrace/internal/schema/processor.py | 18 +++++++- 5 files changed, 91 insertions(+), 2 deletions(-) diff --git a/ddtrace/contrib/internal/botocore/patch.py b/ddtrace/contrib/internal/botocore/patch.py index 327feb028fc..ddcec028fce 100644 --- a/ddtrace/contrib/internal/botocore/patch.py +++ b/ddtrace/contrib/internal/botocore/patch.py @@ -24,6 +24,8 @@ from ddtrace.ext import SpanTypes from ddtrace.internal import core from ddtrace.internal.constants import COMPONENT +from ddtrace.ext import net # type: ignore[attr-defined] +from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_api_operation from ddtrace.internal.schema import schematize_cloud_faas_operation @@ -272,6 +274,15 @@ def patched_api_call_fallback(original_func, instance, args, kwargs, function_va span_type=SpanTypes.HTTP, span_key="instrumented_api_call", ) as ctx, ctx.span: + # Set peer.hostname if applicable + try: + region = getattr(instance, "meta", None) + region_name = getattr(region, "region_name", None) if region else None + host = compute_peer_hostname(endpoint_name, region_name, params) + if host: + ctx.span.set_tag_str(net.PEER_HOSTNAME, host) + except Exception: + log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_api_call.started", [ctx]) if args and config.botocore["distributed_tracing"]: prep_context_injection(ctx, endpoint_name, operation, trace_operation, params) diff --git a/ddtrace/contrib/internal/botocore/services/kinesis.py b/ddtrace/contrib/internal/botocore/services/kinesis.py index f1f71d0b819..ce3de4cb3f3 100644 --- a/ddtrace/contrib/internal/botocore/services/kinesis.py +++ b/ddtrace/contrib/internal/botocore/services/kinesis.py @@ -13,6 +13,8 @@ from ddtrace.contrib.internal.trace_utils import ext_service from ddtrace.ext import SpanTypes from ddtrace.internal import core +from ddtrace.ext import net # type: ignore[attr-defined] +from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_messaging_operation from ddtrace.internal.schema import schematize_service_name @@ -154,6 +156,15 @@ def _patched_kinesis_api_call(parent_ctx, original_func, instance, args, kwargs, func_run=is_getrecords_call, start_ns=start_ns, ) as ctx, ctx.span: + # Set peer.hostname when applicable + try: + region = getattr(instance, "meta", None) + region_name = getattr(region, "region_name", None) if region else None + host = compute_peer_hostname(endpoint_name, region_name, params) + if host: + ctx.span.set_tag_str(net.PEER_HOSTNAME, host) + except Exception: + log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_kinesis_api_call.started", [ctx]) if is_kinesis_put_operation: diff --git a/ddtrace/contrib/internal/botocore/services/sqs.py b/ddtrace/contrib/internal/botocore/services/sqs.py index 5bf238c8cfd..3d68f0d6123 100644 --- a/ddtrace/contrib/internal/botocore/services/sqs.py +++ b/ddtrace/contrib/internal/botocore/services/sqs.py @@ -10,6 +10,8 @@ from ddtrace.contrib.internal.trace_utils import ext_service from ddtrace.ext import SpanTypes from ddtrace.internal import core +from ddtrace.ext import net # type: ignore[attr-defined] +from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_messaging_operation from ddtrace.internal.schema import schematize_service_name @@ -157,6 +159,15 @@ def _patched_sqs_api_call(parent_ctx, original_func, instance, args, kwargs, fun call_trace=False, pin=pin, ) as ctx, ctx.span: + # Set peer.hostname when applicable + try: + region = getattr(instance, "meta", None) + region_name = getattr(region, "region_name", None) if region else None + host = compute_peer_hostname(endpoint_name, region_name, params) + if host: + ctx.span.set_tag_str(net.PEER_HOSTNAME, host) + except Exception: + log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_sqs_api_call.started", [ctx]) if should_update_messages: diff --git a/ddtrace/contrib/internal/botocore/utils.py b/ddtrace/contrib/internal/botocore/utils.py index 664bc5d7741..f142eca23fd 100644 --- a/ddtrace/contrib/internal/botocore/utils.py +++ b/ddtrace/contrib/internal/botocore/utils.py @@ -7,6 +7,7 @@ from typing import Dict from typing import Optional from typing import Tuple +from typing import Optional, Mapping from ddtrace import config from ddtrace.internal import core @@ -164,3 +165,44 @@ def extract_DD_json(message): except Exception: log.debug("Unable to parse AWS message attributes for Datadog Context.") return context_json + + +# ------------------------------------------------------------ +# Helper: derive peer.hostname for AWS services +# ------------------------------------------------------------ + + +def compute_peer_hostname(endpoint_name: str, region: Optional[str], params: Optional[Mapping[str, Any]] = None) -> Optional[str]: + """Return the URL hostname that should be set as *peer.hostname* for the given AWS service. + + Only handles services for which Datadog wants explicit hostnames (EventBridge, SQS, SNS, + Kinesis, DynamoDB/DocumentDB, S3). For other services or if *region* is unknown the + function returns ``None``. + """ + + if not region: + return None + + endpoint_name = (endpoint_name or "").lower() + + if endpoint_name == "events": # EventBridge + return f"events.{region}.amazonaws.com" + elif endpoint_name == "sqs": + return f"sqs.{region}.amazonaws.com" + elif endpoint_name == "sns": + return f"sns.{region}.amazonaws.com" + elif endpoint_name == "kinesis": + return f"kinesis.{region}.amazonaws.com" + elif endpoint_name in {"dynamodb", "dynamodbstreams"}: + return f"dynamodb.{region}.amazonaws.com" + elif endpoint_name == "s3": + bucket = None + if params is not None: + # params might be any mapping (botocore.client call arguments) + bucket = params.get("Bucket") + if bucket: + return f"{bucket}.s3.{region}.amazonaws.com" + return f"s3.{region}.amazonaws.com" + + # Service not in the mapping + return None diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index b83a0187b0c..48fae98b1fd 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -1,17 +1,31 @@ from ddtrace._trace.processor import TraceProcessor from ddtrace.constants import _BASE_SERVICE_KEY from ddtrace.settings._config import config +from ddtrace.internal.serverless import in_aws_lambda, in_gcp_function, in_azure_function from . import schematize_service_name class BaseServiceProcessor(TraceProcessor): def __init__(self): + # Determine the global (root) service for this process according to the + # active schema. In serverless environments the inferred base service + # often resolves to the string ``"runtime"`` which is not useful to + # users and pollutes span metadata. Detect that situation once and, if + # applicable, disable tagging entirely. + self._global_service = schematize_service_name((config.service or "").lower()) + # Skip tagging when running in a serverless runtime *and* the inferred + # service name is the generic "runtime" placeholder. + self._skip_tagging = self._global_service == "runtime" and ( + in_aws_lambda() or in_gcp_function() or in_azure_function() + ) + def process_trace(self, trace): - if not trace: - return + if not trace or self._skip_tagging: + # Nothing to do (either no spans, or tagging disabled for this env) + return trace traces_to_process = filter( lambda x: x.service and x.service.lower() != self._global_service, From dceba8e28e5d90249bffa2881e541a9627481ca6 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 17 Jul 2025 19:02:28 -0400 Subject: [PATCH 02/14] remove hostname --- ddtrace/contrib/internal/botocore/patch.py | 11 ----- .../internal/botocore/services/kinesis.py | 11 ----- .../contrib/internal/botocore/services/sqs.py | 11 ----- ddtrace/contrib/internal/botocore/utils.py | 42 ------------------- 4 files changed, 75 deletions(-) diff --git a/ddtrace/contrib/internal/botocore/patch.py b/ddtrace/contrib/internal/botocore/patch.py index ddcec028fce..327feb028fc 100644 --- a/ddtrace/contrib/internal/botocore/patch.py +++ b/ddtrace/contrib/internal/botocore/patch.py @@ -24,8 +24,6 @@ from ddtrace.ext import SpanTypes from ddtrace.internal import core from ddtrace.internal.constants import COMPONENT -from ddtrace.ext import net # type: ignore[attr-defined] -from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_api_operation from ddtrace.internal.schema import schematize_cloud_faas_operation @@ -274,15 +272,6 @@ def patched_api_call_fallback(original_func, instance, args, kwargs, function_va span_type=SpanTypes.HTTP, span_key="instrumented_api_call", ) as ctx, ctx.span: - # Set peer.hostname if applicable - try: - region = getattr(instance, "meta", None) - region_name = getattr(region, "region_name", None) if region else None - host = compute_peer_hostname(endpoint_name, region_name, params) - if host: - ctx.span.set_tag_str(net.PEER_HOSTNAME, host) - except Exception: - log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_api_call.started", [ctx]) if args and config.botocore["distributed_tracing"]: prep_context_injection(ctx, endpoint_name, operation, trace_operation, params) diff --git a/ddtrace/contrib/internal/botocore/services/kinesis.py b/ddtrace/contrib/internal/botocore/services/kinesis.py index ce3de4cb3f3..f1f71d0b819 100644 --- a/ddtrace/contrib/internal/botocore/services/kinesis.py +++ b/ddtrace/contrib/internal/botocore/services/kinesis.py @@ -13,8 +13,6 @@ from ddtrace.contrib.internal.trace_utils import ext_service from ddtrace.ext import SpanTypes from ddtrace.internal import core -from ddtrace.ext import net # type: ignore[attr-defined] -from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_messaging_operation from ddtrace.internal.schema import schematize_service_name @@ -156,15 +154,6 @@ def _patched_kinesis_api_call(parent_ctx, original_func, instance, args, kwargs, func_run=is_getrecords_call, start_ns=start_ns, ) as ctx, ctx.span: - # Set peer.hostname when applicable - try: - region = getattr(instance, "meta", None) - region_name = getattr(region, "region_name", None) if region else None - host = compute_peer_hostname(endpoint_name, region_name, params) - if host: - ctx.span.set_tag_str(net.PEER_HOSTNAME, host) - except Exception: - log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_kinesis_api_call.started", [ctx]) if is_kinesis_put_operation: diff --git a/ddtrace/contrib/internal/botocore/services/sqs.py b/ddtrace/contrib/internal/botocore/services/sqs.py index 3d68f0d6123..5bf238c8cfd 100644 --- a/ddtrace/contrib/internal/botocore/services/sqs.py +++ b/ddtrace/contrib/internal/botocore/services/sqs.py @@ -10,8 +10,6 @@ from ddtrace.contrib.internal.trace_utils import ext_service from ddtrace.ext import SpanTypes from ddtrace.internal import core -from ddtrace.ext import net # type: ignore[attr-defined] -from ddtrace.contrib.internal.botocore.utils import compute_peer_hostname from ddtrace.internal.logger import get_logger from ddtrace.internal.schema import schematize_cloud_messaging_operation from ddtrace.internal.schema import schematize_service_name @@ -159,15 +157,6 @@ def _patched_sqs_api_call(parent_ctx, original_func, instance, args, kwargs, fun call_trace=False, pin=pin, ) as ctx, ctx.span: - # Set peer.hostname when applicable - try: - region = getattr(instance, "meta", None) - region_name = getattr(region, "region_name", None) if region else None - host = compute_peer_hostname(endpoint_name, region_name, params) - if host: - ctx.span.set_tag_str(net.PEER_HOSTNAME, host) - except Exception: - log.debug("Failed to compute peer.hostname", exc_info=True) core.dispatch("botocore.patched_sqs_api_call.started", [ctx]) if should_update_messages: diff --git a/ddtrace/contrib/internal/botocore/utils.py b/ddtrace/contrib/internal/botocore/utils.py index f142eca23fd..664bc5d7741 100644 --- a/ddtrace/contrib/internal/botocore/utils.py +++ b/ddtrace/contrib/internal/botocore/utils.py @@ -7,7 +7,6 @@ from typing import Dict from typing import Optional from typing import Tuple -from typing import Optional, Mapping from ddtrace import config from ddtrace.internal import core @@ -165,44 +164,3 @@ def extract_DD_json(message): except Exception: log.debug("Unable to parse AWS message attributes for Datadog Context.") return context_json - - -# ------------------------------------------------------------ -# Helper: derive peer.hostname for AWS services -# ------------------------------------------------------------ - - -def compute_peer_hostname(endpoint_name: str, region: Optional[str], params: Optional[Mapping[str, Any]] = None) -> Optional[str]: - """Return the URL hostname that should be set as *peer.hostname* for the given AWS service. - - Only handles services for which Datadog wants explicit hostnames (EventBridge, SQS, SNS, - Kinesis, DynamoDB/DocumentDB, S3). For other services or if *region* is unknown the - function returns ``None``. - """ - - if not region: - return None - - endpoint_name = (endpoint_name or "").lower() - - if endpoint_name == "events": # EventBridge - return f"events.{region}.amazonaws.com" - elif endpoint_name == "sqs": - return f"sqs.{region}.amazonaws.com" - elif endpoint_name == "sns": - return f"sns.{region}.amazonaws.com" - elif endpoint_name == "kinesis": - return f"kinesis.{region}.amazonaws.com" - elif endpoint_name in {"dynamodb", "dynamodbstreams"}: - return f"dynamodb.{region}.amazonaws.com" - elif endpoint_name == "s3": - bucket = None - if params is not None: - # params might be any mapping (botocore.client call arguments) - bucket = params.get("Bucket") - if bucket: - return f"{bucket}.s3.{region}.amazonaws.com" - return f"s3.{region}.amazonaws.com" - - # Service not in the mapping - return None From f506423a967f05a1130eac3a8a29840a9789a253 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 17 Jul 2025 21:11:32 -0400 Subject: [PATCH 03/14] apply peer.service in aws.lambda --- ddtrace/_trace/utils_botocore/span_tags.py | 49 +++++++++++++++++++ ddtrace/contrib/internal/aiobotocore/patch.py | 49 +++++++++++++++++++ ddtrace/contrib/internal/boto/patch.py | 47 ++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/ddtrace/_trace/utils_botocore/span_tags.py b/ddtrace/_trace/utils_botocore/span_tags.py index b1d9440581a..d0efcc808e4 100644 --- a/ddtrace/_trace/utils_botocore/span_tags.py +++ b/ddtrace/_trace/utils_botocore/span_tags.py @@ -13,11 +13,53 @@ from ddtrace.ext import http from ddtrace.internal.constants import COMPONENT from ddtrace.internal.utils.formats import deep_getattr +from ddtrace.internal.serverless import in_aws_lambda _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. + + Logic mirrors the JS mapping provided by the user: + + events -> events..amazonaws.com + sqs -> sqs..amazonaws.com + sns -> sns..amazonaws.com + kinesis -> kinesis..amazonaws.com + dynamodb -> dynamodb..amazonaws.com + s3 -> .s3..amazonaws.com (if Bucket param present) + s3..amazonaws.com (otherwise) + + Unknown services or missing region return ``None``. + """ + + if not region: + return None + + aws_service = service.lower() + + 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 + + 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 @@ -51,6 +93,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 diff --git a/ddtrace/contrib/internal/aiobotocore/patch.py b/ddtrace/contrib/internal/aiobotocore/patch.py index d1eaef0f3e3..4fd1216faab 100644 --- a/ddtrace/contrib/internal/aiobotocore/patch.py +++ b/ddtrace/contrib/internal/aiobotocore/patch.py @@ -21,6 +21,7 @@ from ddtrace.internal.utils.formats import asbool from ddtrace.internal.utils.formats import deep_getattr from ddtrace.internal.utils.version import parse_version +from ddtrace.internal.serverless import in_aws_lambda from ddtrace.trace import Pin @@ -69,6 +70,47 @@ def unpatch(): aiobotocore.client._datadog_patch = False unwrap(aiobotocore.client.AioBaseClient, "_make_api_call") +# 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. + + Logic mirrors the JS mapping provided by the user: + + events -> events..amazonaws.com + sqs -> sqs..amazonaws.com + sns -> sns..amazonaws.com + kinesis -> kinesis..amazonaws.com + dynamodb -> dynamodb..amazonaws.com + s3 -> .s3..amazonaws.com (if Bucket param present) + s3..amazonaws.com (otherwise) + + Unknown services or missing region return ``None``. + """ + + if not region: + return None + + aws_service = service.lower() + + 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 + + class WrappedClientResponseContentProxy(wrapt.ObjectProxy): def __init__(self, body, pin, parent_span): @@ -145,6 +187,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, @@ -179,3 +227,4 @@ async def _wrapped_api_call(original_func, instance, args, kwargs): span.set_tag_str("aws.requestid2", request_id2) return result + diff --git a/ddtrace/contrib/internal/boto/patch.py b/ddtrace/contrib/internal/boto/patch.py index d68fe6e9b56..27c1cf7f3a3 100644 --- a/ddtrace/contrib/internal/boto/patch.py +++ b/ddtrace/contrib/internal/boto/patch.py @@ -19,6 +19,7 @@ from ddtrace.internal.utils import get_argument_value from ddtrace.internal.utils.formats import asbool from ddtrace.internal.utils.wrappers import unwrap +from ddtrace.internal.serverless import in_aws_lambda from ddtrace.trace import Pin @@ -76,6 +77,46 @@ def unpatch(): unwrap(boto.connection.AWSQueryConnection, "make_request") unwrap(boto.connection.AWSAuthConnection, "make_request") +# 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. + + Logic mirrors the JS mapping provided by the user: + + events -> events..amazonaws.com + sqs -> sqs..amazonaws.com + sns -> sns..amazonaws.com + kinesis -> kinesis..amazonaws.com + dynamodb -> dynamodb..amazonaws.com + s3 -> .s3..amazonaws.com (if Bucket param present) + s3..amazonaws.com (otherwise) + + Unknown services or missing region return ``None``. + """ + + if not region: + return None + + aws_service = service.lower() + + 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 + # ec2, sqs, kinesis def patched_query_request(original_func, instance, args, kwargs): @@ -122,6 +163,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 From c02aaf40f9858081fb3139bf61016a1bd0fc6eb6 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Tue, 22 Jul 2025 15:14:36 -0400 Subject: [PATCH 04/14] add release note --- ...rvice-representation-b1f81fe0553a7a45.yaml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml diff --git a/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml new file mode 100644 index 00000000000..47514409f25 --- /dev/null +++ b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml @@ -0,0 +1,65 @@ +--- +#instructions: +# The style guide below provides explanations, instructions, and templates to write your own release note. +# Once finished, all irrelevant sections (including this instruction section) should be removed, +# and the release note should be committed with the rest of the changes. +# +# The main goal of a release note is to provide a brief overview of a change and provide actionable steps to the user. +# The release note should clearly communicate what the change is, why the change was made, and how a user can migrate their code. +# +# The release note should also clearly distinguish between announcements and user instructions. Use: +# * Past tense for previous/existing behavior (ex: ``resulted, caused, failed``) +# * Third person present tense for the change itself (ex: ``adds, fixes, upgrades``) +# * Active present infinitive for user instructions (ex: ``set, use, add``) +# +# Release notes should: +# * Use plain language +# * Be concise +# * Include actionable steps with the necessary code changes +# * Include relevant links (bug issues, upstream issues or release notes, documentation pages) +# * Use full sentences with sentence-casing and punctuation. +# * Before using Datadog specific acronyms/terminology, a release note must first introduce them with a definition. +# +# Release notes should not: +# * Be vague. Example: ``fixes an issue in tracing``. +# * Use overly technical language +# * Use dynamic links (``stable/latest/1.x`` URLs). Instead, use static links (specific version, commit hash) whenever possible so that they don't break in the future. +prelude: > + Usually in tandem with a new feature or major change, meant to provide context or background for a major change. + No specific format other than a required scope is provided and the author is requested to use their best judgment. + Format: : . +features: + - | + For new features such as a new integration or component. Use present tense with the following format: + Format: : This introduces . +issues: + - | + For known issues. Use present tense with the following format: + Format: : There is a known issue with . + . +upgrade: + - | + For enhanced functionality or if package dependencies are upgraded. If applicable, include instructions + for how a user can migrate their code. + Use present tense with the following formats, respectively for enhancements or removals: + Format: : This upgrades . With this upgrade, you can . + - | + Format: : is now removed. As an alternative to , you can use instead. +deprecations: + - | + Warning of a component or member of the public API being removed in the future. + Use present tense for when deprecation actually happens and future tense for when removal is planned to happen. + Include deprecation/removal timeline, as well as workarounds and alternatives in the following format: + Format: : is deprecated and will be removed in . + As an alternative to , you can use instead. +fixes: + - | + For reporting bug fixes. + Use past tense for the problem and present tense for the fix and solution in the following format: + Format: : This fix resolves an issue where caused . +other: + - | + For any change which does not fall into any of the above categories. Since changes falling into this category are + likely rare and not very similar to each other, no specific format other than a required scope is provided. + The author is requested to use their best judgment to ensure a quality release note. + Format: : . From 1ca706cbfde210fddf113745c0867f9784670531 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Tue, 22 Jul 2025 15:38:40 -0400 Subject: [PATCH 05/14] linter --- ddtrace/_trace/utils_botocore/span_tags.py | 23 +++++---- ddtrace/contrib/internal/aiobotocore/patch.py | 45 +--------------- ddtrace/contrib/internal/boto/patch.py | 51 ++++--------------- ddtrace/internal/schema/processor.py | 4 +- 4 files changed, 26 insertions(+), 97 deletions(-) diff --git a/ddtrace/_trace/utils_botocore/span_tags.py b/ddtrace/_trace/utils_botocore/span_tags.py index d0efcc808e4..2c0581d2ef6 100644 --- a/ddtrace/_trace/utils_botocore/span_tags.py +++ b/ddtrace/_trace/utils_botocore/span_tags.py @@ -12,8 +12,8 @@ from ddtrace.ext import aws from ddtrace.ext import http from ddtrace.internal.constants import COMPONENT -from ddtrace.internal.utils.formats import deep_getattr from ddtrace.internal.serverless import in_aws_lambda +from ddtrace.internal.utils.formats import deep_getattr _PAYLOAD_TAGGER = AWSPayloadTagging() @@ -23,17 +23,16 @@ 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. - Logic mirrors the JS mapping provided by the user: - - events -> events..amazonaws.com - sqs -> sqs..amazonaws.com - sns -> sns..amazonaws.com - kinesis -> kinesis..amazonaws.com - dynamodb -> dynamodb..amazonaws.com - s3 -> .s3..amazonaws.com (if Bucket param present) - s3..amazonaws.com (otherwise) + Only returns hostnames for specific AWS services: + - eventbridge/events -> events..amazonaws.com + - sqs -> sqs..amazonaws.com + - sns -> sns..amazonaws.com + - kinesis -> kinesis..amazonaws.com + - dynamodb -> dynamodb..amazonaws.com + - s3 -> .s3..amazonaws.com (if Bucket param present) + s3..amazonaws.com (otherwise) - Unknown services or missing region return ``None``. + Other services return ``None``. """ if not region: @@ -41,6 +40,7 @@ def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, 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": @@ -57,6 +57,7 @@ def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, return f"{bucket}.s3.{region}.amazonaws.com" return f"s3.{region}.amazonaws.com" + # Return None for all other services return None diff --git a/ddtrace/contrib/internal/aiobotocore/patch.py b/ddtrace/contrib/internal/aiobotocore/patch.py index 4fd1216faab..558d8e0af60 100644 --- a/ddtrace/contrib/internal/aiobotocore/patch.py +++ b/ddtrace/contrib/internal/aiobotocore/patch.py @@ -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 @@ -16,12 +17,12 @@ 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 from ddtrace.internal.utils.formats import deep_getattr from ddtrace.internal.utils.version import parse_version -from ddtrace.internal.serverless import in_aws_lambda from ddtrace.trace import Pin @@ -70,47 +71,6 @@ def unpatch(): aiobotocore.client._datadog_patch = False unwrap(aiobotocore.client.AioBaseClient, "_make_api_call") -# 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. - - Logic mirrors the JS mapping provided by the user: - - events -> events..amazonaws.com - sqs -> sqs..amazonaws.com - sns -> sns..amazonaws.com - kinesis -> kinesis..amazonaws.com - dynamodb -> dynamodb..amazonaws.com - s3 -> .s3..amazonaws.com (if Bucket param present) - s3..amazonaws.com (otherwise) - - Unknown services or missing region return ``None``. - """ - - if not region: - return None - - aws_service = service.lower() - - 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 - - class WrappedClientResponseContentProxy(wrapt.ObjectProxy): def __init__(self, body, pin, parent_span): @@ -227,4 +187,3 @@ async def _wrapped_api_call(original_func, instance, args, kwargs): span.set_tag_str("aws.requestid2", request_id2) return result - diff --git a/ddtrace/contrib/internal/boto/patch.py b/ddtrace/contrib/internal/boto/patch.py index 27c1cf7f3a3..e0725a5d64d 100644 --- a/ddtrace/contrib/internal/boto/patch.py +++ b/ddtrace/contrib/internal/boto/patch.py @@ -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 @@ -16,10 +17,10 @@ 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 -from ddtrace.internal.serverless import in_aws_lambda from ddtrace.trace import Pin @@ -77,46 +78,6 @@ def unpatch(): unwrap(boto.connection.AWSQueryConnection, "make_request") unwrap(boto.connection.AWSAuthConnection, "make_request") -# 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. - - Logic mirrors the JS mapping provided by the user: - - events -> events..amazonaws.com - sqs -> sqs..amazonaws.com - sns -> sns..amazonaws.com - kinesis -> kinesis..amazonaws.com - dynamodb -> dynamodb..amazonaws.com - s3 -> .s3..amazonaws.com (if Bucket param present) - s3..amazonaws.com (otherwise) - - Unknown services or missing region return ``None``. - """ - - if not region: - return None - - aws_service = service.lower() - - 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 - # ec2, sqs, kinesis def patched_query_request(original_func, instance, args, kwargs): @@ -163,7 +124,7 @@ def patched_query_request(original_func, instance, args, kwargs): meta[aws.REGION] = region_name meta[aws.AWSREGION] = region_name - if in_aws_lambda(): + 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: @@ -230,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 diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index 48fae98b1fd..f74fafa6330 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -1,7 +1,9 @@ from ddtrace._trace.processor import TraceProcessor from ddtrace.constants import _BASE_SERVICE_KEY +from ddtrace.internal.serverless import in_aws_lambda +from ddtrace.internal.serverless import in_azure_function +from ddtrace.internal.serverless import in_gcp_function from ddtrace.settings._config import config -from ddtrace.internal.serverless import in_aws_lambda, in_gcp_function, in_azure_function from . import schematize_service_name From a6b1f2cd73337f3470dcb46944d4266f6f972d90 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Tue, 22 Jul 2025 17:26:56 -0400 Subject: [PATCH 06/14] add tests --- tests/contrib/aiobotocore/test.py | 99 ++++++++++++++++++++++++++++ tests/contrib/botocore/test.py | 104 ++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/tests/contrib/aiobotocore/test.py b/tests/contrib/aiobotocore/test.py index d21e3fa52e6..72c36461a41 100644 --- a/tests/contrib/aiobotocore/test.py +++ b/tests/contrib/aiobotocore/test.py @@ -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" diff --git a/tests/contrib/botocore/test.py b/tests/contrib/botocore/test.py index 2e1bd4e1e8d..74271415b7b 100644 --- a/tests/contrib/botocore/test.py +++ b/tests/contrib/botocore/test.py @@ -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" From b70b7edbad838dcf22a02ad6a5dc81553b5396e6 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Wed, 23 Jul 2025 17:28:41 -0400 Subject: [PATCH 07/14] add feature note --- ...rvice-representation-b1f81fe0553a7a45.yaml | 66 ++----------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml index 47514409f25..82b62038eca 100644 --- a/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml +++ b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml @@ -1,65 +1,7 @@ --- -#instructions: -# The style guide below provides explanations, instructions, and templates to write your own release note. -# Once finished, all irrelevant sections (including this instruction section) should be removed, -# and the release note should be committed with the rest of the changes. -# -# The main goal of a release note is to provide a brief overview of a change and provide actionable steps to the user. -# The release note should clearly communicate what the change is, why the change was made, and how a user can migrate their code. -# -# The release note should also clearly distinguish between announcements and user instructions. Use: -# * Past tense for previous/existing behavior (ex: ``resulted, caused, failed``) -# * Third person present tense for the change itself (ex: ``adds, fixes, upgrades``) -# * Active present infinitive for user instructions (ex: ``set, use, add``) -# -# Release notes should: -# * Use plain language -# * Be concise -# * Include actionable steps with the necessary code changes -# * Include relevant links (bug issues, upstream issues or release notes, documentation pages) -# * Use full sentences with sentence-casing and punctuation. -# * Before using Datadog specific acronyms/terminology, a release note must first introduce them with a definition. -# -# Release notes should not: -# * Be vague. Example: ``fixes an issue in tracing``. -# * Use overly technical language -# * Use dynamic links (``stable/latest/1.x`` URLs). Instead, use static links (specific version, commit hash) whenever possible so that they don't break in the future. -prelude: > - Usually in tandem with a new feature or major change, meant to provide context or background for a major change. - No specific format other than a required scope is provided and the author is requested to use their best judgment. - Format: : . features: - | - For new features such as a new integration or component. Use present tense with the following format: - Format: : This introduces . -issues: - - | - For known issues. Use present tense with the following format: - Format: : There is a known issue with . - . -upgrade: - - | - For enhanced functionality or if package dependencies are upgraded. If applicable, include instructions - for how a user can migrate their code. - Use present tense with the following formats, respectively for enhancements or removals: - Format: : This upgrades . With this upgrade, you can . - - | - Format: : is now removed. As an alternative to , you can use instead. -deprecations: - - | - Warning of a component or member of the public API being removed in the future. - Use present tense for when deprecation actually happens and future tense for when removal is planned to happen. - Include deprecation/removal timeline, as well as workarounds and alternatives in the following format: - Format: : is deprecated and will be removed in . - As an alternative to , you can use instead. -fixes: - - | - For reporting bug fixes. - Use past tense for the problem and present tense for the fix and solution in the following format: - Format: : This fix resolves an issue where caused . -other: - - | - For any change which does not fall into any of the above categories. Since changes falling into this category are - likely rare and not very similar to each other, no specific format other than a required scope is provided. - The author is requested to use their best judgment to ensure a quality release note. - Format: : . + aws: set peer.service explictly and remove base_service in serverless + environments to improve the accuracy of serverless service representation. + + From d3a3e774fa0a1bc345f01c2b45e52cef3cda4eb3 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 14:11:03 -0400 Subject: [PATCH 08/14] simplify base_service changes --- ddtrace/internal/schema/processor.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index f74fafa6330..e9c10d22bf9 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -10,23 +10,10 @@ class BaseServiceProcessor(TraceProcessor): def __init__(self): - # Determine the global (root) service for this process according to the - # active schema. In serverless environments the inferred base service - # often resolves to the string ``"runtime"`` which is not useful to - # users and pollutes span metadata. Detect that situation once and, if - # applicable, disable tagging entirely. - self._global_service = schematize_service_name((config.service or "").lower()) - # Skip tagging when running in a serverless runtime *and* the inferred - # service name is the generic "runtime" placeholder. - self._skip_tagging = self._global_service == "runtime" and ( - in_aws_lambda() or in_gcp_function() or in_azure_function() - ) - def process_trace(self, trace): - if not trace or self._skip_tagging: - # Nothing to do (either no spans, or tagging disabled for this env) + if not trace or in_aws_lambda() or in_gcp_function() or in_azure_function(): return trace traces_to_process = filter( From c51fda4f8338536683baf76e7ee59b656858bc97 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 14:32:09 -0400 Subject: [PATCH 09/14] update base_service tag in serverless environments --- ...ons_snapshot.test_http_get_distributed_tracing[disabled].json | 1 - ...ions_snapshot.test_http_get_distributed_tracing[enabled].json | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json index d61de563a9a..3ce794ce899 100644 --- a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json +++ b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json @@ -40,7 +40,6 @@ "parent_id": 1, "type": "http", "meta": { - "_dd.base_service": "test-func", "component": "requests", "http.method": "GET", "http.status_code": "200", diff --git a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json index 92456662910..91551dfec2f 100644 --- a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json +++ b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json @@ -40,7 +40,6 @@ "parent_id": 1, "type": "http", "meta": { - "_dd.base_service": "test-func", "component": "requests", "http.method": "GET", "http.status_code": "200", From e7188d2bf772dee1698c153c8b8d0eb8860fc0da Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 14:55:25 -0400 Subject: [PATCH 10/14] focus changes only on aws --- ddtrace/internal/schema/processor.py | 2 +- ...ns_snapshot.test_http_get_distributed_tracing[disabled].json | 1 + ...ons_snapshot.test_http_get_distributed_tracing[enabled].json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index e9c10d22bf9..4f0aa1b2b45 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -13,7 +13,7 @@ def __init__(self): self._global_service = schematize_service_name((config.service or "").lower()) def process_trace(self, trace): - if not trace or in_aws_lambda() or in_gcp_function() or in_azure_function(): + if not trace or in_aws_lambda(): return trace traces_to_process = filter( diff --git a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json index 3ce794ce899..d61de563a9a 100644 --- a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json +++ b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[disabled].json @@ -40,6 +40,7 @@ "parent_id": 1, "type": "http", "meta": { + "_dd.base_service": "test-func", "component": "requests", "http.method": "GET", "http.status_code": "200", diff --git a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json index 91551dfec2f..92456662910 100644 --- a/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json +++ b/tests/snapshots/tests.contrib.azure_functions.test_azure_functions_snapshot.test_http_get_distributed_tracing[enabled].json @@ -40,6 +40,7 @@ "parent_id": 1, "type": "http", "meta": { + "_dd.base_service": "test-func", "component": "requests", "http.method": "GET", "http.status_code": "200", From 53c52d12e531bbb336f680cdf636f11bf786cd75 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 15:18:18 -0400 Subject: [PATCH 11/14] linter --- ddtrace/internal/schema/processor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index 4f0aa1b2b45..1f84a24748d 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -1,8 +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.internal.serverless import in_azure_function -from ddtrace.internal.serverless import in_gcp_function from ddtrace.settings._config import config from . import schematize_service_name From fbac06eaf698ef5f90fc1fc94be4fd21e2413c3d Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 16:00:19 -0400 Subject: [PATCH 12/14] cleanup fixes --- ddtrace/_trace/utils_botocore/span_tags.py | 30 +++++++++---------- ddtrace/internal/schema/processor.py | 2 ++ ...rvice-representation-b1f81fe0553a7a45.yaml | 8 ++--- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ddtrace/_trace/utils_botocore/span_tags.py b/ddtrace/_trace/utils_botocore/span_tags.py index 2c0581d2ef6..e6e4b570274 100644 --- a/ddtrace/_trace/utils_botocore/span_tags.py +++ b/ddtrace/_trace/utils_botocore/span_tags.py @@ -18,6 +18,16 @@ _PAYLOAD_TAGGER = AWSPayloadTagging() +SERVICE_MAP = { + "eventbridge": "events", + "events": "events", + "sqs": "sqs", + "sns": "sns", + "kinesis": "kinesis", + "dynamodb": "dynamodb", + "dynamodbdocument": "dynamodb", +} + # 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]: @@ -40,25 +50,13 @@ def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, 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 f"{bucket}.s3.{region}.amazonaws.com" if bucket else f"s3.{region}.amazonaws.com" + + mapped = SERVICE_MAP.get(aws_service) - # Return None for all other services - return None + return f"{mapped}.{region}.amazonaws.com" if mapped else None def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params, endpoint_name, operation): diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index 1f84a24748d..aebeedac9fa 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -11,6 +11,8 @@ def __init__(self): self._global_service = schematize_service_name((config.service or "").lower()) def process_trace(self, trace): + # AWS Lambda spans receive unhelpful base_service value of runtime + # Remove base_service to prevent service overrides in Lambda spans if not trace or in_aws_lambda(): return trace diff --git a/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml index 82b62038eca..0535ee9e8cd 100644 --- a/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml +++ b/releasenotes/notes/fix-serverless-service-representation-b1f81fe0553a7a45.yaml @@ -1,7 +1,7 @@ --- features: - | - aws: set peer.service explictly and remove base_service in serverless - environments to improve the accuracy of serverless service representation. - - + aws: Set peer.service explictly to improve the accuracy of serverless + service representation. Base_service defaults to unhelpful value "runtime" + in serverless spans. Remove base_service to prevent unwanted service + overrides in Lambda spans. From 4e0ef0989bda40dcd538b59a8f7d74b2b1baef8f Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 16:50:40 -0400 Subject: [PATCH 13/14] improve SLO --- ddtrace/internal/schema/processor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index aebeedac9fa..de888bb1441 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -9,11 +9,13 @@ class BaseServiceProcessor(TraceProcessor): def __init__(self): self._global_service = schematize_service_name((config.service or "").lower()) + self._in_aws_lambda = in_aws_lambda() + def process_trace(self, trace): # AWS Lambda spans receive unhelpful base_service value of runtime # Remove base_service to prevent service overrides in Lambda spans - if not trace or in_aws_lambda(): + if not trace or self._in_aws_lambda: return trace traces_to_process = filter( From b7090b8b246efa2680a84dc28dfed9a0951d7fc7 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 24 Jul 2025 16:56:25 -0400 Subject: [PATCH 14/14] linter --- ddtrace/internal/schema/processor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ddtrace/internal/schema/processor.py b/ddtrace/internal/schema/processor.py index de888bb1441..1cdf204279b 100644 --- a/ddtrace/internal/schema/processor.py +++ b/ddtrace/internal/schema/processor.py @@ -11,7 +11,6 @@ def __init__(self): self._global_service = schematize_service_name((config.service or "").lower()) self._in_aws_lambda = in_aws_lambda() - def process_trace(self, trace): # AWS Lambda spans receive unhelpful base_service value of runtime # Remove base_service to prevent service overrides in Lambda spans