From be08bab9a43083dc2478fd5af748ef80cbc399c3 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Fri, 10 Oct 2025 15:36:53 -0700 Subject: [PATCH 01/23] add new bedrock agentcore patching and attributes --- .../distro/_aws_attribute_keys.py | 9 ++ .../patches/_bedrock_agentcore_patches.py | 103 ++++++++++++++++++ .../distro/patches/_botocore_patches.py | 7 ++ .../test_instrumentation_patch.py | 100 +++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py rename aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/{ => patches}/test_instrumentation_patch.py (89%) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py index d349b8890..2ccbf64b9 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py @@ -20,6 +20,8 @@ # TODO:Move to Semantic Conventions when these attributes are added. AWS_AUTH_ACCESS_KEY: str = "aws.auth.account.access_key" AWS_AUTH_REGION: str = "aws.auth.region" +AWS_AUTH_CREDENTIAL_PROVIDER_ARN: str = "aws.auth.credential_provider.arn" +AWS_GATEWAY_TARGET_ID = "aws.gateway.target.id" AWS_SQS_QUEUE_URL: str = "aws.sqs.queue.url" AWS_SQS_QUEUE_NAME: str = "aws.sqs.queue.name" AWS_KINESIS_STREAM_ARN: str = "aws.kinesis.stream.arn" @@ -41,3 +43,10 @@ AWS_REMOTE_RESOURCE_ACCOUNT_ID: str = "aws.remote.resource.account.id" AWS_REMOTE_RESOURCE_REGION: str = "aws.remote.resource.region" AWS_SERVICE_TYPE: str = "aws.service.type" +AWS_BEDROCK_AGENTCORE_RUNTIME_ARN: str = "aws.bedrock.agentcore.runtime.arn" +AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN: str = "aws.bedrock.agentcore.runtime_endpoint.arn" +AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN: str = "aws.bedrock.agentcore.code_interpreter.arn" +AWS_BEDROCK_AGENTCORE_BROWSER_ARN: str = "aws.bedrock.agentcore.browser.arn" +AWS_BEDROCK_AGENTCORE_MEMORY_ARN: str = "aws.bedrock.agentcore.memory.arn" +AWS_BEDROCK_AGENTCORE_GATEWAY_ARN: str = "aws.bedrock.agentcore.gateway.arn" +AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN: str = "aws.bedrock.agentcore.identity.workload_identity.arn" diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py new file mode 100644 index 000000000..6cc8ef2ae --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py @@ -0,0 +1,103 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict + +from amazon.opentelemetry.distro._aws_attribute_keys import ( + AWS_AUTH_CREDENTIAL_PROVIDER_ARN, + AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN, + AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, + AWS_BEDROCK_AGENTCORE_MEMORY_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, + AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, + AWS_GATEWAY_TARGET_ID, +) +from opentelemetry.instrumentation.botocore.extensions.types import ( + _AttributeMapT, + _AwsSdkExtension, + _BotocoreInstrumentorContext, + _BotoResultT, +) +from opentelemetry.trace.span import Span + +GEN_AI_RUNTIME_ID = "gen_ai.runtime.id" +GEN_AI_BROWSER_ID = "gen_ai.browser.id" +GEN_AI_CODE_INTERPRETER_ID = "gen_ai.code_interpreter.id" +GEN_AI_MEMORY_ID = "gen_ai.memory.id" +GEN_AI_GATEWAY_ID = "gen_ai.gateway.id" + +# Mapping of flattened JSON paths to attribute keys +_ATTRIBUTE_MAPPING = { + "agentRuntimeArn": AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + "agentRuntimeEndpointArn": AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, + "agentRuntimeId": GEN_AI_RUNTIME_ID, + "browserArn": AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + "browserId": GEN_AI_BROWSER_ID, + "browserIdentifier": GEN_AI_BROWSER_ID, + "codeInterpreterArn": AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN, + "codeInterpreterId": GEN_AI_CODE_INTERPRETER_ID, + "codeInterpreterIdentifier": GEN_AI_CODE_INTERPRETER_ID, + "gatewayArn": AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, + "gatewayId": GEN_AI_GATEWAY_ID, + "gatewayIdentifier": GEN_AI_GATEWAY_ID, + "targetId": AWS_GATEWAY_TARGET_ID, + "memory.arn": AWS_BEDROCK_AGENTCORE_MEMORY_ARN, + "memory.id": GEN_AI_MEMORY_ID, + "memoryId": GEN_AI_MEMORY_ID, + "credentialProviderArn": AWS_AUTH_CREDENTIAL_PROVIDER_ARN, + "workloadIdentityArn": AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, + "workloadIdentityDetails.workloadIdentityArn": AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, +} + + +class _BedrockAgentCoreExtension(_AwsSdkExtension): + def extract_attributes(self, attributes: _AttributeMapT): + extracted_attrs = self._extract_attributes(self._call_context.params) + attributes.update(extracted_attrs) + + def on_success( + self, + span: Span, + result: _BotoResultT, + instrumentor_context: _BotocoreInstrumentorContext, + ): + if span is None or not span.is_recording(): + return + + extracted_attrs = self._extract_attributes(result) + for attr_name, attr_value in extracted_attrs.items(): + span.set_attribute(attr_name, attr_value) + + @staticmethod + def _extract_attributes(params: Dict[str, Any]): + """Extracts all Bedrock AgentCore attributes using mapping-based traversal""" + attrs = {} + for path, attr_key in _ATTRIBUTE_MAPPING.items(): + value = _BedrockAgentCoreExtension._get_nested_value(params, path) + if value: + attrs[attr_key] = value + return attrs + + @staticmethod + def _get_nested_value(data: Dict[str, Any], path: str): + """Get value from nested dictionary using dot notation path""" + keys = path.split(".") + value = data + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + else: + return None + return value diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py index 10fc77182..900ce688f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py @@ -23,6 +23,9 @@ AWS_STEPFUNCTIONS_ACTIVITY_ARN, AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) +from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( # noqa # pylint: disable=unused-import + _BedrockAgentCoreExtension, +) from amazon.opentelemetry.distro.patches._bedrock_patches import ( # noqa # pylint: disable=unused-import _BedrockAgentExtension, _BedrockAgentRuntimeExtension, @@ -247,6 +250,10 @@ def _apply_botocore_bedrock_patch() -> None: # pylint: disable=too-many-stateme _KNOWN_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension") _KNOWN_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension") _KNOWN_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension") + _KNOWN_EXTENSIONS["bedrock-agentcore"] = _lazy_load(".._bedrock_agentcore_patches", "_BedrockAgentCoreExtension") + _KNOWN_EXTENSIONS["bedrock-agentcore-control"] = _lazy_load( + ".._bedrock_agentcore_patches", "_BedrockAgentCoreExtension" + ) # TODO: The following code is to patch bedrock-runtime bugs that are fixed in # opentelemetry-instrumentation-botocore==0.56b0 in these PRs: diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py similarity index 89% rename from aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py rename to aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 256ee3673..57eb7378f 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -10,6 +10,24 @@ import opentelemetry.sdk.extension.aws.resource.ec2 as ec2_resource import opentelemetry.sdk.extension.aws.resource.eks as eks_resource +from amazon.opentelemetry.distro._aws_attribute_keys import ( + AWS_AUTH_CREDENTIAL_PROVIDER_ARN, + AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN, + AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, + AWS_BEDROCK_AGENTCORE_MEMORY_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, + AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, + AWS_GATEWAY_TARGET_ID, +) +from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( + GEN_AI_BROWSER_ID, + GEN_AI_CODE_INTERPRETER_ID, + GEN_AI_GATEWAY_ID, + GEN_AI_MEMORY_ID, + GEN_AI_RUNTIME_ID, +) from amazon.opentelemetry.distro.patches._instrumentation_patch import ( AWS_GEVENT_PATCH_MODULES, apply_instrumentation_patches, @@ -38,6 +56,20 @@ _LAMBDA_FUNCTION_NAME: str = "lambdaFunctionName" _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" _TABLE_ARN: str = "arn:aws:dynamodb:us-west-2:123456789012:table/testTable" +_AGENTCORE_RUNTIME_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime" +_AGENTCORE_RUNTIME_ENDPOINT_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" +_AGENTCORE_RUNTIME_ID: str = "test-runtime-123" +_AGENTCORE_BROWSER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/test-browser" +_AGENTCORE_BROWSER_ID: str = "agentBrowser-123456789" +_AGENTCORE_CODE_INTERPRETER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/test-ci" +_AGENTCORE_CODE_INTERPRETER_ID: str = "agentCodeInterpreter-123456789" +_AGENTCORE_GATEWAY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/test-gateway" +_AGENTCORE_GATEWAY_ID: str = "agentGateway-123456789" +_AGENTCORE_TARGET_ID: str = "target-123456789" +_AGENTCORE_MEMORY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/test-memory" +_AGENTCORE_MEMORY_ID: str = "agentMemory-123456789" +_AGENTCORE_CREDENTIAL_PROVIDER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:credential-provider/test-cp" +_AGENTCORE_WORKLOAD_IDENTITY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:workload-identity/test-wi" # Patch names IMPORTLIB_METADATA_VERSION_PATCH: str = "amazon.opentelemetry.distro._utils.version" @@ -160,6 +192,14 @@ def _test_unpatched_botocore_instrumentation(self): "bedrock-agent-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock Agent Runtime extension" ) + # Bedrock AgentCore + self.assertFalse("bedrock-agentcore" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock AgentCore extension") + + # Bedrock AgentCore Control + self.assertFalse( + "bedrock-agentcore-control" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock AgentCore Control extension" + ) + # BedrockRuntime self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") @@ -237,6 +277,41 @@ def _test_patched_botocore_instrumentation(self): bedrock_agent_runtime_sucess_attributes: Dict[str, str] = _do_on_success_bedrock("bedrock-agent-runtime") self.assertEqual(len(bedrock_agent_runtime_sucess_attributes), 0) + # Bedrock AgentCore + self.assertTrue("bedrock-agentcore" in _KNOWN_EXTENSIONS) + bedrock_agentcore_attributes: Dict[str, str] = _do_extract_bedrock_agentcore_attributes() + # Runtime attributes + self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_RUNTIME_ARN], _AGENTCORE_RUNTIME_ARN) + self.assertEqual( + bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], _AGENTCORE_RUNTIME_ENDPOINT_ARN + ) + self.assertEqual(bedrock_agentcore_attributes[GEN_AI_RUNTIME_ID], _AGENTCORE_RUNTIME_ID) + # Browser attributes + self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_BROWSER_ARN], _AGENTCORE_BROWSER_ARN) + self.assertEqual(bedrock_agentcore_attributes[GEN_AI_BROWSER_ID], _AGENTCORE_BROWSER_ID) + # Code interpreter attributes + self.assertEqual( + bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN], _AGENTCORE_CODE_INTERPRETER_ARN + ) + self.assertEqual(bedrock_agentcore_attributes[GEN_AI_CODE_INTERPRETER_ID], _AGENTCORE_CODE_INTERPRETER_ID) + # Gateway attributes + self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_GATEWAY_ARN], _AGENTCORE_GATEWAY_ARN) + self.assertEqual(bedrock_agentcore_attributes[GEN_AI_GATEWAY_ID], _AGENTCORE_GATEWAY_ID) + self.assertEqual(bedrock_agentcore_attributes[AWS_GATEWAY_TARGET_ID], _AGENTCORE_TARGET_ID) + # Memory attributes + self.assertEqual(bedrock_agentcore_attributes[GEN_AI_MEMORY_ID], _AGENTCORE_MEMORY_ID) + self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_MEMORY_ARN], _AGENTCORE_MEMORY_ARN) + # Auth and identity attributes + self.assertEqual( + bedrock_agentcore_attributes[AWS_AUTH_CREDENTIAL_PROVIDER_ARN], _AGENTCORE_CREDENTIAL_PROVIDER_ARN + ) + self.assertEqual( + bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN], _AGENTCORE_WORKLOAD_IDENTITY_ARN + ) + + # Bedrock AgentCore Control + self.assertTrue("bedrock-agentcore-control" in _KNOWN_EXTENSIONS) + # BedrockRuntime self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS) @@ -854,6 +929,31 @@ def _do_extract_lambda_attributes() -> Dict[str, str]: return _do_extract_attributes(service_name, params) +def _do_extract_bedrock_agentcore_attributes() -> Dict[str, str]: + service_name: str = "bedrock-agentcore" + params: Dict[str, Any] = { + "agentRuntimeArn": _AGENTCORE_RUNTIME_ARN, + "agentRuntimeEndpointArn": _AGENTCORE_RUNTIME_ENDPOINT_ARN, + "agentRuntimeId": _AGENTCORE_RUNTIME_ID, + "browserArn": _AGENTCORE_BROWSER_ARN, + "browserId": _AGENTCORE_BROWSER_ID, + "browserIdentifier": _AGENTCORE_BROWSER_ID, + "codeInterpreterArn": _AGENTCORE_CODE_INTERPRETER_ARN, + "codeInterpreterId": _AGENTCORE_CODE_INTERPRETER_ID, + "codeInterpreterIdentifier": _AGENTCORE_CODE_INTERPRETER_ID, + "gatewayArn": _AGENTCORE_GATEWAY_ARN, + "gatewayId": _AGENTCORE_GATEWAY_ID, + "gatewayIdentifier": _AGENTCORE_GATEWAY_ID, + "targetId": _AGENTCORE_TARGET_ID, + "memoryId": _AGENTCORE_MEMORY_ID, + "credentialProviderArn": _AGENTCORE_CREDENTIAL_PROVIDER_ARN, + "workloadIdentityArn": _AGENTCORE_WORKLOAD_IDENTITY_ARN, + "memory": {"arn": _AGENTCORE_MEMORY_ARN, "id": _AGENTCORE_MEMORY_ID}, + "workloadIdentityDetails": {"workloadIdentityArn": _AGENTCORE_WORKLOAD_IDENTITY_ARN}, + } + return _do_extract_attributes(service_name, params) + + def _do_extract_attributes(service_name: str, params: Dict[str, Any], operation: str = None) -> Dict[str, str]: mock_call_context: MagicMock = MagicMock() mock_call_context.params = params From 98d567a277137152f85a81c8aa9a603e79be7dcc Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Fri, 10 Oct 2025 15:45:38 -0700 Subject: [PATCH 02/23] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d39093671..e0f969c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,5 @@ For any change that affects end users of this package, please add an entry under If your change does not need a CHANGELOG entry, add the "skip changelog" label to your PR. ## Unreleased +- Add botocore instrumentation extension for Bedrock AgentCore services with span attributes + ([#490](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/490)) From 17716ac77041e75ac80efbf2e0c836012bf37fdb Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Fri, 10 Oct 2025 15:51:55 -0700 Subject: [PATCH 03/23] lint fix --- .../distro/patches/_bedrock_agentcore_patches.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py index 6cc8ef2ae..0e227b2f6 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py @@ -1,16 +1,5 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 from typing import Any, Dict from amazon.opentelemetry.distro._aws_attribute_keys import ( From 25f91ac73abc2f1dbe540ea966fe85bc423a84d2 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Fri, 10 Oct 2025 16:16:46 -0700 Subject: [PATCH 04/23] fix arn names --- .../distro/patches/test_instrumentation_patch.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 57eb7378f..89c65cb56 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -56,17 +56,19 @@ _LAMBDA_FUNCTION_NAME: str = "lambdaFunctionName" _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" _TABLE_ARN: str = "arn:aws:dynamodb:us-west-2:123456789012:table/testTable" -_AGENTCORE_RUNTIME_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime" +_AGENTCORE_RUNTIME_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123" _AGENTCORE_RUNTIME_ENDPOINT_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" _AGENTCORE_RUNTIME_ID: str = "test-runtime-123" -_AGENTCORE_BROWSER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/test-browser" +_AGENTCORE_BROWSER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/agentBrowser-123456789" _AGENTCORE_BROWSER_ID: str = "agentBrowser-123456789" -_AGENTCORE_CODE_INTERPRETER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/test-ci" +_AGENTCORE_CODE_INTERPRETER_ARN: str = ( + "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/agentCodeInterpreter-123456789" +) _AGENTCORE_CODE_INTERPRETER_ID: str = "agentCodeInterpreter-123456789" -_AGENTCORE_GATEWAY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/test-gateway" +_AGENTCORE_GATEWAY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/agentGateway-123456789" _AGENTCORE_GATEWAY_ID: str = "agentGateway-123456789" _AGENTCORE_TARGET_ID: str = "target-123456789" -_AGENTCORE_MEMORY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/test-memory" +_AGENTCORE_MEMORY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789" _AGENTCORE_MEMORY_ID: str = "agentMemory-123456789" _AGENTCORE_CREDENTIAL_PROVIDER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:credential-provider/test-cp" _AGENTCORE_WORKLOAD_IDENTITY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:workload-identity/test-wi" From 9e39da9e8cc0b3eff18ff9d35bce8b4029088f19 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Sun, 12 Oct 2025 21:36:43 -0700 Subject: [PATCH 05/23] fix arn namse in test --- .../distro/patches/test_instrumentation_patch.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 89c65cb56..979f79f7b 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -59,18 +59,20 @@ _AGENTCORE_RUNTIME_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123" _AGENTCORE_RUNTIME_ENDPOINT_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" _AGENTCORE_RUNTIME_ID: str = "test-runtime-123" -_AGENTCORE_BROWSER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/agentBrowser-123456789" -_AGENTCORE_BROWSER_ID: str = "agentBrowser-123456789" +_AGENTCORE_BROWSER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/testBrowser-1234567890" +_AGENTCORE_BROWSER_ID: str = "testBrowser-1234567890" _AGENTCORE_CODE_INTERPRETER_ARN: str = ( - "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/agentCodeInterpreter-123456789" + "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/testCodeInt-1234567890" ) -_AGENTCORE_CODE_INTERPRETER_ID: str = "agentCodeInterpreter-123456789" +_AGENTCORE_CODE_INTERPRETER_ID: str = "testCodeInt-1234567890" _AGENTCORE_GATEWAY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/agentGateway-123456789" _AGENTCORE_GATEWAY_ID: str = "agentGateway-123456789" _AGENTCORE_TARGET_ID: str = "target-123456789" _AGENTCORE_MEMORY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789" _AGENTCORE_MEMORY_ID: str = "agentMemory-123456789" -_AGENTCORE_CREDENTIAL_PROVIDER_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:credential-provider/test-cp" +_AGENTCORE_CREDENTIAL_PROVIDER_ARN: str = ( + "arn:aws:acps:us-east-1:123456789012:token-vault/test-vault/apikeycredentialprovider/test-provider" +) _AGENTCORE_WORKLOAD_IDENTITY_ARN: str = "arn:aws:bedrock-agentcore:us-east-1:123456789012:workload-identity/test-wi" # Patch names From 28c1fae33e4d0b31e1579354a58a06d9ae1f65be Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Wed, 15 Oct 2025 18:15:32 -0700 Subject: [PATCH 06/23] add app signals attribute translation --- .../distro/_aws_metric_attribute_generator.py | 131 +++++- .../test_aws_metric_attribute_generator.py | 378 +++++++++++------- .../applications/botocore/botocore_server.py | 100 ++++- .../applications/botocore/requirements.txt | 4 +- .../test/amazon/botocore/botocore_test.py | 66 +++ 5 files changed, 512 insertions(+), 167 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 88eedb152..7d173689d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -8,14 +8,22 @@ from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_AUTH_ACCESS_KEY, + AWS_AUTH_CREDENTIAL_PROVIDER_ARN, AWS_AUTH_REGION, AWS_BEDROCK_AGENT_ID, + AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN, + AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, + AWS_BEDROCK_AGENTCORE_MEMORY_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, AWS_BEDROCK_DATA_SOURCE_ID, AWS_BEDROCK_GUARDRAIL_ARN, AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, AWS_DYNAMODB_TABLE_ARN, + AWS_GATEWAY_TARGET_ID, AWS_KINESIS_STREAM_ARN, AWS_KINESIS_STREAM_NAME, AWS_LAMBDA_FUNCTION_ARN, @@ -63,6 +71,13 @@ SERVICE_METRIC, MetricAttributeGenerator, ) +from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( + GEN_AI_BROWSER_ID, + GEN_AI_CODE_INTERPRETER_ID, + GEN_AI_GATEWAY_ID, + GEN_AI_MEMORY_ID, + GEN_AI_RUNTIME_ID, +) from amazon.opentelemetry.distro.regional_resource_arn_parser import RegionalResourceArnParser from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser from opentelemetry.sdk.resources import Resource @@ -105,6 +120,7 @@ _NORMALIZED_SQS_SERVICE_NAME: str = "AWS::SQS" _NORMALIZED_BEDROCK_SERVICE_NAME: str = "AWS::Bedrock" _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME: str = "AWS::BedrockRuntime" +_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME: str = "AWS::BedrockAgentCore" _NORMALIZED_SECRETSMANAGER_SERVICE_NAME: str = "AWS::SecretsManager" _NORMALIZED_SNS_SERVICE_NAME: str = "AWS::SNS" _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME: str = "AWS::StepFunctions" @@ -118,6 +134,19 @@ # Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. _GRAPHQL: str = "graphql" +# AWS SDK service mapping for normalization +_AWS_SDK_SERVICE_MAPPING = { + "Bedrock Agent": _NORMALIZED_BEDROCK_SERVICE_NAME, + "Bedrock Agent Runtime": _NORMALIZED_BEDROCK_SERVICE_NAME, + "Bedrock Runtime": _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME, + "Bedrock AgentCore Control": _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME, + "Bedrock AgentCore": _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME, + "Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME, + "SNS": _NORMALIZED_SNS_SERVICE_NAME, + "SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME, + "Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME, +} + _logger: Logger = getLogger(__name__) @@ -327,16 +356,6 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str as the associated remote resource (Model) is not listed in Cloud Control. """ if is_aws_sdk_span(span): - aws_sdk_service_mapping = { - "Bedrock Agent": _NORMALIZED_BEDROCK_SERVICE_NAME, - "Bedrock Agent Runtime": _NORMALIZED_BEDROCK_SERVICE_NAME, - "Bedrock Runtime": _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME, - "Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME, - "SNS": _NORMALIZED_SNS_SERVICE_NAME, - "SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME, - "Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME, - } - # Special handling for Lambda invoke operations if _is_lambda_invoke_operation(span): lambda_function_name = span.attributes.get(AWS_LAMBDA_FUNCTION_NAME) @@ -345,7 +364,7 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str # is missing rather than falling back to a generic service name return lambda_function_name if lambda_function_name else UNKNOWN_REMOTE_SERVICE - return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name) + return _AWS_SDK_SERVICE_MAPPING.get(service_name, "AWS::" + service_name) return service_name @@ -466,6 +485,13 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri elif is_key_present(span, GEN_AI_REQUEST_MODEL): remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::Model" remote_resource_identifier = _escape_delimiters(span.attributes.get(GEN_AI_REQUEST_MODEL)) + elif ( + _AWS_SDK_SERVICE_MAPPING.get(str(span.attributes.get(_RPC_SERVICE))) + == _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME + ): + agentcore_type, agentcore_identifier = _get_agentcore_resource_type_and_identifier(span) + remote_resource_type = agentcore_type + remote_resource_identifier = _escape_delimiters(agentcore_identifier) if agentcore_identifier else None elif is_key_present(span, AWS_SECRETSMANAGER_SECRET_ARN): remote_resource_type = _NORMALIZED_SECRETSMANAGER_SERVICE_NAME + "::Secret" remote_resource_identifier = _escape_delimiters( @@ -674,6 +700,89 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu attributes[AWS_SPAN_KIND] = span_kind +def _get_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: + """Get BedrockAgentCore resource type and identifier based on span attributes.""" + + attrs = span.attributes + + # This check is not necessary but added to satisfy the linter. + if not attrs: + return None, None + + def extract_id_from_arn(arn: str) -> Optional[str]: + if not arn: + return None + parts = arn.split("/") + return parts[-1] if parts else None + + resource_type = None + resource_identifier = None + + # Browser + browser_id = attrs.get(GEN_AI_BROWSER_ID) + browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN) + if browser_id or browser_arn: + id_value = browser_id or extract_id_from_arn(str(browser_arn) if browser_arn else "") + resource_type = "Browser" if id_value == "aws.browser.v1" else "BrowserCustom" + resource_identifier = str(id_value) if id_value else None + + # Gateway + gateway_id = attrs.get(GEN_AI_GATEWAY_ID) + gateway_arn = attrs.get(AWS_BEDROCK_AGENTCORE_GATEWAY_ARN) + if gateway_id or gateway_arn: + gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) + if gateway_target_id: + resource_type = "GatewayTarget" + resource_identifier = str(gateway_target_id) + else: + resource_type = "Gateway" + resource_identifier = ( + str(gateway_id) if gateway_id else extract_id_from_arn(str(gateway_arn) if gateway_arn else "") + ) + + # Runtime + runtime_id = attrs.get(GEN_AI_RUNTIME_ID) + runtime_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ARN) + runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN) + if runtime_id or runtime_arn: + if runtime_endpoint_arn: + resource_type = "RuntimeEndpoint" + resource_identifier = str(runtime_endpoint_arn) + else: + resource_type = "Runtime" + resource_identifier = ( + str(runtime_id) if runtime_id else extract_id_from_arn(str(runtime_arn) if runtime_arn else "") + ) + + # Code interpreter + code_interpreter_id = attrs.get(GEN_AI_CODE_INTERPRETER_ID) + code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN) + if code_interpreter_id or code_interpreter_arn: + id_value = code_interpreter_id or extract_id_from_arn(str(code_interpreter_arn) if code_interpreter_arn else "") + resource_type = "CodeInterpreter" if id_value == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" + resource_identifier = str(id_value) if id_value else None + + # Identity + credential_arn = attrs.get(AWS_AUTH_CREDENTIAL_PROVIDER_ARN) + if credential_arn: + credential_arn_str = str(credential_arn) + if "apikeycredentialprovider" in credential_arn_str: + resource_type = "APIKeyCredentialProvider" + if "oauth2credentialprovider" in credential_arn_str: + resource_type = "OAuth2CredentialProvider" + resource_identifier = extract_id_from_arn(credential_arn_str) + + # Memory + memory_id = attrs.get(GEN_AI_MEMORY_ID) + memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) + if memory_id or memory_arn: + resource_type = "Memory" + resource_identifier = str(memory_arn) if memory_arn else None + + full_resource_type = f"{_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME}::{resource_type}" if resource_type else None + return full_resource_type, resource_identifier + + def _log_unknown_attribute(attribute_key: str, span: ReadableSpan) -> None: message: str = "No valid %s value found for %s span %s" if _logger.isEnabledFor(DEBUG): diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index abf03bba1..d02ecff86 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -12,6 +12,10 @@ AWS_AUTH_ACCESS_KEY, AWS_AUTH_REGION, AWS_BEDROCK_AGENT_ID, + AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + AWS_BEDROCK_AGENTCORE_MEMORY_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, AWS_BEDROCK_DATA_SOURCE_ID, AWS_BEDROCK_GUARDRAIL_ARN, AWS_BEDROCK_GUARDRAIL_ID, @@ -19,6 +23,7 @@ AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, AWS_CONSUMER_PARENT_SPAN_KIND, AWS_DYNAMODB_TABLE_ARN, + AWS_GATEWAY_TARGET_ID, AWS_KINESIS_STREAM_ARN, AWS_KINESIS_STREAM_NAME, AWS_LAMBDA_FUNCTION_ARN, @@ -45,6 +50,13 @@ ) from amazon.opentelemetry.distro._aws_metric_attribute_generator import _AwsMetricAttributeGenerator from amazon.opentelemetry.distro.metric_attribute_generator import DEPENDENCY_METRIC, SERVICE_METRIC +from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( + GEN_AI_BROWSER_ID, + GEN_AI_CODE_INTERPRETER_ID, + GEN_AI_GATEWAY_ID, + GEN_AI_MEMORY_ID, + GEN_AI_RUNTIME_ID, +) from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.resources import _DEFAULT_RESOURCE, SERVICE_NAME from opentelemetry.sdk.trace import ReadableSpan, Resource @@ -75,8 +87,7 @@ _GENERATOR: _AwsMetricAttributeGenerator = _AwsMetricAttributeGenerator() -# pylint: disable=too-many-public-methods -class TestAwsMetricAttributeGenerator(TestCase): +class TestUtil(TestCase): def setUp(self): self.attributes_mock: Attributes = MagicMock() self.instrumentation_scope_info_mock: InstrumentationScope = MagicMock() @@ -95,6 +106,150 @@ def setUp(self): # OTel strongly recommends to start out with the default instead of Resource.empty() self.resource: Resource = _DEFAULT_RESOURCE + def _mock_attribute( + self, + keys: List[str], + values: Optional[List[str]], + exist_keys: Optional[List[str]] = None, + exist_values: Optional[List[str]] = None, + ) -> (Optional[List[str]], Optional[List[str]]): + if exist_keys is not None and exist_values is not None: + for key in exist_keys: + if key not in keys: + keys = keys + [key] + values = values + [exist_values[exist_keys.index(key)]] + + def get_side_effect(get_key): + if get_key in keys: + return values[keys.index(get_key)] + return None + + self.attributes_mock.get.side_effect = get_side_effect + + return keys, values + + def _validate_expected_remote_attributes( + self, expected_remote_service: str, expected_remote_operation: str + ) -> None: + self.span_mock.kind = SpanKind.CLIENT + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes[AWS_REMOTE_SERVICE], expected_remote_service) + self.assertEqual(actual_attributes[AWS_REMOTE_OPERATION], expected_remote_operation) + + self.span_mock.kind = SpanKind.PRODUCER + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes[AWS_REMOTE_SERVICE], expected_remote_service) + self.assertEqual(actual_attributes[AWS_REMOTE_OPERATION], expected_remote_operation) + + def _validate_and_remove_remote_attributes( + self, + remote_service_key: str, + remote_service_value: str, + remote_operation_key: str, + remote_operation_value: str, + keys: Optional[List[str]], + values: Optional[List[str]], + ): + keys, values = self._mock_attribute( + [remote_service_key, remote_operation_key], [remote_service_value, remote_operation_value], keys, values + ) + self._validate_expected_remote_attributes(remote_service_value, remote_operation_value) + + keys, values = self._mock_attribute( + [remote_service_key, remote_operation_key], [None, remote_operation_value], keys, values + ) + self._validate_expected_remote_attributes(_UNKNOWN_REMOTE_SERVICE, remote_operation_value) + + keys, values = self._mock_attribute( + [remote_service_key, remote_operation_key], [remote_service_value, None], keys, values + ) + self._validate_expected_remote_attributes(remote_service_value, _UNKNOWN_REMOTE_OPERATION) + + keys, values = self._mock_attribute([remote_service_key, remote_operation_key], [None, None], keys, values) + return keys, values + + def _validate_remote_resource_attributes( + self, + expected_type: str, + expected_identifier: str, + expected_cfn_primary_id: str = None, + expected_region: str = None, + expected_account_id: str = None, + expected_access_key: str = None, + ) -> None: + # If expected_cfn_primary_id is not provided, it defaults to expected_identifier + if expected_cfn_primary_id is None: + expected_cfn_primary_id = expected_identifier + + # Client, Producer, and Consumer spans should generate the expected remote resource attribute + for kind in [SpanKind.CLIENT, SpanKind.PRODUCER, SpanKind.CONSUMER]: + self.span_mock.kind = kind + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(expected_type, actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE)) + self.assertEqual(expected_identifier, actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER)) + self.assertEqual(expected_cfn_primary_id, actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER)) + + # Cross account support + if expected_region is not None: + self.assertEqual(expected_region, actual_attributes.get(AWS_REMOTE_RESOURCE_REGION)) + else: + self.assertNotIn(AWS_REMOTE_RESOURCE_REGION, actual_attributes) + + if expected_access_key is not None: + self.assertEqual(expected_access_key, actual_attributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)) + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) + else: + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) + + if expected_account_id is not None: + self.assertEqual(expected_account_id, actual_attributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)) + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) + else: + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) + + # Server span should not generate remote resource attribute + self.span_mock.kind = SpanKind.SERVER + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + SERVICE_METRIC + ) + self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, actual_attributes) + self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, actual_attributes) + self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, actual_attributes) + self.assertNotIn(AWS_REMOTE_RESOURCE_REGION, actual_attributes) + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) + self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) + + self._mock_attribute([SpanAttributes.DB_SYSTEM], [None]) + + def _validate_attributes_produced_for_non_local_root_span_of_kind( + self, expected_attributes: Attributes, kind: SpanKind + ) -> None: + self.span_mock.kind = kind + + attribute_map: {str, BoundedAttributes} = _GENERATOR.generate_metric_attributes_dict_from_span( + self.span_mock, self.resource + ) + service_attributes: BoundedAttributes = attribute_map.get(SERVICE_METRIC) + dependency_attributes: BoundedAttributes = attribute_map.get(DEPENDENCY_METRIC) + if attribute_map is not None and len(attribute_map) > 0: + if kind in [SpanKind.PRODUCER, SpanKind.CLIENT, SpanKind.CONSUMER]: + self.assertIsNone(service_attributes) + self.assertEqual(len(dependency_attributes), len(BoundedAttributes(attributes=expected_attributes))) + self.assertEqual(dependency_attributes, BoundedAttributes(attributes=expected_attributes)) + else: + self.assertIsNone(dependency_attributes) + self.assertEqual(len(service_attributes), len(BoundedAttributes(attributes=expected_attributes))) + self.assertEqual(service_attributes, BoundedAttributes(attributes=expected_attributes)) + + +# pylint: disable=too-many-public-methods +class TestAwsMetricAttributeGenerator(TestUtil): def test_span_attributes_for_empty_resource(self): self.resource = Resource.get_empty() expected_attributes: Attributes = { @@ -897,6 +1052,8 @@ def test_normalize_remote_service_name_aws_sdk(self): self.validate_aws_sdk_service_normalization("Bedrock Agent", "AWS::Bedrock") self.validate_aws_sdk_service_normalization("Bedrock Agent Runtime", "AWS::Bedrock") self.validate_aws_sdk_service_normalization("Bedrock Runtime", "AWS::BedrockRuntime") + self.validate_aws_sdk_service_normalization("Bedrock AgentCore", "AWS::BedrockAgentCore") + self.validate_aws_sdk_service_normalization("Bedrock AgentCore Control", "AWS::BedrockAgentCore") self.validate_aws_sdk_service_normalization("Secrets Manager", "AWS::SecretsManager") self.validate_aws_sdk_service_normalization("SNS", "AWS::SNS") self.validate_aws_sdk_service_normalization("SFN", "AWS::StepFunctions") @@ -947,72 +1104,6 @@ def validate_aws_sdk_service_normalization(self, service_name: str, expected_rem def _update_resource_with_service_name(self) -> None: self.resource: Resource = Resource(attributes={SERVICE_NAME: _SERVICE_NAME_VALUE}) - def _mock_attribute( - self, - keys: List[str], - values: Optional[List[str]], - exist_keys: Optional[List[str]] = None, - exist_values: Optional[List[str]] = None, - ) -> (Optional[List[str]], Optional[List[str]]): - if exist_keys is not None and exist_values is not None: - for key in exist_keys: - if key not in keys: - keys = keys + [key] - values = values + [exist_values[exist_keys.index(key)]] - - def get_side_effect(get_key): - if get_key in keys: - return values[keys.index(get_key)] - return None - - self.attributes_mock.get.side_effect = get_side_effect - - return keys, values - - def _validate_expected_remote_attributes( - self, expected_remote_service: str, expected_remote_operation: str - ) -> None: - self.span_mock.kind = SpanKind.CLIENT - actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( - DEPENDENCY_METRIC - ) - self.assertEqual(actual_attributes[AWS_REMOTE_SERVICE], expected_remote_service) - self.assertEqual(actual_attributes[AWS_REMOTE_OPERATION], expected_remote_operation) - - self.span_mock.kind = SpanKind.PRODUCER - actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( - DEPENDENCY_METRIC - ) - self.assertEqual(actual_attributes[AWS_REMOTE_SERVICE], expected_remote_service) - self.assertEqual(actual_attributes[AWS_REMOTE_OPERATION], expected_remote_operation) - - def _validate_and_remove_remote_attributes( - self, - remote_service_key: str, - remote_service_value: str, - remote_operation_key: str, - remote_operation_value: str, - keys: Optional[List[str]], - values: Optional[List[str]], - ): - keys, values = self._mock_attribute( - [remote_service_key, remote_operation_key], [remote_service_value, remote_operation_value], keys, values - ) - self._validate_expected_remote_attributes(remote_service_value, remote_operation_value) - - keys, values = self._mock_attribute( - [remote_service_key, remote_operation_key], [None, remote_operation_value], keys, values - ) - self._validate_expected_remote_attributes(_UNKNOWN_REMOTE_SERVICE, remote_operation_value) - - keys, values = self._mock_attribute( - [remote_service_key, remote_operation_key], [remote_service_value, None], keys, values - ) - self._validate_expected_remote_attributes(remote_service_value, _UNKNOWN_REMOTE_OPERATION) - - keys, values = self._mock_attribute([remote_service_key, remote_operation_key], [None, None], keys, values) - return keys, values - def _validate_peer_service_does_override(self, remote_service_key: str) -> None: self._mock_attribute([remote_service_key, SpanAttributes.PEER_SERVICE], ["TestString", "PeerService"]) self.span_mock.kind = SpanKind.CLIENT @@ -1858,81 +1949,6 @@ def test_client_db_span_with_remote_resource_attributes(self): [None], ) - def _validate_remote_resource_attributes( - self, - expected_type: str, - expected_identifier: str, - expected_cfn_primary_id: str = None, - expected_region: str = None, - expected_account_id: str = None, - expected_access_key: str = None, - ) -> None: - # If expected_cfn_primary_id is not provided, it defaults to expected_identifier - if expected_cfn_primary_id is None: - expected_cfn_primary_id = expected_identifier - - # Client, Producer, and Consumer spans should generate the expected remote resource attribute - for kind in [SpanKind.CLIENT, SpanKind.PRODUCER, SpanKind.CONSUMER]: - self.span_mock.kind = kind - actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( - DEPENDENCY_METRIC - ) - self.assertEqual(expected_type, actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE)) - self.assertEqual(expected_identifier, actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER)) - self.assertEqual(expected_cfn_primary_id, actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER)) - - # Cross account support - if expected_region is not None: - self.assertEqual(expected_region, actual_attributes.get(AWS_REMOTE_RESOURCE_REGION)) - else: - self.assertNotIn(AWS_REMOTE_RESOURCE_REGION, actual_attributes) - - if expected_access_key is not None: - self.assertEqual(expected_access_key, actual_attributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)) - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) - else: - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) - - if expected_account_id is not None: - self.assertEqual(expected_account_id, actual_attributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)) - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) - else: - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) - - # Server span should not generate remote resource attribute - self.span_mock.kind = SpanKind.SERVER - actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( - SERVICE_METRIC - ) - self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, actual_attributes) - self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, actual_attributes) - self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, actual_attributes) - self.assertNotIn(AWS_REMOTE_RESOURCE_REGION, actual_attributes) - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCOUNT_ID, actual_attributes) - self.assertNotIn(AWS_REMOTE_RESOURCE_ACCESS_KEY, actual_attributes) - - self._mock_attribute([SpanAttributes.DB_SYSTEM], [None]) - - def _validate_attributes_produced_for_non_local_root_span_of_kind( - self, expected_attributes: Attributes, kind: SpanKind - ) -> None: - self.span_mock.kind = kind - - attribute_map: {str, BoundedAttributes} = _GENERATOR.generate_metric_attributes_dict_from_span( - self.span_mock, self.resource - ) - service_attributes: BoundedAttributes = attribute_map.get(SERVICE_METRIC) - dependency_attributes: BoundedAttributes = attribute_map.get(DEPENDENCY_METRIC) - if attribute_map is not None and len(attribute_map) > 0: - if kind in [SpanKind.PRODUCER, SpanKind.CLIENT, SpanKind.CONSUMER]: - self.assertIsNone(service_attributes) - self.assertEqual(len(dependency_attributes), len(BoundedAttributes(attributes=expected_attributes))) - self.assertEqual(dependency_attributes, BoundedAttributes(attributes=expected_attributes)) - else: - self.assertIsNone(dependency_attributes) - self.assertEqual(len(service_attributes), len(BoundedAttributes(attributes=expected_attributes))) - self.assertEqual(service_attributes, BoundedAttributes(attributes=expected_attributes)) - def test_set_remote_environment(self): """Test remote environment setting for Lambda invoke operations.""" keys = [] @@ -2102,3 +2118,79 @@ def test_cloudformation_primary_identifier_fallback_to_remote_resource_identifie ) keys, values = self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None], keys, values) + + def test_bedrock_agentcore_resource_attributes(self): + """Test Bedrock AgentCore resource attributes for all resource types.""" + + def validate_bedrock_agentcore_resource(attribute_keys, attribute_values, expected_type, expected_identifier): + keys = [SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE] + values = ["aws-api", "Bedrock AgentCore"] + self._mock_attribute(keys, values) + self.span_mock.kind = SpanKind.CLIENT + + self._mock_attribute(attribute_keys, attribute_values, keys, values) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE), expected_type) + self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER), expected_identifier) + self.assertEqual(actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER), expected_identifier) + self._mock_attribute(attribute_keys, [None] * len(attribute_keys)) + + validate_bedrock_agentcore_resource( + [GEN_AI_BROWSER_ID], ["aws.browser.v1"], "AWS::BedrockAgentCore::Browser", "aws.browser.v1" + ) + validate_bedrock_agentcore_resource( + [GEN_AI_BROWSER_ID], + ["testBrowser-1234567890"], + "AWS::BedrockAgentCore::BrowserCustom", + "testBrowser-1234567890", + ) + validate_bedrock_agentcore_resource( + [AWS_BEDROCK_AGENTCORE_BROWSER_ARN], + ["arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/testBrowser-1234567890"], + "AWS::BedrockAgentCore::BrowserCustom", + "testBrowser-1234567890", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_GATEWAY_ID, AWS_GATEWAY_TARGET_ID], + ["agentGateway-123456789", "target-123456789"], + "AWS::BedrockAgentCore::GatewayTarget", + "target-123456789", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_GATEWAY_ID], ["agentGateway-123456789"], "AWS::BedrockAgentCore::Gateway", "agentGateway-123456789" + ) + validate_bedrock_agentcore_resource( + [GEN_AI_RUNTIME_ID, AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], + ["test-runtime-123", "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint"], + "AWS::BedrockAgentCore::RuntimeEndpoint", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_RUNTIME_ID], ["test-runtime-123"], "AWS::BedrockAgentCore::Runtime", "test-runtime-123" + ) + validate_bedrock_agentcore_resource( + [AWS_BEDROCK_AGENTCORE_RUNTIME_ARN], + ["arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123"], + "AWS::BedrockAgentCore::Runtime", + "test-runtime-123", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_CODE_INTERPRETER_ID], + ["aws.codeinterpreter.v1"], + "AWS::BedrockAgentCore::CodeInterpreter", + "aws.codeinterpreter.v1", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_CODE_INTERPRETER_ID], + ["testCodeInt-1234567890"], + "AWS::BedrockAgentCore::CodeInterpreterCustom", + "testCodeInt-1234567890", + ) + validate_bedrock_agentcore_resource( + [GEN_AI_MEMORY_ID, AWS_BEDROCK_AGENTCORE_MEMORY_ARN], + ["agentMemory-123456789", "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789"], + "AWS::BedrockAgentCore::Memory", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789", + ) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 36bf87dbb..5b59eae95 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -24,6 +24,7 @@ _AWS_SDK_S3_ENDPOINT: str = os.environ.get("AWS_SDK_S3_ENDPOINT") _AWS_SDK_ENDPOINT: str = os.environ.get("AWS_SDK_ENDPOINT") _AWS_REGION: str = os.environ.get("AWS_REGION") +_AWS_ACCOUNT_ID: str = "123456789012" _ERROR_ENDPOINT: str = "http://error.test:8080" _FAULT_ENDPOINT: str = "http://fault.test:8080" os.environ.setdefault("AWS_ACCESS_KEY_ID", "testcontainers-localstack") @@ -47,7 +48,10 @@ def do_GET(self): if self.in_path("kinesis"): self._handle_kinesis_request() if self.in_path("bedrock"): - self._handle_bedrock_request() + if self.in_path("bedrock-agentcore"): + self._handle_bedrock_agentcore_request() + else: + self._handle_bedrock_request() if self.in_path("secretsmanager"): self._handle_secretsmanager_request() if self.in_path("stepfunctions"): @@ -104,6 +108,84 @@ def _handle_cross_account_request(self) -> None: else: set_main_status(404) + def _handle_bedrock_agentcore_request(self) -> None: + bedrock_agentcore_client = boto3.client( + "bedrock-agentcore", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION + ) + bedrock_agentcore_control_client = boto3.client( + "bedrock-agentcore-control", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION + ) + + if self.in_path("runtime"): + path_parts = self.path.split("/") + operation = path_parts[3] + + if operation == "invokeagentruntime": + agent_id = path_parts[4] + set_main_status(200) + bedrock_agentcore_client.meta.events.register( + "before-call.bedrock-agentcore.InvokeAgentRuntime", + inject_200_success, + ) + bedrock_agentcore_client.invoke_agent_runtime( + agentRuntimeArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:runtime/{agent_id}", + payload=b'{"message": "Hello, test message"}', + ) + return + elif operation == "createendpoint": + set_main_status(200) + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateAgentRuntimeEndpoint", + lambda **kwargs: inject_200_success( + agentRuntimeArn=( + f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:" + "runtime/completeAgent-w8slyU6q5M" + ), + agentRuntimeEndpointArn=( + f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:endpoint/invokeEndpoint" + ), + agentRuntimeId="completeAgent-w8slyU6q5M", + createdAt="2024-01-01T00:00:00Z", + endpointName="invokeEndpoint", + status="ACTIVE", + targetVersion="1.0", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_agent_runtime_endpoint( + agentRuntimeId="completeAgent-w8slyU6q5M", + name="invokeEndpoint", + description="Endpoint for invoking agent runtime", + ) + return + elif operation == "startbrowsersession": + browser_id = path_parts[4] + set_main_status(200) + bedrock_agentcore_client.meta.events.register( + "before-call.bedrock-agentcore.StartBrowserSession", + lambda **kwargs: inject_200_success( + browserIdentifier=browser_id, + createdAt="2024-01-01T00:00:00Z", + sessionId="testBrowserSession", + streams={ + "automationStream": { + "streamEndpoint": "wss://example.com/automation", + "streamStatus": "ENABLED", + }, + "liveViewStream": {"streamEndpoint": "wss://example.com/liveview"}, + }, + **kwargs, + ), + ) + bedrock_agentcore_client.start_browser_session( + browserIdentifier=browser_id, + name="testBrowserSession", + viewPort={"width": 1920, "height": 1080}, + ) + return + + set_main_status(404) + def _handle_s3_request(self) -> None: s3_client: BaseClient = boto3.client("s3", endpoint_url=_AWS_SDK_S3_ENDPOINT, region_name=_AWS_REGION) if self.in_path(_ERROR): @@ -650,6 +732,7 @@ def prepare_aws_server() -> None: def inject_200_success(**kwargs): + print(f"inject_200_success kwargs: {kwargs}") response_metadata = { "HTTPStatusCode": 200, "RequestId": "mock-request-id", @@ -660,20 +743,15 @@ def inject_200_success(**kwargs): "ResponseMetadata": response_metadata, } - guardrail_id = kwargs.get("guardrailId") - if guardrail_id is not None: - response_body["guardrailId"] = guardrail_id - guardrail_arn = kwargs.get("guardrailArn") - if guardrail_arn is not None: - response_body["guardrailArn"] = guardrail_arn - model_id = kwargs.get("modelId") - if model_id is not None: - response_body["modelId"] = model_id + for key, value in kwargs.items(): + if key not in ["headers", "body"]: + response_body[key] = value HTTPResponse = namedtuple("HTTPResponse", ["status_code", "headers", "body"]) headers = kwargs.get("headers", {}) body = kwargs.get("body", "") - response_body["body"] = body + if body: + response_body["body"] = body http_response = HTTPResponse(200, headers=headers, body=body) return http_response, response_body diff --git a/contract-tests/images/applications/botocore/requirements.txt b/contract-tests/images/applications/botocore/requirements.txt index 61ddebf98..22d198583 100644 --- a/contract-tests/images/applications/botocore/requirements.txt +++ b/contract-tests/images/applications/botocore/requirements.txt @@ -1,3 +1,3 @@ typing-extensions==4.12.2 -botocore==1.34.143 -boto3==1.34.143 +botocore==1.40.53 +boto3==1.40.53 diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index f5b5638ae..e6ba226e5 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -750,6 +750,72 @@ def test_bedrock_agent_get_data_source(self): span_name="Bedrock Agent.GetDataSource", ) + def test_bedrock_agentcore_invoke_agent_runtime(self): + self.do_test_requests( + "bedrock-agentcore/runtime/invokeagentruntime/myAgent-w8slyU6q5M", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="InvokeAgentRuntime", + remote_resource_type="AWS::BedrockAgentCore::Runtime", + remote_resource_identifier="myAgent-w8slyU6q5M", + cloudformation_primary_identifier="myAgent-w8slyU6q5M", + span_name="Bedrock AgentCore.InvokeAgentRuntime", + ) + + def test_bedrock_agentcore_create_agent_runtime_endpoint(self): + self.do_test_requests( + "bedrock-agentcore/runtime/createendpoint", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateAgentRuntimeEndpoint", + remote_resource_type="AWS::BedrockAgentCore::RuntimeEndpoint", + remote_resource_identifier=("arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint"), + cloudformation_primary_identifier=( + "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" + ), + span_name="Bedrock AgentCore Control.CreateAgentRuntimeEndpoint", + ) + + def test_bedrock_agentcore_start_browser_session(self): + self.do_test_requests( + "bedrock-agentcore/runtime/startbrowsersession/agentBrowser-qYkrpgjS2M", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartBrowserSession", + remote_resource_type="AWS::BedrockAgentCore::BrowserCustom", + remote_resource_identifier="agentBrowser-qYkrpgjS2M", + cloudformation_primary_identifier="agentBrowser-qYkrpgjS2M", + span_name="Bedrock AgentCore.StartBrowserSession", + ) + + def test_bedrock_agentcore_start_browser_session_v1(self): + self.do_test_requests( + "bedrock-agentcore/runtime/startbrowsersession/aws.browser.v1", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartBrowserSession", + remote_resource_type="AWS::BedrockAgentCore::Browser", + remote_resource_identifier="aws.browser.v1", + cloudformation_primary_identifier="aws.browser.v1", + span_name="Bedrock AgentCore.StartBrowserSession", + ) + def test_secretsmanager_describe_secret(self): self.do_test_requests( "secretsmanager/describesecret/my-secret", From 82bd89287c099f2b27cfc3998ed80fac67aa895b Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Wed, 15 Oct 2025 19:09:02 -0700 Subject: [PATCH 07/23] move gen ai constants to avoid botocore dependency --- .../distro/_aws_metric_attribute_generator.py | 2 +- .../distro/patches/_bedrock_agentcore_patches.py | 13 +++++++------ .../_incubating/attributes/gen_ai_attributes.py | 10 ++++++++++ .../distro/patches/test_instrumentation_patch.py | 10 +++++----- .../distro/test_aws_metric_attribute_generator.py | 2 +- 5 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 7d173689d..0e7784a57 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -71,7 +71,7 @@ SERVICE_METRIC, MetricAttributeGenerator, ) -from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( +from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, GEN_AI_GATEWAY_ID, diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py index 0e227b2f6..393d657d4 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_agentcore_patches.py @@ -13,6 +13,13 @@ AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, AWS_GATEWAY_TARGET_ID, ) +from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import ( + GEN_AI_BROWSER_ID, + GEN_AI_CODE_INTERPRETER_ID, + GEN_AI_GATEWAY_ID, + GEN_AI_MEMORY_ID, + GEN_AI_RUNTIME_ID, +) from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, @@ -21,12 +28,6 @@ ) from opentelemetry.trace.span import Span -GEN_AI_RUNTIME_ID = "gen_ai.runtime.id" -GEN_AI_BROWSER_ID = "gen_ai.browser.id" -GEN_AI_CODE_INTERPRETER_ID = "gen_ai.code_interpreter.id" -GEN_AI_MEMORY_ID = "gen_ai.memory.id" -GEN_AI_GATEWAY_ID = "gen_ai.gateway.id" - # Mapping of flattened JSON paths to attribute keys _ATTRIBUTE_MAPPING = { "agentRuntimeArn": AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py new file mode 100644 index 000000000..3571ee8bb --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py @@ -0,0 +1,10 @@ +""" +Utility module holding attribute keys for incubating Gen AI semantic conventions. +Remove this once we've contributed them to upstream. +""" + +GEN_AI_RUNTIME_ID = "gen_ai.runtime.id" +GEN_AI_BROWSER_ID = "gen_ai.browser.id" +GEN_AI_CODE_INTERPRETER_ID = "gen_ai.code_interpreter.id" +GEN_AI_MEMORY_ID = "gen_ai.memory.id" +GEN_AI_GATEWAY_ID = "gen_ai.gateway.id" diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 979f79f7b..2697ebea5 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -21,17 +21,17 @@ AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, AWS_GATEWAY_TARGET_ID, ) -from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( +from amazon.opentelemetry.distro.patches._instrumentation_patch import ( + AWS_GEVENT_PATCH_MODULES, + apply_instrumentation_patches, +) +from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, GEN_AI_GATEWAY_ID, GEN_AI_MEMORY_ID, GEN_AI_RUNTIME_ID, ) -from amazon.opentelemetry.distro.patches._instrumentation_patch import ( - AWS_GEVENT_PATCH_MODULES, - apply_instrumentation_patches, -) from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, bedrock_utils from opentelemetry.propagate import get_global_textmap diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index d02ecff86..0e34a60b8 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -50,7 +50,7 @@ ) from amazon.opentelemetry.distro._aws_metric_attribute_generator import _AwsMetricAttributeGenerator from amazon.opentelemetry.distro.metric_attribute_generator import DEPENDENCY_METRIC, SERVICE_METRIC -from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import ( +from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, GEN_AI_GATEWAY_ID, From 7dbabb5cba14f8b5db12091829e2564a7173e36f Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Wed, 15 Oct 2025 19:13:01 -0700 Subject: [PATCH 08/23] lint fix --- .../semconv/_incubating/attributes/gen_ai_attributes.py | 3 +++ .../images/applications/botocore/botocore_server.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py index 3571ee8bb..001234184 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py @@ -1,5 +1,8 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """ Utility module holding attribute keys for incubating Gen AI semantic conventions. + Remove this once we've contributed them to upstream. """ diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 5b59eae95..2452bd703 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -132,7 +132,7 @@ def _handle_bedrock_agentcore_request(self) -> None: payload=b'{"message": "Hello, test message"}', ) return - elif operation == "createendpoint": + if operation == "createendpoint": set_main_status(200) bedrock_agentcore_control_client.meta.events.register( "before-call.bedrock-agentcore-control.CreateAgentRuntimeEndpoint", @@ -158,7 +158,7 @@ def _handle_bedrock_agentcore_request(self) -> None: description="Endpoint for invoking agent runtime", ) return - elif operation == "startbrowsersession": + if operation == "startbrowsersession": browser_id = path_parts[4] set_main_status(200) bedrock_agentcore_client.meta.events.register( From e0b5fd64484effe5145834421dc6d45a88a664c6 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 11:33:32 -0700 Subject: [PATCH 09/23] linting fix --- .../distro/_aws_metric_attribute_generator.py | 1 + .../images/applications/botocore/botocore_server.py | 8 +++++--- .../tests/test/amazon/botocore/botocore_test.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 0e7784a57..97425f243 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -700,6 +700,7 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu attributes[AWS_SPAN_KIND] = span_kind +# pylint: disable=too-many-locals def _get_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: """Get BedrockAgentCore resource type and identifier based on span attributes.""" diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 2452bd703..c3151bc51 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -115,11 +115,12 @@ def _handle_bedrock_agentcore_request(self) -> None: bedrock_agentcore_control_client = boto3.client( "bedrock-agentcore-control", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION ) + # Bedrock AgentCore tests will have path of the following structure: + # /bedrock-agentcore/{service}/{operation}/{args} + path_parts = self.path.split("/") + operation = path_parts[3] if self.in_path("runtime"): - path_parts = self.path.split("/") - operation = path_parts[3] - if operation == "invokeagentruntime": agent_id = path_parts[4] set_main_status(200) @@ -158,6 +159,7 @@ def _handle_bedrock_agentcore_request(self) -> None: description="Endpoint for invoking agent runtime", ) return + if self.in_path("browser"): if operation == "startbrowsersession": browser_id = path_parts[4] set_main_status(200) diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index e6ba226e5..27f2e85d9 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -786,7 +786,7 @@ def test_bedrock_agentcore_create_agent_runtime_endpoint(self): def test_bedrock_agentcore_start_browser_session(self): self.do_test_requests( - "bedrock-agentcore/runtime/startbrowsersession/agentBrowser-qYkrpgjS2M", + "bedrock-agentcore/browser/startbrowsersession/agentBrowser-qYkrpgjS2M", "GET", 200, 0, @@ -802,7 +802,7 @@ def test_bedrock_agentcore_start_browser_session(self): def test_bedrock_agentcore_start_browser_session_v1(self): self.do_test_requests( - "bedrock-agentcore/runtime/startbrowsersession/aws.browser.v1", + "bedrock-agentcore/browser/startbrowsersession/aws.browser.v1", "GET", 200, 0, From 32f27b2e4e62e1ecd6569603313eb7c41546252d Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:04:51 -0700 Subject: [PATCH 10/23] add contract tests --- .../distro/_aws_metric_attribute_generator.py | 79 +++--- .../applications/botocore/botocore_server.py | 250 ++++++++++++++++-- .../test/amazon/botocore/botocore_test.py | 170 ++++++++++-- 3 files changed, 427 insertions(+), 72 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 97425f243..47b204210 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -706,16 +706,19 @@ def _get_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Opt attrs = span.attributes - # This check is not necessary but added to satisfy the linter. + # This check is not necessary but added to fix linter. if not attrs: return None, None - def extract_id_from_arn(arn: str) -> Optional[str]: + def extract_id_from_arn(arn: Optional[str]) -> Optional[str]: if not arn: return None parts = arn.split("/") return parts[-1] if parts else None + def format_resource_type(resource_type: Optional[str]) -> Optional[str]: + return f"{_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME}::{resource_type}" if resource_type else None + resource_type = None resource_identifier = None @@ -723,45 +726,57 @@ def extract_id_from_arn(arn: str) -> Optional[str]: browser_id = attrs.get(GEN_AI_BROWSER_ID) browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN) if browser_id or browser_arn: - id_value = browser_id or extract_id_from_arn(str(browser_arn) if browser_arn else "") - resource_type = "Browser" if id_value == "aws.browser.v1" else "BrowserCustom" - resource_identifier = str(id_value) if id_value else None + if browser_arn: + resource_identifier = extract_id_from_arn(str(browser_arn)) if browser_arn else None + if browser_id: + resource_identifier = str(browser_id) + resource_type = "Browser" if resource_identifier == "aws.browser.v1" else "BrowserCustom" + return format_resource_type(resource_type), resource_identifier # Gateway gateway_id = attrs.get(GEN_AI_GATEWAY_ID) gateway_arn = attrs.get(AWS_BEDROCK_AGENTCORE_GATEWAY_ARN) - if gateway_id or gateway_arn: - gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) - if gateway_target_id: - resource_type = "GatewayTarget" - resource_identifier = str(gateway_target_id) - else: - resource_type = "Gateway" - resource_identifier = ( - str(gateway_id) if gateway_id else extract_id_from_arn(str(gateway_arn) if gateway_arn else "") - ) + gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) + if gateway_target_id: + resource_type = "GatewayTarget" + resource_identifier = str(gateway_target_id) + return format_resource_type(resource_type), resource_identifier + if gateway_arn or gateway_id: + if gateway_arn: + resource_identifier = extract_id_from_arn(str(gateway_arn)) if gateway_arn else None + if gateway_id: + resource_identifier = str(gateway_id) + resource_type = "Gateway" + return format_resource_type(resource_type), resource_identifier # Runtime runtime_id = attrs.get(GEN_AI_RUNTIME_ID) runtime_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ARN) runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN) - if runtime_id or runtime_arn: - if runtime_endpoint_arn: - resource_type = "RuntimeEndpoint" - resource_identifier = str(runtime_endpoint_arn) - else: - resource_type = "Runtime" - resource_identifier = ( - str(runtime_id) if runtime_id else extract_id_from_arn(str(runtime_arn) if runtime_arn else "") - ) + if runtime_endpoint_arn: + resource_type = "RuntimeEndpoint" + resource_identifier = str(runtime_endpoint_arn) + return format_resource_type(resource_type), resource_identifier + if runtime_arn or runtime_id: + if runtime_arn: + resource_identifier = extract_id_from_arn(str(runtime_arn)) if runtime_arn else None + if runtime_id: + resource_identifier = str(runtime_id) + resource_type = "Runtime" + return format_resource_type(resource_type), resource_identifier # Code interpreter code_interpreter_id = attrs.get(GEN_AI_CODE_INTERPRETER_ID) code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN) if code_interpreter_id or code_interpreter_arn: - id_value = code_interpreter_id or extract_id_from_arn(str(code_interpreter_arn) if code_interpreter_arn else "") - resource_type = "CodeInterpreter" if id_value == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" - resource_identifier = str(id_value) if id_value else None + if code_interpreter_arn: + resource_identifier = extract_id_from_arn(str(code_interpreter_arn)) if code_interpreter_arn else None + if code_interpreter_id: + resource_identifier = str(code_interpreter_id) + resource_type = ( + "CodeInterpreter" if resource_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" + ) + return format_resource_type(resource_type), resource_identifier # Identity credential_arn = attrs.get(AWS_AUTH_CREDENTIAL_PROVIDER_ARN) @@ -772,16 +787,20 @@ def extract_id_from_arn(arn: str) -> Optional[str]: if "oauth2credentialprovider" in credential_arn_str: resource_type = "OAuth2CredentialProvider" resource_identifier = extract_id_from_arn(credential_arn_str) + return format_resource_type(resource_type), resource_identifier # Memory memory_id = attrs.get(GEN_AI_MEMORY_ID) memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) if memory_id or memory_arn: resource_type = "Memory" - resource_identifier = str(memory_arn) if memory_arn else None + if memory_arn: + resource_identifier = extract_id_from_arn(str(memory_arn)) if memory_arn else None + if memory_id: + resource_identifier = str(memory_id) + return format_resource_type(resource_type), resource_identifier - full_resource_type = f"{_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME}::{resource_type}" if resource_type else None - return full_resource_type, resource_identifier + return None, None def _log_unknown_attribute(attribute_key: str, span: ReadableSpan) -> None: diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index c3151bc51..66a0789a5 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -7,6 +7,7 @@ from collections import namedtuple from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from io import BytesIO +from pathlib import PurePath from threading import Thread import boto3 @@ -115,37 +116,60 @@ def _handle_bedrock_agentcore_request(self) -> None: bedrock_agentcore_control_client = boto3.client( "bedrock-agentcore-control", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION ) - # Bedrock AgentCore tests will have path of the following structure: - # /bedrock-agentcore/{service}/{operation}/{args} - path_parts = self.path.split("/") - operation = path_parts[3] - - if self.in_path("runtime"): - if operation == "invokeagentruntime": - agent_id = path_parts[4] - set_main_status(200) - bedrock_agentcore_client.meta.events.register( - "before-call.bedrock-agentcore.InvokeAgentRuntime", - inject_200_success, + # Parse URL structure: /bedrock-agentcore/{service}/{operation}/{resource_id} + path = PurePath(self.path) + path_parts = path.parts[1:] # Remove leading '/' + + service = path_parts[1] if len(path_parts) > 1 else None + operation = path_parts[2] if len(path_parts) > 2 else None + resource_id = path_parts[3] if len(path_parts) > 3 else None + + set_main_status(200) + if service == "runtime": + agent_id = resource_id + if operation == "createagentruntime": + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateAgentRuntime", + lambda **kwargs: inject_200_success( + agentRuntimeArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:runtime/{agent_id}", + agentRuntimeId=agent_id, + agentRuntimeVersion="1.0", + createdAt="2024-01-01T00:00:00Z", + status="ACTIVE", + workloadIdentityDetails={ + "workloadIdentityArn": ( + f"arn:aws:iam::{_AWS_ACCOUNT_ID}:role/service-role/" + "AmazonBedrockAgentCoreRuntimeDefaultServiceRole" + ) + }, + **kwargs, + ), ) - bedrock_agentcore_client.invoke_agent_runtime( - agentRuntimeArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:runtime/{agent_id}", - payload=b'{"message": "Hello, test message"}', + bedrock_agentcore_control_client.create_agent_runtime( + agentRuntimeName="completeAgent", + description="Complete agent with all components", + agentRuntimeArtifact={ + "containerConfiguration": { + "containerUri": f"{_AWS_ACCOUNT_ID}.dkr.ecr.{_AWS_REGION}.amazonaws.com/test-agent:latest" + } + }, + roleArn=( + f"arn:aws:iam::{_AWS_ACCOUNT_ID}:role/service-role/" + "AmazonBedrockAgentCoreRuntimeDefaultServiceRole" + ), + networkConfiguration={"networkMode": "PUBLIC"}, + protocolConfiguration={"serverProtocol": "HTTP"}, ) return if operation == "createendpoint": - set_main_status(200) bedrock_agentcore_control_client.meta.events.register( "before-call.bedrock-agentcore-control.CreateAgentRuntimeEndpoint", lambda **kwargs: inject_200_success( - agentRuntimeArn=( - f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:" - "runtime/completeAgent-w8slyU6q5M" - ), + agentRuntimeArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:runtime/{agent_id}", agentRuntimeEndpointArn=( f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:endpoint/invokeEndpoint" ), - agentRuntimeId="completeAgent-w8slyU6q5M", + agentRuntimeId=agent_id, createdAt="2024-01-01T00:00:00Z", endpointName="invokeEndpoint", status="ACTIVE", @@ -154,15 +178,24 @@ def _handle_bedrock_agentcore_request(self) -> None: ), ) bedrock_agentcore_control_client.create_agent_runtime_endpoint( - agentRuntimeId="completeAgent-w8slyU6q5M", + agentRuntimeId=agent_id, name="invokeEndpoint", description="Endpoint for invoking agent runtime", ) return - if self.in_path("browser"): + if operation == "invokeagentruntime": + bedrock_agentcore_client.meta.events.register( + "before-call.bedrock-agentcore.InvokeAgentRuntime", + inject_200_success, + ) + bedrock_agentcore_client.invoke_agent_runtime( + agentRuntimeArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:runtime/{agent_id}", + payload=b'{"message": "Hello, test message"}', + ) + return + if service == "browser": if operation == "startbrowsersession": - browser_id = path_parts[4] - set_main_status(200) + browser_id = resource_id bedrock_agentcore_client.meta.events.register( "before-call.bedrock-agentcore.StartBrowserSession", lambda **kwargs: inject_200_success( @@ -185,6 +218,172 @@ def _handle_bedrock_agentcore_request(self) -> None: viewPort={"width": 1920, "height": 1080}, ) return + if service == "codeinterpreter": + if operation == "startcodeinterpretersession": + code_interpreter_id = resource_id + bedrock_agentcore_client.meta.events.register( + "before-call.bedrock-agentcore.StartCodeInterpreterSession", + lambda **kwargs: inject_200_success( + codeInterpreterIdentifier=code_interpreter_id, + createdAt="2024-01-01T00:00:00Z", + sessionId="testCodeInterpreterSession", + **kwargs, + ), + ) + bedrock_agentcore_client.start_code_interpreter_session( + codeInterpreterIdentifier=code_interpreter_id, + ) + return + if service == "memory": + if operation == "createevent": + memory_id = resource_id + bedrock_agentcore_client.meta.events.register( + "before-call.bedrock-agentcore.CreateEvent", + lambda **kwargs: inject_200_success( + memoryId=memory_id, + eventId="test-event-123", + createdAt="2024-01-01T00:00:00Z", + **kwargs, + ), + ) + bedrock_agentcore_client.create_event( + memoryId=memory_id, + actorId="test-actor-123", + eventTimestamp=1704067200, + payload=[ + { + "conversational": { + "content": {"text": "Test memory event for testing"}, + "role": "USER", + } + } + ], + ) + return + if service == "gateway": + if operation == "creategateway": + gateway_id = resource_id + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateGateway", + lambda **kwargs: inject_200_success( + gatewayId=gateway_id, + gatewayArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:gateway/{gateway_id}", + name="agentGateway", + status="ACTIVE", + createdAt="2024-01-01T00:00:00Z", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_gateway( + name="workingGateway", + description="Working test gateway", + authorizerType="CUSTOM_JWT", + protocolType="MCP", + roleArn=( + f"arn:aws:iam::{_AWS_ACCOUNT_ID}:role/service-role/" + "AmazonBedrockAgentCoreRuntimeDefaultServiceRole-swsx9" + ), + authorizerConfiguration={ + "customJWTAuthorizer": { + "discoveryUrl": "https://example.com/.well-known/openid-configuration", + "allowedAudience": ["test-audience"], + "allowedClients": ["test-client"], + } + }, + ) + return + if operation == "creategatewaytarget": + gateway_id = resource_id + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateGatewayTarget", + lambda **kwargs: inject_200_success( + targetId="testTarget-123", + gatewayArn=f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:gateway/{gateway_id}", + name="testTarget", + status="ACTIVE", + createdAt="2024-01-01T00:00:00Z", + description="Test gateway target", + lastSynchronizedAt="2024-01-01T00:00:00Z", + updatedAt="2024-01-01T00:00:00Z", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_gateway_target( + gatewayIdentifier=gateway_id, + name="testTarget", + description="Test gateway target", + targetConfiguration={ + "mcp": { + "openApiSchema": { + "inlinePayload": ( + '{"openapi": "3.0.0", "info": ' '{"title": "Test API", "version": "1.0.0"}}' + ) + } + } + }, + credentialProviderConfigurations=[ + { + "credentialProviderType": "API_KEY", + "credentialProvider": { + "apiKeyCredentialProvider": { + "providerArn": ( + f"arn:aws:bedrock:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:" + "api-key-credential-provider/test-provider" + ), + "credentialParameterName": "api-key", + "credentialLocation": "HEADER", + } + }, + } + ], + ) + return + if service == "identity": + if operation == "createoauth2credentialprovider": + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateOauth2CredentialProvider", + lambda **kwargs: inject_200_success( + credentialProviderId="test-oauth2-provider-123", + credentialProviderArn=( + f"arn:aws:acps:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:" + "token-vault/default/oauth2credentialprovider/test-oauth2-provider-123" + ), + name="testOAuth2Provider", + status="ACTIVE", + createdAt="2024-01-01T00:00:00Z", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_oauth2_credential_provider( + name="testOAuth2Provider", + credentialProviderVendor="CUSTOM", + oauth2ProviderConfigInput={ + "customOauth2ProviderConfig": { + "oauthDiscovery": {"discoveryUrl": "https://example.com/.well-known/openid-configuration"}, + "clientId": "test-client-id", + "clientSecret": "test-client-secret", + } + }, + ) + return + if operation == "createapikeycredentialprovider": + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateApiKeyCredentialProvider", + lambda **kwargs: inject_200_success( + credentialProviderId="test-apikey-provider-123", + credentialProviderArn=( + f"arn:aws:acps:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:" + "token-vault/default/apikeycredentialprovider/test-apikey-provider-123" + ), + name="testAPIKeyProvider", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_api_key_credential_provider( + name="testAPIKeyProvider", + apiKey="test-api-key-value", + ) + return set_main_status(404) @@ -734,7 +933,6 @@ def prepare_aws_server() -> None: def inject_200_success(**kwargs): - print(f"inject_200_success kwargs: {kwargs}") response_metadata = { "HTTPStatusCode": 200, "RequestId": "mock-request-id", diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index 27f2e85d9..6254b85c4 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -750,25 +750,27 @@ def test_bedrock_agent_get_data_source(self): span_name="Bedrock Agent.GetDataSource", ) - def test_bedrock_agentcore_invoke_agent_runtime(self): + def test_bedrock_agentcore_create_agent_runtime(self): + expected_identifier = "myAgent-w8slyU6q5M" self.do_test_requests( - "bedrock-agentcore/runtime/invokeagentruntime/myAgent-w8slyU6q5M", + "bedrock-agentcore/runtime/createagentruntime/myAgent-w8slyU6q5M", "GET", 200, 0, 0, - rpc_service="Bedrock AgentCore", + rpc_service="Bedrock AgentCore Control", remote_service="AWS::BedrockAgentCore", - remote_operation="InvokeAgentRuntime", + remote_operation="CreateAgentRuntime", remote_resource_type="AWS::BedrockAgentCore::Runtime", - remote_resource_identifier="myAgent-w8slyU6q5M", - cloudformation_primary_identifier="myAgent-w8slyU6q5M", - span_name="Bedrock AgentCore.InvokeAgentRuntime", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore Control.CreateAgentRuntime", ) def test_bedrock_agentcore_create_agent_runtime_endpoint(self): + expected_identifier = "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" self.do_test_requests( - "bedrock-agentcore/runtime/createendpoint", + "bedrock-agentcore/runtime/createendpoint/myAgent-w8slyU6q5M", "GET", 200, 0, @@ -777,14 +779,30 @@ def test_bedrock_agentcore_create_agent_runtime_endpoint(self): remote_service="AWS::BedrockAgentCore", remote_operation="CreateAgentRuntimeEndpoint", remote_resource_type="AWS::BedrockAgentCore::RuntimeEndpoint", - remote_resource_identifier=("arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint"), - cloudformation_primary_identifier=( - "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" - ), + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, span_name="Bedrock AgentCore Control.CreateAgentRuntimeEndpoint", ) + def test_bedrock_agentcore_invoke_agent_runtime(self): + expected_identifier = "myAgent-w8slyU6q5M" + self.do_test_requests( + "bedrock-agentcore/runtime/invokeagentruntime/myAgent-w8slyU6q5M", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="InvokeAgentRuntime", + remote_resource_type="AWS::BedrockAgentCore::Runtime", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore.InvokeAgentRuntime", + ) + def test_bedrock_agentcore_start_browser_session(self): + expected_identifier = "agentBrowser-qYkrpgjS2M" self.do_test_requests( "bedrock-agentcore/browser/startbrowsersession/agentBrowser-qYkrpgjS2M", "GET", @@ -795,12 +813,13 @@ def test_bedrock_agentcore_start_browser_session(self): remote_service="AWS::BedrockAgentCore", remote_operation="StartBrowserSession", remote_resource_type="AWS::BedrockAgentCore::BrowserCustom", - remote_resource_identifier="agentBrowser-qYkrpgjS2M", - cloudformation_primary_identifier="agentBrowser-qYkrpgjS2M", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, span_name="Bedrock AgentCore.StartBrowserSession", ) def test_bedrock_agentcore_start_browser_session_v1(self): + expected_identifier = "aws.browser.v1" self.do_test_requests( "bedrock-agentcore/browser/startbrowsersession/aws.browser.v1", "GET", @@ -811,11 +830,130 @@ def test_bedrock_agentcore_start_browser_session_v1(self): remote_service="AWS::BedrockAgentCore", remote_operation="StartBrowserSession", remote_resource_type="AWS::BedrockAgentCore::Browser", - remote_resource_identifier="aws.browser.v1", - cloudformation_primary_identifier="aws.browser.v1", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, span_name="Bedrock AgentCore.StartBrowserSession", ) + def test_bedrock_agentcore_start_code_interpreter_session(self): + expected_identifier = "agentCodeInterpreter-m9Mvuwkg6j" + self.do_test_requests( + "bedrock-agentcore/codeinterpreter/startcodeinterpretersession/agentCodeInterpreter-m9Mvuwkg6j", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartCodeInterpreterSession", + remote_resource_type="AWS::BedrockAgentCore::CodeInterpreterCustom", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore.StartCodeInterpreterSession", + ) + + def test_bedrock_agentcore_start_code_interpreter_session_v1(self): + expected_identifier = "aws.codeinterpreter.v1" + self.do_test_requests( + "bedrock-agentcore/codeinterpreter/startcodeinterpretersession/aws.codeinterpreter.v1", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartCodeInterpreterSession", + remote_resource_type="AWS::BedrockAgentCore::CodeInterpreter", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore.StartCodeInterpreterSession", + ) + + def test_bedrock_agentcore_create_event(self): + expected_identifier = "agentMemory-1mtr9C5pGt" + self.do_test_requests( + "bedrock-agentcore/memory/createevent/agentMemory-1mtr9C5pGt", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateEvent", + remote_resource_type="AWS::BedrockAgentCore::Memory", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore.CreateEvent", + ) + + def test_bedrock_agentcore_create_gateway(self): + expected_identifier = "agentGateway-k8Nt2pLm9Q" + self.do_test_requests( + "bedrock-agentcore/gateway/creategateway/agentGateway-k8Nt2pLm9Q", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateGateway", + remote_resource_type="AWS::BedrockAgentCore::Gateway", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore Control.CreateGateway", + ) + + def test_bedrock_agentcore_create_gateway_target(self): + expected_identifier = "testTarget-123" + self.do_test_requests( + "bedrock-agentcore/gateway/creategatewaytarget/workinggateway-oersefsjga", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateGatewayTarget", + remote_resource_type="AWS::BedrockAgentCore::GatewayTarget", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore Control.CreateGatewayTarget", + ) + + def test_bedrock_agentcore_create_oauth2_credential_provider(self): + expected_identifier = "test-oauth2-provider-123" + self.do_test_requests( + "bedrock-agentcore/identity/createoauth2credentialprovider", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateOauth2CredentialProvider", + remote_resource_type="AWS::BedrockAgentCore::OAuth2CredentialProvider", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore Control.CreateOauth2CredentialProvider", + ) + + def test_bedrock_agentcore_create_api_key_credential_provider(self): + expected_identifier = "test-apikey-provider-123" + self.do_test_requests( + "bedrock-agentcore/identity/createapikeycredentialprovider", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateApiKeyCredentialProvider", + remote_resource_type="AWS::BedrockAgentCore::APIKeyCredentialProvider", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=expected_identifier, + span_name="Bedrock AgentCore Control.CreateApiKeyCredentialProvider", + ) + def test_secretsmanager_describe_secret(self): self.do_test_requests( "secretsmanager/describesecret/my-secret", From 2fb78950b48e83fdfddb9c558007fa17bd9da26c Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:07:14 -0700 Subject: [PATCH 11/23] lint fix --- contract-tests/images/applications/botocore/botocore_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 66a0789a5..91acfbaa9 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -109,6 +109,7 @@ def _handle_cross_account_request(self) -> None: else: set_main_status(404) + # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements def _handle_bedrock_agentcore_request(self) -> None: bedrock_agentcore_client = boto3.client( "bedrock-agentcore", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION From 7968b5bcafa04518b48fdf45b244e9ddbcbbc60e Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:16:15 -0700 Subject: [PATCH 12/23] add Changelog.md --- CHANGELOG.md | 2 ++ .../distro/_aws_metric_attribute_generator.py | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f969c13..3cdba4162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,5 +11,7 @@ For any change that affects end users of this package, please add an entry under If your change does not need a CHANGELOG entry, add the "skip changelog" label to your PR. ## Unreleased +- Add Application Signals Attributes for Bedrock AgentCore spans + ([#495](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/495)) - Add botocore instrumentation extension for Bedrock AgentCore services with span attributes ([#490](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/490)) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 47b204210..661093dc5 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -790,14 +790,10 @@ def format_resource_type(resource_type: Optional[str]) -> Optional[str]: return format_resource_type(resource_type), resource_identifier # Memory - memory_id = attrs.get(GEN_AI_MEMORY_ID) memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) - if memory_id or memory_arn: + if memory_arn: resource_type = "Memory" - if memory_arn: - resource_identifier = extract_id_from_arn(str(memory_arn)) if memory_arn else None - if memory_id: - resource_identifier = str(memory_id) + resource_identifier = str(memory_arn) if memory_arn else None return format_resource_type(resource_type), resource_identifier return None, None From 77031e69715c7500765c2a9c397a670291b3159d Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:18:01 -0700 Subject: [PATCH 13/23] lint fix --- .../opentelemetry/distro/_aws_metric_attribute_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 661093dc5..b9601ed13 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -75,7 +75,6 @@ GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, GEN_AI_GATEWAY_ID, - GEN_AI_MEMORY_ID, GEN_AI_RUNTIME_ID, ) from amazon.opentelemetry.distro.regional_resource_arn_parser import RegionalResourceArnParser From f51f9979a96ce19bf63a12dad3b0957ab9947c8f Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:20:04 -0700 Subject: [PATCH 14/23] lint fix --- .../opentelemetry/distro/_aws_metric_attribute_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index b9601ed13..279cc5a73 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -699,7 +699,7 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu attributes[AWS_SPAN_KIND] = span_kind -# pylint: disable=too-many-locals +# pylint: disable=too-many-locals,too-many-return-statements def _get_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: """Get BedrockAgentCore resource type and identifier based on span attributes.""" From 6a54c9575e025de2d496d2a81c3824073dc0415e Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 15:26:22 -0700 Subject: [PATCH 15/23] rename function --- .../opentelemetry/distro/_aws_metric_attribute_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 279cc5a73..fc51080a4 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -488,7 +488,7 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri _AWS_SDK_SERVICE_MAPPING.get(str(span.attributes.get(_RPC_SERVICE))) == _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME ): - agentcore_type, agentcore_identifier = _get_agentcore_resource_type_and_identifier(span) + agentcore_type, agentcore_identifier = _get_bedrock_agentcore_resource_type_and_identifier(span) remote_resource_type = agentcore_type remote_resource_identifier = _escape_delimiters(agentcore_identifier) if agentcore_identifier else None elif is_key_present(span, AWS_SECRETSMANAGER_SECRET_ARN): @@ -700,7 +700,7 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu # pylint: disable=too-many-locals,too-many-return-statements -def _get_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: +def _get_bedrock_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: """Get BedrockAgentCore resource type and identifier based on span attributes.""" attrs = span.attributes From 37c398e1d20ec0601d77859d5f9e882b70aec9dd Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 19:09:45 -0700 Subject: [PATCH 16/23] fix cfn primary identifier parsing --- .../distro/_aws_metric_attribute_generator.py | 136 +++++--- .../test_aws_metric_attribute_generator.py | 322 ++++++++++++++---- .../applications/botocore/botocore_server.py | 23 ++ .../test/amazon/botocore/botocore_test.py | 30 +- 4 files changed, 388 insertions(+), 123 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index fc51080a4..c5bf4d07c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -75,6 +75,7 @@ GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, GEN_AI_GATEWAY_ID, + GEN_AI_MEMORY_ID, GEN_AI_RUNTIME_ID, ) from amazon.opentelemetry.distro.regional_resource_arn_parser import RegionalResourceArnParser @@ -488,9 +489,12 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri _AWS_SDK_SERVICE_MAPPING.get(str(span.attributes.get(_RPC_SERVICE))) == _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME ): - agentcore_type, agentcore_identifier = _get_bedrock_agentcore_resource_type_and_identifier(span) + agentcore_type, agentcore_identifier, agentcore_cfn_id = ( + _get_bedrock_agentcore_resource_type_and_identifier(span) + ) remote_resource_type = agentcore_type remote_resource_identifier = _escape_delimiters(agentcore_identifier) if agentcore_identifier else None + cloudformation_primary_identifier = _escape_delimiters(agentcore_cfn_id) if agentcore_cfn_id else None elif is_key_present(span, AWS_SECRETSMANAGER_SECRET_ARN): remote_resource_type = _NORMALIZED_SECRETSMANAGER_SERVICE_NAME + "::Secret" remote_resource_identifier = _escape_delimiters( @@ -699,103 +703,147 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu attributes[AWS_SPAN_KIND] = span_kind -# pylint: disable=too-many-locals,too-many-return-statements -def _get_bedrock_agentcore_resource_type_and_identifier(span: ReadableSpan) -> tuple[Optional[str], Optional[str]]: - """Get BedrockAgentCore resource type and identifier based on span attributes.""" - +def _get_bedrock_agentcore_resource_type_and_identifier( + span: ReadableSpan, +) -> tuple[Optional[str], Optional[str], Optional[str]]: + """Get BedrockAgentCore resource type, identifier, and CFN primary identifier based on span attributes.""" attrs = span.attributes - - # This check is not necessary but added to fix linter. if not attrs: - return None, None - - def extract_id_from_arn(arn: Optional[str]) -> Optional[str]: - if not arn: - return None - parts = arn.split("/") - return parts[-1] if parts else None + return None, None, None def format_resource_type(resource_type: Optional[str]) -> Optional[str]: return f"{_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME}::{resource_type}" if resource_type else None - resource_type = None - resource_identifier = None + for handler in [ + _handle_browser_attrs, + _handle_gateway_attrs, + _handle_runtime_attrs, + _handle_code_interpreter_attrs, + _handle_identity_attrs, + _handle_memory_attrs, + ]: + resource_type, resource_identifier, cfn_primary_identifier = handler(attrs) + if resource_type: + return format_resource_type(resource_type), resource_identifier, cfn_primary_identifier - # Browser + return None, None, None + + +def _handle_browser_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: browser_id = attrs.get(GEN_AI_BROWSER_ID) browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN) if browser_id or browser_arn: + resource_identifier = None if browser_arn: - resource_identifier = extract_id_from_arn(str(browser_arn)) if browser_arn else None + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(browser_arn)) if browser_id: resource_identifier = str(browser_id) resource_type = "Browser" if resource_identifier == "aws.browser.v1" else "BrowserCustom" - return format_resource_type(resource_type), resource_identifier + return resource_type, resource_identifier, resource_identifier + return None, None, None + - # Gateway +def _handle_gateway_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: gateway_id = attrs.get(GEN_AI_GATEWAY_ID) gateway_arn = attrs.get(AWS_BEDROCK_AGENTCORE_GATEWAY_ARN) gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) + if gateway_target_id: - resource_type = "GatewayTarget" resource_identifier = str(gateway_target_id) - return format_resource_type(resource_type), resource_identifier + if gateway_arn: + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + if gateway_id: + resource_identifier = str(gateway_id) + return "GatewayTarget", resource_identifier, resource_identifier + if gateway_arn or gateway_id: + resource_identifier = None if gateway_arn: - resource_identifier = extract_id_from_arn(str(gateway_arn)) if gateway_arn else None + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) if gateway_id: resource_identifier = str(gateway_id) - resource_type = "Gateway" - return format_resource_type(resource_type), resource_identifier + return "Gateway", resource_identifier, resource_identifier + + return None, None, None + - # Runtime +def _handle_runtime_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: runtime_id = attrs.get(GEN_AI_RUNTIME_ID) runtime_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ARN) runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN) + if runtime_endpoint_arn: - resource_type = "RuntimeEndpoint" - resource_identifier = str(runtime_endpoint_arn) - return format_resource_type(resource_type), resource_identifier + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_endpoint_arn)) + return "RuntimeEndpoint", resource_identifier, str(runtime_endpoint_arn) + if runtime_arn or runtime_id: + resource_identifier = None if runtime_arn: - resource_identifier = extract_id_from_arn(str(runtime_arn)) if runtime_arn else None + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_arn)) if runtime_id: resource_identifier = str(runtime_id) - resource_type = "Runtime" - return format_resource_type(resource_type), resource_identifier + return "Runtime", resource_identifier, resource_identifier + + return None, None, None + - # Code interpreter +def _handle_code_interpreter_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: code_interpreter_id = attrs.get(GEN_AI_CODE_INTERPRETER_ID) code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN) + if code_interpreter_id or code_interpreter_arn: + resource_identifier = None if code_interpreter_arn: - resource_identifier = extract_id_from_arn(str(code_interpreter_arn)) if code_interpreter_arn else None + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(code_interpreter_arn)) if code_interpreter_id: resource_identifier = str(code_interpreter_id) resource_type = ( "CodeInterpreter" if resource_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" ) - return format_resource_type(resource_type), resource_identifier + return resource_type, resource_identifier, resource_identifier + + return None, None, None + - # Identity +def _handle_identity_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: credential_arn = attrs.get(AWS_AUTH_CREDENTIAL_PROVIDER_ARN) + if credential_arn: credential_arn_str = str(credential_arn) + resource_type = None if "apikeycredentialprovider" in credential_arn_str: resource_type = "APIKeyCredentialProvider" if "oauth2credentialprovider" in credential_arn_str: resource_type = "OAuth2CredentialProvider" - resource_identifier = extract_id_from_arn(credential_arn_str) - return format_resource_type(resource_type), resource_identifier + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(credential_arn_str) + return resource_type, resource_identifier, resource_identifier + + return None, None, None - # Memory + +def _handle_memory_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: + memory_id = attrs.get(GEN_AI_MEMORY_ID) memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) - if memory_arn: - resource_type = "Memory" - resource_identifier = str(memory_arn) if memory_arn else None - return format_resource_type(resource_type), resource_identifier - return None, None + if memory_id or memory_arn: + resource_identifier = None + cfn_primary_identifier = None + if memory_arn: + resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(memory_arn)) + cfn_primary_identifier = str(memory_arn) + if memory_id: + resource_identifier = str(memory_id) + return "Memory", resource_identifier, cfn_primary_identifier + + return None, None, None + + +def extract_bedrock_agentcore_resource_id_from_arn(arn: str) -> Optional[str]: + """Extract resource ID from ARN resource part.""" + resource_part = RegionalResourceArnParser.extract_resource_name_from_arn(arn) + if not resource_part: + return None + return resource_part.split("/")[-1] if "/" in resource_part else resource_part def _log_unknown_attribute(attribute_key: str, span: ReadableSpan) -> None: diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index 0e34a60b8..f447de8ad 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -13,6 +13,8 @@ AWS_AUTH_REGION, AWS_BEDROCK_AGENT_ID, AWS_BEDROCK_AGENTCORE_BROWSER_ARN, + AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN, + AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, AWS_BEDROCK_AGENTCORE_MEMORY_ARN, AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, @@ -247,6 +249,23 @@ def _validate_attributes_produced_for_non_local_root_span_of_kind( self.assertEqual(len(service_attributes), len(BoundedAttributes(attributes=expected_attributes))) self.assertEqual(service_attributes, BoundedAttributes(attributes=expected_attributes)) + def validate_bedrock_agentcore_resource( + self, attribute_keys, attribute_values, expected_type, expected_identifier, expected_cfn_primary_identifier + ): + keys = [SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE] + values = ["aws-api", "Bedrock AgentCore"] + self._mock_attribute(keys, values) + self.span_mock.kind = SpanKind.CLIENT + + self._mock_attribute(attribute_keys, attribute_values, keys, values) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE), expected_type) + self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER), expected_identifier) + self.assertEqual(actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER), expected_cfn_primary_identifier) + self._mock_attribute(attribute_keys, [None] * len(attribute_keys)) + # pylint: disable=too-many-public-methods class TestAwsMetricAttributeGenerator(TestUtil): @@ -2119,78 +2138,233 @@ def test_cloudformation_primary_identifier_fallback_to_remote_resource_identifie keys, values = self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None], keys, values) - def test_bedrock_agentcore_resource_attributes(self): - """Test Bedrock AgentCore resource attributes for all resource types.""" - - def validate_bedrock_agentcore_resource(attribute_keys, attribute_values, expected_type, expected_identifier): - keys = [SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE] - values = ["aws-api", "Bedrock AgentCore"] - self._mock_attribute(keys, values) - self.span_mock.kind = SpanKind.CLIENT - - self._mock_attribute(attribute_keys, attribute_values, keys, values) - actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( - DEPENDENCY_METRIC - ) - self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE), expected_type) - self.assertEqual(actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER), expected_identifier) - self.assertEqual(actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER), expected_identifier) - self._mock_attribute(attribute_keys, [None] * len(attribute_keys)) - - validate_bedrock_agentcore_resource( - [GEN_AI_BROWSER_ID], ["aws.browser.v1"], "AWS::BedrockAgentCore::Browser", "aws.browser.v1" - ) - validate_bedrock_agentcore_resource( - [GEN_AI_BROWSER_ID], - ["testBrowser-1234567890"], - "AWS::BedrockAgentCore::BrowserCustom", - "testBrowser-1234567890", - ) - validate_bedrock_agentcore_resource( - [AWS_BEDROCK_AGENTCORE_BROWSER_ARN], - ["arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/testBrowser-1234567890"], - "AWS::BedrockAgentCore::BrowserCustom", - "testBrowser-1234567890", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_GATEWAY_ID, AWS_GATEWAY_TARGET_ID], - ["agentGateway-123456789", "target-123456789"], - "AWS::BedrockAgentCore::GatewayTarget", - "target-123456789", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_GATEWAY_ID], ["agentGateway-123456789"], "AWS::BedrockAgentCore::Gateway", "agentGateway-123456789" - ) - validate_bedrock_agentcore_resource( - [GEN_AI_RUNTIME_ID, AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], - ["test-runtime-123", "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint"], - "AWS::BedrockAgentCore::RuntimeEndpoint", - "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_RUNTIME_ID], ["test-runtime-123"], "AWS::BedrockAgentCore::Runtime", "test-runtime-123" - ) - validate_bedrock_agentcore_resource( - [AWS_BEDROCK_AGENTCORE_RUNTIME_ARN], - ["arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123"], - "AWS::BedrockAgentCore::Runtime", - "test-runtime-123", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_CODE_INTERPRETER_ID], - ["aws.codeinterpreter.v1"], - "AWS::BedrockAgentCore::CodeInterpreter", - "aws.codeinterpreter.v1", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_CODE_INTERPRETER_ID], - ["testCodeInt-1234567890"], - "AWS::BedrockAgentCore::CodeInterpreterCustom", - "testCodeInt-1234567890", - ) - validate_bedrock_agentcore_resource( - [GEN_AI_MEMORY_ID, AWS_BEDROCK_AGENTCORE_MEMORY_ARN], - ["agentMemory-123456789", "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789"], - "AWS::BedrockAgentCore::Memory", - "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789", + def test_bedrock_agentcore_browser_resource_attributes(self): + """Test Bedrock AgentCore browser resource attributes.""" + + # Test managed browser resource type when browser ID is aws.browser.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_BROWSER_ID], + attribute_values=["aws.browser.v1"], + expected_type="AWS::BedrockAgentCore::Browser", + expected_identifier="aws.browser.v1", + expected_cfn_primary_identifier="aws.browser.v1", + ) + # Test custom browser resource type when browser ID is not the standard aws.browser.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_BROWSER_ID], + attribute_values=["testBrowser-1234567890"], + expected_type="AWS::BedrockAgentCore::BrowserCustom", + expected_identifier="testBrowser-1234567890", + expected_cfn_primary_identifier="testBrowser-1234567890", + ) + # Test custom browser resource type when browser ARN is provided + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_BROWSER_ARN], + attribute_values=["arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/testBrowser-1234567890"], + expected_type="AWS::BedrockAgentCore::BrowserCustom", + expected_identifier="testBrowser-1234567890", + expected_cfn_primary_identifier="testBrowser-1234567890", + ) + # Test managed browser resource type when browser ARN contains aws.browser.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_BROWSER_ARN], + attribute_values=["arn:aws:bedrock-agentcore:us-east-1:123456789012:browser/aws.browser.v1"], + expected_type="AWS::BedrockAgentCore::Browser", + expected_identifier="aws.browser.v1", + expected_cfn_primary_identifier="aws.browser.v1", + ) + + def test_bedrock_agentcore_gateway_resource_attributes(self): + """Test Bedrock AgentCore gateway resource attributes.""" + + # Test managed gateway resource: when both gateway ID and target ID are present, both resource and + # CFN identifiers should be set to the gateway ID + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_GATEWAY_ID, AWS_GATEWAY_TARGET_ID], + attribute_values=["agentGateway-123456789", "target-123456789"], + expected_type="AWS::BedrockAgentCore::GatewayTarget", + expected_identifier="agentGateway-123456789", + expected_cfn_primary_identifier="agentGateway-123456789", + ) + # Test gateway resource with ARN and target ID: both resource and CFN identifiers should be set to + # the extracted gateway ID from ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_GATEWAY_ARN, AWS_GATEWAY_TARGET_ID], + attribute_values=[ + "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/gateway-from-arn", + "target-123456789", + ], + expected_type="AWS::BedrockAgentCore::GatewayTarget", + expected_identifier="gateway-from-arn", + expected_cfn_primary_identifier="gateway-from-arn", + ) + # Test gateway target resource: when only target ID is present, both identifiers should be set to + # the target ID + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_GATEWAY_TARGET_ID], + attribute_values=["target-only-123456789"], + expected_type="AWS::BedrockAgentCore::GatewayTarget", + expected_identifier="target-only-123456789", + expected_cfn_primary_identifier="target-only-123456789", + ) + # Test gateway resource with ID only: both identifiers should be set to the gateway ID + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_GATEWAY_ID], + attribute_values=["agentGateway-123456789"], + expected_type="AWS::BedrockAgentCore::Gateway", + expected_identifier="agentGateway-123456789", + expected_cfn_primary_identifier="agentGateway-123456789", + ) + # Test gateway resource with ARN only: both identifiers should be set to the extracted gateway ID + # from ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_GATEWAY_ARN], + attribute_values=["arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/gateway-arn-only"], + expected_type="AWS::BedrockAgentCore::Gateway", + expected_identifier="gateway-arn-only", + expected_cfn_primary_identifier="gateway-arn-only", + ) + + def test_bedrock_agentcore_memory_resource_attributes(self): + """Test Bedrock AgentCore memory resource attributes.""" + + # Test memory resource with both ID and ARN: resource identifier should be set to the memory ID + # and CFN primary identifier should be set to the memory ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_MEMORY_ID, AWS_BEDROCK_AGENTCORE_MEMORY_ARN], + attribute_values=[ + "agentMemory-123456789", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789", + ], + expected_type="AWS::BedrockAgentCore::Memory", + expected_identifier="agentMemory-123456789", + expected_cfn_primary_identifier=( + "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/agentMemory-123456789" + ), + ) + # Test memory resource with ARN only: resource identifier should be set to the extracted memory ID + # and CFN primary identifier should be set to the memory ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_MEMORY_ARN], + attribute_values=["arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/memory-arn-only"], + expected_type="AWS::BedrockAgentCore::Memory", + expected_identifier="memory-arn-only", + expected_cfn_primary_identifier=("arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/memory-arn-only"), + ) + # Test memory resource with ID only: both identifiers should be set to the memory ID + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_MEMORY_ID], + attribute_values=["memory-id-only"], + expected_type="AWS::BedrockAgentCore::Memory", + expected_identifier="memory-id-only", + expected_cfn_primary_identifier="memory-id-only", + ) + + def test_bedrock_agentcore_code_interpreter_resource_attributes(self): + """Test Bedrock AgentCore code interpreter resource attributes.""" + + # Test managed code interpreter resource type when code interpreter ID is aws.codeinterpreter.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_CODE_INTERPRETER_ID], + attribute_values=["aws.codeinterpreter.v1"], + expected_type="AWS::BedrockAgentCore::CodeInterpreter", + expected_identifier="aws.codeinterpreter.v1", + expected_cfn_primary_identifier="aws.codeinterpreter.v1", + ) + # Test custom code interpreter resource type when code interpreter ID is not the standard + # aws.codeinterpreter.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_CODE_INTERPRETER_ID], + attribute_values=["testCodeInt-1234567890"], + expected_type="AWS::BedrockAgentCore::CodeInterpreterCustom", + expected_identifier="testCodeInt-1234567890", + expected_cfn_primary_identifier="testCodeInt-1234567890", + ) + # Test custom code interpreter resource type when code interpreter ARN is provided + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN], + attribute_values=[ + "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/" "testCodeInt-1234567890" + ], + expected_type="AWS::BedrockAgentCore::CodeInterpreterCustom", + expected_identifier="testCodeInt-1234567890", + expected_cfn_primary_identifier="testCodeInt-1234567890", + ) + # Test managed code interpreter resource type when code interpreter ARN contains + # aws.codeinterpreter.v1 + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN], + attribute_values=[ + "arn:aws:bedrock-agentcore:us-east-1:123456789012:code-interpreter/" "aws.codeinterpreter.v1" + ], + expected_type="AWS::BedrockAgentCore::CodeInterpreter", + expected_identifier="aws.codeinterpreter.v1", + expected_cfn_primary_identifier="aws.codeinterpreter.v1", + ) + + def test_bedrock_agentcore_runtime_resource_attributes(self): + """Test Bedrock AgentCore runtime resource attributes.""" + # Test runtime endpoint resource with all attributes: resource identifier should be set to extracted + # endpoint ID and CFN primary identifier should be set to endpoint ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[ + GEN_AI_RUNTIME_ID, + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN, + ], + attribute_values=[ + "test-runtime-123", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint", + ], + expected_type="AWS::BedrockAgentCore::RuntimeEndpoint", + expected_identifier="test-endpoint", + expected_cfn_primary_identifier=( + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" + ), + ) + # Test runtime endpoint resource with runtime ID and endpoint ARN: resource identifier should be + # set to extracted endpoint ID and CFN primary identifier should be set to endpoint ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_RUNTIME_ID, AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], + attribute_values=[ + "test-runtime-123", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint", + ], + expected_type="AWS::BedrockAgentCore::RuntimeEndpoint", + expected_identifier="test-endpoint", + expected_cfn_primary_identifier=( + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" + ), + ) + # Test runtime endpoint resource with runtime ARN and endpoint ARN: resource identifier should be + # set to extracted endpoint ID and CFN primary identifier should be set to endpoint ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_RUNTIME_ARN, AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], + attribute_values=[ + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint", + ], + expected_type="AWS::BedrockAgentCore::RuntimeEndpoint", + expected_identifier="test-endpoint", + expected_cfn_primary_identifier=( + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/test-endpoint" + ), + ) + # Test runtime resource with ID only: both identifiers should be set to the runtime ID + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_RUNTIME_ID], + attribute_values=["test-runtime-123"], + expected_type="AWS::BedrockAgentCore::Runtime", + expected_identifier="test-runtime-123", + expected_cfn_primary_identifier="test-runtime-123", + ) + # Test runtime resource with ARN only: both identifiers should be set to the extracted + # runtime ID from ARN + self.validate_bedrock_agentcore_resource( + attribute_keys=[AWS_BEDROCK_AGENTCORE_RUNTIME_ARN], + attribute_values=["arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test-runtime-123"], + expected_type="AWS::BedrockAgentCore::Runtime", + expected_identifier="test-runtime-123", + expected_cfn_primary_identifier="test-runtime-123", ) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 91acfbaa9..ecf16680b 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -261,6 +261,29 @@ def _handle_bedrock_agentcore_request(self) -> None: ], ) return + if operation == "creatememory": + memory_id = resource_id + bedrock_agentcore_control_client.meta.events.register( + "before-call.bedrock-agentcore-control.CreateMemory", + lambda **kwargs: inject_200_success( + memory={ + "id": memory_id, + "arn": f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:memory/{memory_id}", + "name": "testMemory", + "status": "ACTIVE", + "createdAt": 1704067200, + "updatedAt": 1704067200, + "eventExpiryDuration": 30, + }, + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_memory( + name="testMemory", + eventExpiryDuration=30, + description="Test memory for testing", + ) + return if service == "gateway": if operation == "creategateway": gateway_id = resource_id diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index 6254b85c4..4a3a2b837 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -312,7 +312,7 @@ def test_sqs_create_queue(self): _AWS_SQS_QUEUE_NAME: "test_queue", }, response_specific_attributes={ - _AWS_SQS_QUEUE_URL: "http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/test_queue", + _AWS_SQS_QUEUE_URL: ("http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/test_queue"), }, span_name="SQS.CreateQueue", ) @@ -768,7 +768,6 @@ def test_bedrock_agentcore_create_agent_runtime(self): ) def test_bedrock_agentcore_create_agent_runtime_endpoint(self): - expected_identifier = "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" self.do_test_requests( "bedrock-agentcore/runtime/createendpoint/myAgent-w8slyU6q5M", "GET", @@ -779,8 +778,10 @@ def test_bedrock_agentcore_create_agent_runtime_endpoint(self): remote_service="AWS::BedrockAgentCore", remote_operation="CreateAgentRuntimeEndpoint", remote_resource_type="AWS::BedrockAgentCore::RuntimeEndpoint", - remote_resource_identifier=expected_identifier, - cloudformation_primary_identifier=expected_identifier, + remote_resource_identifier="invokeEndpoint", + cloudformation_primary_identifier=( + "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" + ), span_name="Bedrock AgentCore Control.CreateAgentRuntimeEndpoint", ) @@ -886,6 +887,25 @@ def test_bedrock_agentcore_create_event(self): span_name="Bedrock AgentCore.CreateEvent", ) + def test_bedrock_agentcore_create_memory(self): + expected_identifier = "testMemory-abc123def456" + self.do_test_requests( + "bedrock-agentcore/memory/creatememory/testMemory-abc123def456", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateMemory", + remote_resource_type="AWS::BedrockAgentCore::Memory", + remote_resource_identifier=expected_identifier, + cloudformation_primary_identifier=( + f"arn:aws:bedrock-agentcore:us-west-2:123456789012:memory/{expected_identifier}" + ), + span_name="Bedrock AgentCore Control.CreateMemory", + ) + def test_bedrock_agentcore_create_gateway(self): expected_identifier = "agentGateway-k8Nt2pLm9Q" self.do_test_requests( @@ -904,7 +924,7 @@ def test_bedrock_agentcore_create_gateway(self): ) def test_bedrock_agentcore_create_gateway_target(self): - expected_identifier = "testTarget-123" + expected_identifier = "workinggateway-oersefsjga" self.do_test_requests( "bedrock-agentcore/gateway/creategatewaytarget/workinggateway-oersefsjga", "GET", From d29964eaa880b062a2c6ff2fecf904442a8e0d98 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 19:15:16 -0700 Subject: [PATCH 17/23] lint fix --- contract-tests/images/applications/botocore/botocore_server.py | 1 + contract-tests/tests/test/amazon/botocore/botocore_test.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index ecf16680b..dde7eabf7 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +# pylint: disable=too-many-lines import atexit import json import os diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index 4a3a2b837..7e7f5b6b4 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -312,7 +312,7 @@ def test_sqs_create_queue(self): _AWS_SQS_QUEUE_NAME: "test_queue", }, response_specific_attributes={ - _AWS_SQS_QUEUE_URL: ("http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/test_queue"), + _AWS_SQS_QUEUE_URL: "http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/test_queue", }, span_name="SQS.CreateQueue", ) From 274149443d1fc6b87d969847c80a7e55139c7eea Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 16 Oct 2025 19:26:28 -0700 Subject: [PATCH 18/23] change changelog.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdba4162..8cb76d552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ For any change that affects end users of this package, please add an entry under If your change does not need a CHANGELOG entry, add the "skip changelog" label to your PR. ## Unreleased -- Add Application Signals Attributes for Bedrock AgentCore spans +- Add Resource and CFN Attributes for Bedrock AgentCore spans ([#495](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/495)) - Add botocore instrumentation extension for Bedrock AgentCore services with span attributes ([#490](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/490)) From 03d6654c5e5b44266085f441591fcb5f74c30bf3 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Fri, 17 Oct 2025 10:34:25 -0700 Subject: [PATCH 19/23] use elifs and change var namings --- .../distro/_aws_metric_attribute_generator.py | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index c5bf4d07c..de796e19b 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -733,13 +733,13 @@ def _handle_browser_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional browser_id = attrs.get(GEN_AI_BROWSER_ID) browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN) if browser_id or browser_arn: - resource_identifier = None - if browser_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(browser_arn)) + agentcore_cfn_identifier = None if browser_id: - resource_identifier = str(browser_id) - resource_type = "Browser" if resource_identifier == "aws.browser.v1" else "BrowserCustom" - return resource_type, resource_identifier, resource_identifier + agentcore_cfn_identifier = str(browser_id) + elif browser_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(browser_arn)) + resource_type = "Browser" if agentcore_cfn_identifier == "aws.browser.v1" else "BrowserCustom" + return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None @@ -749,20 +749,20 @@ def _handle_gateway_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) if gateway_target_id: - resource_identifier = str(gateway_target_id) - if gateway_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + agentcore_cfn_identifier = str(gateway_target_id) if gateway_id: - resource_identifier = str(gateway_id) - return "GatewayTarget", resource_identifier, resource_identifier + agentcore_cfn_identifier = str(gateway_id) + elif gateway_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + return "GatewayTarget", agentcore_cfn_identifier, agentcore_cfn_identifier if gateway_arn or gateway_id: - resource_identifier = None - if gateway_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + agentcore_cfn_identifier = None if gateway_id: - resource_identifier = str(gateway_id) - return "Gateway", resource_identifier, resource_identifier + agentcore_cfn_identifier = str(gateway_id) + elif gateway_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + return "Gateway", agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None @@ -773,16 +773,16 @@ def _handle_runtime_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN) if runtime_endpoint_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_endpoint_arn)) - return "RuntimeEndpoint", resource_identifier, str(runtime_endpoint_arn) + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_endpoint_arn)) + return "RuntimeEndpoint", agentcore_cfn_identifier, str(runtime_endpoint_arn) if runtime_arn or runtime_id: - resource_identifier = None - if runtime_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_arn)) + agentcore_cfn_identifier = None if runtime_id: - resource_identifier = str(runtime_id) - return "Runtime", resource_identifier, resource_identifier + agentcore_cfn_identifier = str(runtime_id) + elif runtime_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_arn)) + return "Runtime", agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None @@ -792,15 +792,13 @@ def _handle_code_interpreter_attrs(attrs) -> tuple[Optional[str], Optional[str], code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN) if code_interpreter_id or code_interpreter_arn: - resource_identifier = None - if code_interpreter_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(code_interpreter_arn)) + agentcore_cfn_identifier = None if code_interpreter_id: - resource_identifier = str(code_interpreter_id) - resource_type = ( - "CodeInterpreter" if resource_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" - ) - return resource_type, resource_identifier, resource_identifier + agentcore_cfn_identifier = str(code_interpreter_id) + elif code_interpreter_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(code_interpreter_arn)) + resource_type = "CodeInterpreter" if agentcore_cfn_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" + return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None @@ -813,10 +811,10 @@ def _handle_identity_attrs(attrs) -> tuple[Optional[str], Optional[str], Optiona resource_type = None if "apikeycredentialprovider" in credential_arn_str: resource_type = "APIKeyCredentialProvider" - if "oauth2credentialprovider" in credential_arn_str: + elif "oauth2credentialprovider" in credential_arn_str: resource_type = "OAuth2CredentialProvider" - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(credential_arn_str) - return resource_type, resource_identifier, resource_identifier + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(credential_arn_str) + return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None @@ -826,14 +824,14 @@ def _handle_memory_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[ memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) if memory_id or memory_arn: - resource_identifier = None - cfn_primary_identifier = None - if memory_arn: - resource_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(memory_arn)) - cfn_primary_identifier = str(memory_arn) + agentcore_cfn_identifier = None + agentcore_cfn_primary_identifier = None if memory_id: - resource_identifier = str(memory_id) - return "Memory", resource_identifier, cfn_primary_identifier + agentcore_cfn_identifier = str(memory_id) + elif memory_arn: + agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(memory_arn)) + agentcore_cfn_primary_identifier = str(memory_arn) + return "Memory", agentcore_cfn_identifier, agentcore_cfn_primary_identifier return None, None, None From a812b8fd5b015356bb73ee4d635433b73caa1307 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Tue, 21 Oct 2025 02:15:14 -0700 Subject: [PATCH 20/23] add comments for agentcore handlers and fix tests --- .../distro/_aws_metric_attribute_generator.py | 74 ++++++++++++++++--- .../attributes/gen_ai_attributes.py | 1 - .../test_aws_metric_attribute_generator.py | 11 +++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index de796e19b..f483e2304 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -729,7 +729,11 @@ def format_resource_type(resource_type: Optional[str]) -> Optional[str]: return None, None, None -def _handle_browser_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_browser_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore Browser resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ browser_id = attrs.get(GEN_AI_BROWSER_ID) browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN) if browser_id or browser_arn: @@ -738,22 +742,35 @@ def _handle_browser_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional agentcore_cfn_identifier = str(browser_id) elif browser_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(browser_arn)) + + # Uses browser ID as both resource identifier and CFN primary identifier. + # aws.browser.v1 is a managed AWS resource, custom IDs are user-defined resources. resource_type = "Browser" if agentcore_cfn_identifier == "aws.browser.v1" else "BrowserCustom" return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None -def _handle_gateway_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_gateway_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore Gateway resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ gateway_id = attrs.get(GEN_AI_GATEWAY_ID) gateway_arn = attrs.get(AWS_BEDROCK_AGENTCORE_GATEWAY_ARN) gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID) if gateway_target_id: - agentcore_cfn_identifier = str(gateway_target_id) if gateway_id: agentcore_cfn_identifier = str(gateway_id) elif gateway_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + else: + agentcore_cfn_identifier = str(gateway_target_id) + + # GatewayTarget contains two primary identifiers: gateway ID and gateway target ID. + # Uses gateway ID and/or gateway target ID as both resource identifier and CFN primary identifier. + # If gateway ID exists or can be extracted from ARN, use it as CFN primary identifier, + # otherwise use target ID. return "GatewayTarget", agentcore_cfn_identifier, agentcore_cfn_identifier if gateway_arn or gateway_id: @@ -762,18 +779,27 @@ def _handle_gateway_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional agentcore_cfn_identifier = str(gateway_id) elif gateway_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn)) + + # Uses gateway ID as both resource identifier and CFN primary identifier. + # If gateway ID is not available, extract it from the gateway ARN. return "Gateway", agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None -def _handle_runtime_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_runtime_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore Runtime resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ runtime_id = attrs.get(GEN_AI_RUNTIME_ID) runtime_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ARN) runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN) if runtime_endpoint_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_endpoint_arn)) + + # Uses extracted ID as resource identifier and full ARN as CFN primary identifier. return "RuntimeEndpoint", agentcore_cfn_identifier, str(runtime_endpoint_arn) if runtime_arn or runtime_id: @@ -782,12 +808,19 @@ def _handle_runtime_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional agentcore_cfn_identifier = str(runtime_id) elif runtime_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_arn)) + + # Uses runtime ID as both resource identifier and CFN primary identifier. + # If runtime ID is not available, extract it from the runtime ARN. return "Runtime", agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None -def _handle_code_interpreter_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_code_interpreter_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore CodeInterpreter resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ code_interpreter_id = attrs.get(GEN_AI_CODE_INTERPRETER_ID) code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN) @@ -797,13 +830,22 @@ def _handle_code_interpreter_attrs(attrs) -> tuple[Optional[str], Optional[str], agentcore_cfn_identifier = str(code_interpreter_id) elif code_interpreter_arn: agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(code_interpreter_arn)) - resource_type = "CodeInterpreter" if agentcore_cfn_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" + + # Uses code interpreter ID as both resource identifier and CFN primary identifier. + # aws.codeinterpreter.v1 is a managed AWS resource, custom IDs are user-defined resources. + resource_type = ( + "CodeInterpreter" if agentcore_cfn_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom" + ) return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None -def _handle_identity_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_identity_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore Identity resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ credential_arn = attrs.get(AWS_AUTH_CREDENTIAL_PROVIDER_ARN) if credential_arn: @@ -814,12 +856,19 @@ def _handle_identity_attrs(attrs) -> tuple[Optional[str], Optional[str], Optiona elif "oauth2credentialprovider" in credential_arn_str: resource_type = "OAuth2CredentialProvider" agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(credential_arn_str) + + # Uses extracted ID from credential ARN as both resource identifier and CFN primary identifier. + # Resource type is determined by credential provider type in the ARN. return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier return None, None, None -def _handle_memory_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]: +def _handle_memory_attrs(attrs: BoundedAttributes) -> tuple[Optional[str], Optional[str], Optional[str]]: + """ + Handler for BedrockAgentCore Memory resources. + Returns (resource_type, resource_identifier, cfn_primary_identifier). + """ memory_id = attrs.get(GEN_AI_MEMORY_ID) memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN) @@ -828,9 +877,14 @@ def _handle_memory_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[ agentcore_cfn_primary_identifier = None if memory_id: agentcore_cfn_identifier = str(memory_id) - elif memory_arn: - agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(memory_arn)) + if memory_arn: + agentcore_cfn_identifier = agentcore_cfn_identifier or extract_bedrock_agentcore_resource_id_from_arn( + str(memory_arn) + ) agentcore_cfn_primary_identifier = str(memory_arn) + + # Uses memory ID as resource identifier and ARN as CFN primary identifier when available. + # If memory ID is not available, extract it from the memory ARN. return "Memory", agentcore_cfn_identifier, agentcore_cfn_primary_identifier return None, None, None diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py index 001234184..19f33d278 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 """ Utility module holding attribute keys for incubating Gen AI semantic conventions. - Remove this once we've contributed them to upstream. """ diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index f447de8ad..01783b72e 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -2259,6 +2259,17 @@ def test_bedrock_agentcore_memory_resource_attributes(self): expected_identifier="memory-id-only", expected_cfn_primary_identifier="memory-id-only", ) + # Test memory resource with both ID and ARN where ARN contains different ID: memory ID should be prioritized + self.validate_bedrock_agentcore_resource( + attribute_keys=[GEN_AI_MEMORY_ID, AWS_BEDROCK_AGENTCORE_MEMORY_ARN], + attribute_values=[ + "direct-memory-id", + "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/arn-memory-id", + ], + expected_type="AWS::BedrockAgentCore::Memory", + expected_identifier="direct-memory-id", + expected_cfn_primary_identifier=("arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/arn-memory-id"), + ) def test_bedrock_agentcore_code_interpreter_resource_attributes(self): """Test Bedrock AgentCore code interpreter resource attributes.""" From 39b641d2dc2a03af310727e1a8ecd32fb3f05804 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Tue, 21 Oct 2025 02:36:26 -0700 Subject: [PATCH 21/23] add more test coverage --- .../patches/test_instrumentation_patch.py | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 2697ebea5..cb8b1a193 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -283,38 +283,32 @@ def _test_patched_botocore_instrumentation(self): # Bedrock AgentCore self.assertTrue("bedrock-agentcore" in _KNOWN_EXTENSIONS) + self.assertTrue("bedrock-agentcore-control" in _KNOWN_EXTENSIONS) + + _do_extract_bedrock_agentcore_attributes, _do_on_success_bedrock_agentcore = _do_bedrock_agentcore_tests() bedrock_agentcore_attributes: Dict[str, str] = _do_extract_bedrock_agentcore_attributes() - # Runtime attributes - self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_RUNTIME_ARN], _AGENTCORE_RUNTIME_ARN) - self.assertEqual( - bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN], _AGENTCORE_RUNTIME_ENDPOINT_ARN - ) - self.assertEqual(bedrock_agentcore_attributes[GEN_AI_RUNTIME_ID], _AGENTCORE_RUNTIME_ID) - # Browser attributes - self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_BROWSER_ARN], _AGENTCORE_BROWSER_ARN) - self.assertEqual(bedrock_agentcore_attributes[GEN_AI_BROWSER_ID], _AGENTCORE_BROWSER_ID) - # Code interpreter attributes - self.assertEqual( - bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN], _AGENTCORE_CODE_INTERPRETER_ARN - ) - self.assertEqual(bedrock_agentcore_attributes[GEN_AI_CODE_INTERPRETER_ID], _AGENTCORE_CODE_INTERPRETER_ID) - # Gateway attributes - self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_GATEWAY_ARN], _AGENTCORE_GATEWAY_ARN) - self.assertEqual(bedrock_agentcore_attributes[GEN_AI_GATEWAY_ID], _AGENTCORE_GATEWAY_ID) - self.assertEqual(bedrock_agentcore_attributes[AWS_GATEWAY_TARGET_ID], _AGENTCORE_TARGET_ID) - # Memory attributes - self.assertEqual(bedrock_agentcore_attributes[GEN_AI_MEMORY_ID], _AGENTCORE_MEMORY_ID) - self.assertEqual(bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_MEMORY_ARN], _AGENTCORE_MEMORY_ARN) - # Auth and identity attributes - self.assertEqual( - bedrock_agentcore_attributes[AWS_AUTH_CREDENTIAL_PROVIDER_ARN], _AGENTCORE_CREDENTIAL_PROVIDER_ARN - ) - self.assertEqual( - bedrock_agentcore_attributes[AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN], _AGENTCORE_WORKLOAD_IDENTITY_ARN - ) + bedrock_agentcore_success_attributes: Dict[str, str] = _do_on_success_bedrock_agentcore() + + expected_attrs = { + AWS_BEDROCK_AGENTCORE_RUNTIME_ARN: _AGENTCORE_RUNTIME_ARN, + AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN: _AGENTCORE_RUNTIME_ENDPOINT_ARN, + GEN_AI_RUNTIME_ID: _AGENTCORE_RUNTIME_ID, + AWS_BEDROCK_AGENTCORE_BROWSER_ARN: _AGENTCORE_BROWSER_ARN, + GEN_AI_BROWSER_ID: _AGENTCORE_BROWSER_ID, + AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN: _AGENTCORE_CODE_INTERPRETER_ARN, + GEN_AI_CODE_INTERPRETER_ID: _AGENTCORE_CODE_INTERPRETER_ID, + AWS_BEDROCK_AGENTCORE_GATEWAY_ARN: _AGENTCORE_GATEWAY_ARN, + GEN_AI_GATEWAY_ID: _AGENTCORE_GATEWAY_ID, + AWS_GATEWAY_TARGET_ID: _AGENTCORE_TARGET_ID, + GEN_AI_MEMORY_ID: _AGENTCORE_MEMORY_ID, + AWS_BEDROCK_AGENTCORE_MEMORY_ARN: _AGENTCORE_MEMORY_ARN, + AWS_AUTH_CREDENTIAL_PROVIDER_ARN: _AGENTCORE_CREDENTIAL_PROVIDER_ARN, + AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN: _AGENTCORE_WORKLOAD_IDENTITY_ARN, + } - # Bedrock AgentCore Control - self.assertTrue("bedrock-agentcore-control" in _KNOWN_EXTENSIONS) + for attr_key, expected_value in expected_attrs.items(): + self.assertEqual(bedrock_agentcore_attributes[attr_key], expected_value) + self.assertEqual(bedrock_agentcore_success_attributes[attr_key], expected_value) # BedrockRuntime self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS) @@ -933,9 +927,8 @@ def _do_extract_lambda_attributes() -> Dict[str, str]: return _do_extract_attributes(service_name, params) -def _do_extract_bedrock_agentcore_attributes() -> Dict[str, str]: - service_name: str = "bedrock-agentcore" - params: Dict[str, Any] = { +def _do_bedrock_agentcore_tests(): + test_data = { "agentRuntimeArn": _AGENTCORE_RUNTIME_ARN, "agentRuntimeEndpointArn": _AGENTCORE_RUNTIME_ENDPOINT_ARN, "agentRuntimeId": _AGENTCORE_RUNTIME_ID, @@ -955,7 +948,14 @@ def _do_extract_bedrock_agentcore_attributes() -> Dict[str, str]: "memory": {"arn": _AGENTCORE_MEMORY_ARN, "id": _AGENTCORE_MEMORY_ID}, "workloadIdentityDetails": {"workloadIdentityArn": _AGENTCORE_WORKLOAD_IDENTITY_ARN}, } - return _do_extract_attributes(service_name, params) + + def extract_attributes(): + return _do_extract_attributes("bedrock-agentcore", test_data) + + def on_success(): + return _do_on_success("bedrock-agentcore", test_data) + + return extract_attributes, on_success def _do_extract_attributes(service_name: str, params: Dict[str, Any], operation: str = None) -> Dict[str, str]: From 165ac9936d59c103f93b8530fe862d74f9bfff49 Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Thu, 23 Oct 2025 23:30:33 -0700 Subject: [PATCH 22/23] lint fix --- .../distro/patches/test_instrumentation_patch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 8e719d47b..9fc8210d1 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -18,9 +18,7 @@ AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN, AWS_GATEWAY_TARGET_ID, ) -from amazon.opentelemetry.distro.patches._instrumentation_patch import ( - apply_instrumentation_patches, -) +from amazon.opentelemetry.distro.patches._instrumentation_patch import apply_instrumentation_patches from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_BROWSER_ID, GEN_AI_CODE_INTERPRETER_ID, From 6a7e84921dc567704958d34f286744d30fe092db Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Sun, 26 Oct 2025 22:37:01 -0700 Subject: [PATCH 23/23] Empty commit to test author fix