diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f969c13..8cb76d552 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 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)) 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..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 @@ -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.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.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,16 @@ 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, 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( @@ -674,6 +703,147 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu attributes[AWS_SPAN_KIND] = span_kind +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 + if not attrs: + 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 + + 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 + + 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: + agentcore_cfn_identifier = None + if browser_id: + 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 + + +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: + 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)) + return "GatewayTarget", agentcore_cfn_identifier, agentcore_cfn_identifier + + if gateway_arn or gateway_id: + agentcore_cfn_identifier = None + 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)) + return "Gateway", agentcore_cfn_identifier, agentcore_cfn_identifier + + return None, None, None + + +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: + 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: + agentcore_cfn_identifier = None + if runtime_id: + 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 + + +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: + agentcore_cfn_identifier = None + if code_interpreter_id: + 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 + + +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" + elif "oauth2credentialprovider" in credential_arn_str: + resource_type = "OAuth2CredentialProvider" + 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 + + +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_id or memory_arn: + agentcore_cfn_identifier = None + 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)) + agentcore_cfn_primary_identifier = str(memory_arn) + return "Memory", agentcore_cfn_identifier, agentcore_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: message: str = "No valid %s value found for %s span %s" if _logger.isEnabledFor(DEBUG): 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..001234184 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/semconv/_incubating/attributes/gen_ai_attributes.py @@ -0,0 +1,13 @@ +# 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. +""" + +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 abf03bba1..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 @@ -12,6 +12,12 @@ AWS_AUTH_ACCESS_KEY, 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, @@ -19,6 +25,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 +52,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.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.attributes import BoundedAttributes from opentelemetry.sdk.resources import _DEFAULT_RESOURCE, SERVICE_NAME from opentelemetry.sdk.trace import ReadableSpan, Resource @@ -75,8 +89,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 +108,167 @@ 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)) + + 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): def test_span_attributes_for_empty_resource(self): self.resource = Resource.get_empty() expected_attributes: Attributes = { @@ -897,6 +1071,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 +1123,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 +1968,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 +2137,234 @@ 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_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 36bf87dbb..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 @@ -7,6 +8,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 @@ -24,6 +26,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 +50,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 +110,308 @@ 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 + ) + bedrock_agentcore_control_client = boto3.client( + "bedrock-agentcore-control", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION + ) + # 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_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": + 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/{agent_id}", + agentRuntimeEndpointArn=( + f"arn:aws:bedrock-agentcore:{_AWS_REGION}:{_AWS_ACCOUNT_ID}:endpoint/invokeEndpoint" + ), + agentRuntimeId=agent_id, + createdAt="2024-01-01T00:00:00Z", + endpointName="invokeEndpoint", + status="ACTIVE", + targetVersion="1.0", + **kwargs, + ), + ) + bedrock_agentcore_control_client.create_agent_runtime_endpoint( + agentRuntimeId=agent_id, + name="invokeEndpoint", + description="Endpoint for invoking agent runtime", + ) + return + 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 = resource_id + 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 + 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 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 + 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) + 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): @@ -660,20 +968,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..7e7f5b6b4 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -750,6 +750,230 @@ def test_bedrock_agent_get_data_source(self): span_name="Bedrock Agent.GetDataSource", ) + def test_bedrock_agentcore_create_agent_runtime(self): + expected_identifier = "myAgent-w8slyU6q5M" + self.do_test_requests( + "bedrock-agentcore/runtime/createagentruntime/myAgent-w8slyU6q5M", + "GET", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore Control", + remote_service="AWS::BedrockAgentCore", + remote_operation="CreateAgentRuntime", + remote_resource_type="AWS::BedrockAgentCore::Runtime", + 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): + self.do_test_requests( + "bedrock-agentcore/runtime/createendpoint/myAgent-w8slyU6q5M", + "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="invokeEndpoint", + cloudformation_primary_identifier=( + "arn:aws:bedrock-agentcore:us-west-2:123456789012:endpoint/invokeEndpoint" + ), + 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", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartBrowserSession", + remote_resource_type="AWS::BedrockAgentCore::BrowserCustom", + 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", + 200, + 0, + 0, + rpc_service="Bedrock AgentCore", + remote_service="AWS::BedrockAgentCore", + remote_operation="StartBrowserSession", + remote_resource_type="AWS::BedrockAgentCore::Browser", + 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_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( + "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 = "workinggateway-oersefsjga" + 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",