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 069e39c87..0f4a77d1e 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 @@ -178,7 +178,16 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): if queue_url: attributes[AWS_SQS_QUEUE_URL] = queue_url + old_on_success = _SqsExtension.on_success + + def patch_on_success(self, span: Span, result: _BotoResultT): + old_on_success(self, span, result) + queue_url = result.get("QueueUrl") + if queue_url: + span.set_attribute(AWS_SQS_QUEUE_URL, queue_url) + _SqsExtension.extract_attributes = patch_extract_attributes + _SqsExtension.on_success = patch_on_success def _apply_botocore_bedrock_patch() -> None: diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index fd4cc29a4..f16948390 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -12,6 +12,7 @@ import requests from botocore.client import BaseClient from botocore.config import Config +from botocore.exceptions import ClientError from typing_extensions import Tuple, override _PORT: int = 8080 @@ -45,6 +46,10 @@ def do_GET(self): self._handle_kinesis_request() if self.in_path("bedrock"): self._handle_bedrock_request() + if self.in_path("secretsmanager"): + self._handle_secretsmanager_request() + if self.in_path("stepfunctions"): + self._handle_stepfunctions_request() self._end_request(self.main_status) @@ -246,7 +251,11 @@ def _handle_bedrock_request(self) -> None: set_main_status(200) bedrock_client.meta.events.register( "before-call.bedrock.GetGuardrail", - lambda **kwargs: inject_200_success(guardrailId="bt4o77i015cu", **kwargs), + lambda **kwargs: inject_200_success( + guardrailId="bt4o77i015cu", + guardrailArn="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu", + **kwargs, + ), ) bedrock_client.get_guardrail( guardrailIdentifier="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu" @@ -301,6 +310,69 @@ def _handle_bedrock_request(self) -> None: else: set_main_status(404) + def _handle_secretsmanager_request(self) -> None: + secretsmanager_client = boto3.client("secretsmanager", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION) + if self.in_path(_ERROR): + set_main_status(400) + try: + error_client = boto3.client("secretsmanager", endpoint_url=_ERROR_ENDPOINT, region_name=_AWS_REGION) + error_client.describe_secret( + SecretId="arn:aws:secretsmanager:us-west-2:000000000000:secret:unExistSecret" + ) + except Exception as exception: + print("Expected exception occurred", exception) + elif self.in_path(_FAULT): + set_main_status(500) + try: + fault_client = boto3.client( + "secretsmanager", endpoint_url=_FAULT_ENDPOINT, region_name=_AWS_REGION, config=_NO_RETRY_CONFIG + ) + fault_client.get_secret_value( + SecretId="arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret" + ) + except Exception as exception: + print("Expected exception occurred", exception) + elif self.in_path("describesecret/my-secret"): + set_main_status(200) + secretsmanager_client.describe_secret(SecretId="testSecret") + else: + set_main_status(404) + + def _handle_stepfunctions_request(self) -> None: + sfn_client = boto3.client("stepfunctions", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION) + if self.in_path(_ERROR): + set_main_status(400) + try: + error_client = boto3.client("stepfunctions", endpoint_url=_ERROR_ENDPOINT, region_name=_AWS_REGION) + error_client.describe_state_machine( + stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:unExistStateMachine" + ) + except Exception as exception: + print("Expected exception occurred", exception) + elif self.in_path(_FAULT): + set_main_status(500) + try: + fault_client = boto3.client("stepfunctions", endpoint_url=_FAULT_ENDPOINT, region_name=_AWS_REGION) + fault_client.meta.events.register( + "before-call.stepfunctions.ListStateMachineVersions", + lambda **kwargs: inject_500_error("ListStateMachineVersions", **kwargs), + ) + fault_client.list_state_machine_versions( + stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:invalid-state-machine", + ) + except Exception as exception: + print("Expected exception occurred", exception) + elif self.in_path("describestatemachine/my-state-machine"): + set_main_status(200) + sfn_client.describe_state_machine( + stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine" + ) + elif self.in_path("describeactivity/my-activity"): + set_main_status(200) + sfn_client.describe_activity(activityArn="arn:aws:states:us-west-2:000000000000:activity:testActivity") + else: + set_main_status(404) + def _end_request(self, status_code: int): self.send_response_only(status_code) self.end_headers() @@ -310,6 +382,7 @@ def set_main_status(status: int) -> None: RequestHandler.main_status = status +# pylint: disable=too-many-locals def prepare_aws_server() -> None: requests.Request(method="POST", url="http://localhost:4566/_localstack/state/reset") try: @@ -345,6 +418,57 @@ def prepare_aws_server() -> None: # Set up Kinesis so tests can access a stream. kinesis_client: BaseClient = boto3.client("kinesis", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION) kinesis_client.create_stream(StreamName="test_stream", ShardCount=1) + + # Set up Secrets Manager so tests can access a secret. + secretsmanager_client: BaseClient = boto3.client( + "secretsmanager", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION + ) + secretsmanager_response = secretsmanager_client.list_secrets() + secret = next((s for s in secretsmanager_response["SecretList"] if s["Name"] == "testSecret"), None) + if not secret: + secretsmanager_client.create_secret( + Name="testSecret", SecretString="secretValue", Description="This is a test secret" + ) + + # Set up Step Functions so tests can access a state machine and activity. + sfn_client: BaseClient = boto3.client("stepfunctions", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION) + sfn_response = sfn_client.list_state_machines() + state_machine_name = "testStateMachine" + activity_name = "testActivity" + state_machine = next((st for st in sfn_response["stateMachines"] if st["name"] == state_machine_name), None) + if not state_machine: + # create state machine needs an iam role so we create it here + iam_client: BaseClient = boto3.client("iam", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION) + iam_role_name = "testRole" + iam_role_arn = None + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Principal": {"Service": "states.amazonaws.com"}, "Action": "sts:AssumeRole"} + ], + } + try: + iam_response = iam_client.create_role( + RoleName=iam_role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) + ) + iam_client.attach_role_policy( + RoleName=iam_role_name, PolicyArn="arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess" + ) + print(f"IAM Role '{iam_role_name}' create successfully.") + iam_role_arn = iam_response["Role"]["Arn"] + sfn_defintion = { + "Comment": "A simple sequential workflow", + "StartAt": "FirstState", + "States": {"FirstState": {"Type": "Pass", "Result": "Hello, World!", "End": True}}, + } + definition_string = json.dumps(sfn_defintion) + sfn_client.create_state_machine( + name=state_machine_name, definition=definition_string, roleArn=iam_role_arn + ) + sfn_client.create_activity(name=activity_name) + except Exception as exception: + print("Something went wrong with Step Functions setup", exception) + except Exception as exception: print("Unexpected exception occurred", exception) @@ -363,6 +487,9 @@ def inject_200_success(**kwargs): 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 HTTPResponse = namedtuple("HTTPResponse", ["status_code", "headers", "body"]) headers = kwargs.get("headers", {}) @@ -371,6 +498,16 @@ def inject_200_success(**kwargs): return http_response, response_body +def inject_500_error(api_name: str, **kwargs): + raise ClientError( + { + "Error": {"Code": "InternalServerError", "Message": "Internal Server Error"}, + "ResponseMetadata": {"HTTPStatusCode": 500, "RequestId": "mock-request-id"}, + }, + api_name, + ) + + def main() -> None: prepare_aws_server() server_address: Tuple[str, int] = ("0.0.0.0", _PORT) diff --git a/contract-tests/tests/test/amazon/base/contract_test_base.py b/contract-tests/tests/test/amazon/base/contract_test_base.py index fcbcec8a7..ab3a02fe2 100644 --- a/contract-tests/tests/test/amazon/base/contract_test_base.py +++ b/contract-tests/tests/test/amazon/base/contract_test_base.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import re import time from logging import INFO, Logger, getLogger from typing import Dict, List @@ -171,6 +172,12 @@ def _assert_int_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, self.assertIsNotNone(actual_value) self.assertEqual(expected_value, actual_value.int_value) + def _assert_match_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, pattern: str) -> None: + self.assertIn(key, attributes_dict) + actual_value: AnyValue = attributes_dict[key] + self.assertIsNotNone(actual_value) + self.assertRegex(actual_value.string_value, pattern) + def check_sum(self, metric_name: str, actual_sum: float, expected_sum: float) -> None: if metric_name is LATENCY_METRIC: self.assertTrue(0 < actual_sum < expected_sum) @@ -221,3 +228,10 @@ def _assert_metric_attributes( self, resource_scope_metrics: List[ResourceScopeMetric], metric_name: str, expected_sum: int, **kwargs ): self.fail("Tests must implement this function") + + def _is_valid_regex(self, pattern: str) -> bool: + try: + re.compile(pattern) + return True + except re.error: + return False diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index e5a2a608f..f5ae91a59 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -10,6 +10,7 @@ from amazon.base.contract_test_base import NETWORK_NAME, ContractTestBase from amazon.utils.application_signals_constants import ( + AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, AWS_LOCAL_OPERATION, AWS_LOCAL_SERVICE, AWS_REMOTE_OPERATION, @@ -34,6 +35,9 @@ _AWS_BEDROCK_KNOWLEDGE_BASE_ID: str = "aws.bedrock.knowledge_base.id" _AWS_BEDROCK_DATA_SOURCE_ID: str = "aws.bedrock.data_source.id" _GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model" +_AWS_SECRET_ARN: str = "aws.secretsmanager.secret.arn" +_AWS_STATE_MACHINE_ARN: str = "aws.stepfunctions.state_machine.arn" +_AWS_ACTIVITY_ARN: str = "aws.stepfunctions.activity.arn" # pylint: disable=too-many-public-methods @@ -73,7 +77,7 @@ def set_up_dependency_container(cls): cls._local_stack: LocalStackContainer = ( LocalStackContainer(image="localstack/localstack:3.5.0") .with_name("localstack") - .with_services("s3", "sqs", "dynamodb", "kinesis") + .with_services("s3", "sqs", "dynamodb", "kinesis", "secretsmanager", "iam", "stepfunctions") .with_env("DEFAULT_REGION", "us-west-2") .with_kwargs(network=NETWORK_NAME, networking_config=local_stack_networking_config) ) @@ -99,6 +103,7 @@ def test_s3_create_bucket(self): remote_operation="CreateBucket", remote_resource_type="AWS::S3::Bucket", remote_resource_identifier="test-bucket-name", + cloudformation_primary_identifier="test-bucket-name", request_specific_attributes={ SpanAttributes.AWS_S3_BUCKET: "test-bucket-name", }, @@ -116,6 +121,7 @@ def test_s3_create_object(self): remote_operation="PutObject", remote_resource_type="AWS::S3::Bucket", remote_resource_identifier="test-put-object-bucket-name", + cloudformation_primary_identifier="test-put-object-bucket-name", request_specific_attributes={ SpanAttributes.AWS_S3_BUCKET: "test-put-object-bucket-name", }, @@ -133,6 +139,7 @@ def test_s3_get_object(self): remote_operation="GetObject", remote_resource_type="AWS::S3::Bucket", remote_resource_identifier="test-get-object-bucket-name", + cloudformation_primary_identifier="test-get-object-bucket-name", request_specific_attributes={ SpanAttributes.AWS_S3_BUCKET: "test-get-object-bucket-name", }, @@ -150,6 +157,7 @@ def test_s3_error(self): remote_operation="CreateBucket", remote_resource_type="AWS::S3::Bucket", remote_resource_identifier="-", + cloudformation_primary_identifier="-", request_specific_attributes={ SpanAttributes.AWS_S3_BUCKET: "-", }, @@ -167,6 +175,7 @@ def test_s3_fault(self): remote_operation="CreateBucket", remote_resource_type="AWS::S3::Bucket", remote_resource_identifier="valid-bucket-name", + cloudformation_primary_identifier="valid-bucket-name", request_specific_attributes={ SpanAttributes.AWS_S3_BUCKET: "valid-bucket-name", }, @@ -184,6 +193,7 @@ def test_dynamodb_create_table(self): remote_operation="CreateTable", remote_resource_type="AWS::DynamoDB::Table", remote_resource_identifier="test_table", + cloudformation_primary_identifier="test_table", request_specific_attributes={ SpanAttributes.AWS_DYNAMODB_TABLE_NAMES: ["test_table"], }, @@ -201,6 +211,7 @@ def test_dynamodb_put_item(self): remote_operation="PutItem", remote_resource_type="AWS::DynamoDB::Table", remote_resource_identifier="put_test_table", + cloudformation_primary_identifier="put_test_table", request_specific_attributes={ SpanAttributes.AWS_DYNAMODB_TABLE_NAMES: ["put_test_table"], }, @@ -218,6 +229,7 @@ def test_dynamodb_error(self): remote_operation="PutItem", remote_resource_type="AWS::DynamoDB::Table", remote_resource_identifier="invalid_table", + cloudformation_primary_identifier="invalid_table", request_specific_attributes={ SpanAttributes.AWS_DYNAMODB_TABLE_NAMES: ["invalid_table"], }, @@ -235,6 +247,7 @@ def test_dynamodb_fault(self): remote_operation="PutItem", remote_resource_type="AWS::DynamoDB::Table", remote_resource_identifier="invalid_table", + cloudformation_primary_identifier="invalid_table", request_specific_attributes={ SpanAttributes.AWS_DYNAMODB_TABLE_NAMES: ["invalid_table"], }, @@ -252,9 +265,15 @@ def test_sqs_create_queue(self): remote_operation="CreateQueue", remote_resource_type="AWS::SQS::Queue", remote_resource_identifier="test_queue", + cloudformation_primary_identifier=( + "http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/test_queue" + ), request_specific_attributes={ _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", + }, span_name="SQS.CreateQueue", ) @@ -269,6 +288,7 @@ def test_sqs_send_message(self): remote_operation="SendMessage", remote_resource_type="AWS::SQS::Queue", remote_resource_identifier="test_put_get_queue", + cloudformation_primary_identifier="http://localstack:4566/000000000000/test_put_get_queue", request_specific_attributes={ _AWS_SQS_QUEUE_URL: "http://localstack:4566/000000000000/test_put_get_queue", }, @@ -286,6 +306,7 @@ def test_sqs_receive_message(self): remote_operation="ReceiveMessage", remote_resource_type="AWS::SQS::Queue", remote_resource_identifier="test_put_get_queue", + cloudformation_primary_identifier="http://localstack:4566/000000000000/test_put_get_queue", request_specific_attributes={ _AWS_SQS_QUEUE_URL: "http://localstack:4566/000000000000/test_put_get_queue", }, @@ -303,6 +324,7 @@ def test_sqs_error(self): remote_operation="SendMessage", remote_resource_type="AWS::SQS::Queue", remote_resource_identifier="sqserror", + cloudformation_primary_identifier="http://error.test:8080/000000000000/sqserror", request_specific_attributes={ _AWS_SQS_QUEUE_URL: "http://error.test:8080/000000000000/sqserror", }, @@ -320,6 +342,7 @@ def test_sqs_fault(self): remote_operation="CreateQueue", remote_resource_type="AWS::SQS::Queue", remote_resource_identifier="invalid_test", + cloudformation_primary_identifier="invalid_test", request_specific_attributes={ _AWS_SQS_QUEUE_NAME: "invalid_test", }, @@ -337,6 +360,7 @@ def test_kinesis_put_record(self): remote_operation="PutRecord", remote_resource_type="AWS::Kinesis::Stream", remote_resource_identifier="test_stream", + cloudformation_primary_identifier="test_stream", request_specific_attributes={ _AWS_KINESIS_STREAM_NAME: "test_stream", }, @@ -354,6 +378,7 @@ def test_kinesis_error(self): remote_operation="PutRecord", remote_resource_type="AWS::Kinesis::Stream", remote_resource_identifier="invalid_stream", + cloudformation_primary_identifier="invalid_stream", request_specific_attributes={ _AWS_KINESIS_STREAM_NAME: "invalid_stream", }, @@ -371,6 +396,7 @@ def test_kinesis_fault(self): remote_operation="PutRecord", remote_resource_type="AWS::Kinesis::Stream", remote_resource_identifier="test_stream", + cloudformation_primary_identifier="test_stream", request_specific_attributes={ _AWS_KINESIS_STREAM_NAME: "test_stream", }, @@ -389,6 +415,7 @@ def test_bedrock_runtime_invoke_model(self): remote_operation="InvokeModel", remote_resource_type="AWS::Bedrock::Model", remote_resource_identifier="amazon.titan-text-premier-v1:0", + cloudformation_primary_identifier="amazon.titan-text-premier-v1:0", request_specific_attributes={ _GEN_AI_REQUEST_MODEL: "amazon.titan-text-premier-v1:0", }, @@ -407,6 +434,7 @@ def test_bedrock_get_guardrail(self): remote_operation="GetGuardrail", remote_resource_type="AWS::Bedrock::Guardrail", remote_resource_identifier="bt4o77i015cu", + cloudformation_primary_identifier="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu", request_specific_attributes={ _AWS_BEDROCK_GUARDRAIL_ID: "bt4o77i015cu", }, @@ -425,6 +453,7 @@ def test_bedrock_agent_runtime_invoke_agent(self): remote_operation="InvokeAgent", remote_resource_type="AWS::Bedrock::Agent", remote_resource_identifier="Q08WFRPHVL", + cloudformation_primary_identifier="Q08WFRPHVL", request_specific_attributes={ _AWS_BEDROCK_AGENT_ID: "Q08WFRPHVL", }, @@ -443,6 +472,7 @@ def test_bedrock_agent_runtime_retrieve(self): remote_operation="Retrieve", remote_resource_type="AWS::Bedrock::KnowledgeBase", remote_resource_identifier="test-knowledge-base-id", + cloudformation_primary_identifier="test-knowledge-base-id", request_specific_attributes={ _AWS_BEDROCK_KNOWLEDGE_BASE_ID: "test-knowledge-base-id", }, @@ -461,6 +491,7 @@ def test_bedrock_agent_get_agent(self): remote_operation="GetAgent", remote_resource_type="AWS::Bedrock::Agent", remote_resource_identifier="TESTAGENTID", + cloudformation_primary_identifier="TESTAGENTID", request_specific_attributes={ _AWS_BEDROCK_AGENT_ID: "TESTAGENTID", }, @@ -479,6 +510,7 @@ def test_bedrock_agent_get_knowledge_base(self): remote_operation="GetKnowledgeBase", remote_resource_type="AWS::Bedrock::KnowledgeBase", remote_resource_identifier="invalid-knowledge-base-id", + cloudformation_primary_identifier="invalid-knowledge-base-id", request_specific_attributes={ _AWS_BEDROCK_KNOWLEDGE_BASE_ID: "invalid-knowledge-base-id", }, @@ -497,12 +529,153 @@ def test_bedrock_agent_get_data_source(self): remote_operation="GetDataSource", remote_resource_type="AWS::Bedrock::DataSource", remote_resource_identifier="DATASURCID", + cloudformation_primary_identifier="TESTKBSEID|DATASURCID", request_specific_attributes={ _AWS_BEDROCK_DATA_SOURCE_ID: "DATASURCID", + _AWS_BEDROCK_KNOWLEDGE_BASE_ID: "TESTKBSEID", }, span_name="Bedrock Agent.GetDataSource", ) + def test_secretsmanager_describe_secret(self): + self.do_test_requests( + "secretsmanager/describesecret/my-secret", + "GET", + 200, + 0, + 0, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="DescribeSecret", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier=r"testSecret-[a-zA-Z0-9]{6}$", + cloudformation_primary_identifier=( + r"arn:aws:secretsmanager:us-west-2:000000000000:secret:testSecret-[a-zA-Z0-9]{6}$" + ), + response_specific_attributes={ + _AWS_SECRET_ARN: r"arn:aws:secretsmanager:us-west-2:000000000000:secret:testSecret-[a-zA-Z0-9]{6}$", + }, + span_name="Secrets Manager.DescribeSecret", + ) + + def test_secretsmanager_error(self): + self.do_test_requests( + "secretsmanager/error", + "GET", + 400, + 1, + 0, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="DescribeSecret", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier="unExistSecret", + cloudformation_primary_identifier="arn:aws:secretsmanager:us-west-2:000000000000:secret:unExistSecret", + request_specific_attributes={ + _AWS_SECRET_ARN: "arn:aws:secretsmanager:us-west-2:000000000000:secret:unExistSecret", + }, + span_name="Secrets Manager.DescribeSecret", + ) + + def test_secretsmanager_fault(self): + self.do_test_requests( + "secretsmanager/fault", + "GET", + 500, + 0, + 1, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="GetSecretValue", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier="nonexistent-secret", + cloudformation_primary_identifier="arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret", + request_specific_attributes={ + _AWS_SECRET_ARN: "arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret", + }, + span_name="Secrets Manager.GetSecretValue", + ) + + def test_stepfunctions_describe_state_machine(self): + self.do_test_requests( + "stepfunctions/describestatemachine/my-state-machine", + "GET", + 200, + 0, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeStateMachine", + remote_resource_type="AWS::StepFunctions::StateMachine", + remote_resource_identifier="testStateMachine", + cloudformation_primary_identifier="arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine", + request_specific_attributes={ + _AWS_STATE_MACHINE_ARN: "arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine", + }, + span_name="SFN.DescribeStateMachine", + ) + + def test_stepfunctions_describe_activity(self): + self.do_test_requests( + "stepfunctions/describeactivity/my-activity", + "GET", + 200, + 0, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeActivity", + remote_resource_type="AWS::StepFunctions::Activity", + remote_resource_identifier="testActivity", + cloudformation_primary_identifier="arn:aws:states:us-west-2:000000000000:activity:testActivity", + request_specific_attributes={ + _AWS_ACTIVITY_ARN: "arn:aws:states:us-west-2:000000000000:activity:testActivity" + }, + span_name="SFN.DescribeActivity", + ) + + def test_stepfunctions_error(self): + self.do_test_requests( + "stepfunctions/error", + "GET", + 400, + 1, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeStateMachine", + remote_resource_type="AWS::StepFunctions::StateMachine", + remote_resource_identifier="unExistStateMachine", + cloudformation_primary_identifier="arn:aws:states:us-west-2:000000000000:stateMachine:unExistStateMachine", + request_specific_attributes={ + _AWS_STATE_MACHINE_ARN: "arn:aws:states:us-west-2:000000000000:stateMachine:unExistStateMachine", + }, + span_name="SFN.DescribeStateMachine", + ) + + def test_stepfunctions_fault(self): + self.do_test_requests( + "stepfunctions/fault", + "GET", + 500, + 0, + 1, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="ListStateMachineVersions", + remote_resource_type="AWS::StepFunctions::StateMachine", + remote_resource_identifier="invalid-state-machine", + cloudformation_primary_identifier=( + "arn:aws:states:us-west-2:000000000000:stateMachine:invalid-state-machine" + ), + request_specific_attributes={ + _AWS_STATE_MACHINE_ARN: "arn:aws:states:us-west-2:000000000000:stateMachine:invalid-state-machine", + }, + span_name="SFN.ListStateMachineVersions", + ) + + # TODO: Add contract test for lambda event source mapping resource + @override def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSpan], path: str, **kwargs) -> None: target_spans: List[Span] = [] @@ -519,6 +692,7 @@ def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSp "LOCAL_ROOT", kwargs.get("remote_resource_type", "None"), kwargs.get("remote_resource_identifier", "None"), + kwargs.get("cloudformation_primary_identifier", "None"), ) def _assert_aws_attributes( @@ -529,6 +703,7 @@ def _assert_aws_attributes( span_kind: str, remote_resource_type: str, remote_resource_identifier: str, + cloudformation_primary_identifier: str, ) -> None: attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name()) @@ -540,7 +715,21 @@ def _assert_aws_attributes( if remote_resource_type != "None": self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_TYPE, remote_resource_type) if remote_resource_identifier != "None": - self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + if self._is_valid_regex(remote_resource_identifier): + self._assert_match_attribute( + attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier + ) + else: + self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + if cloudformation_primary_identifier != "None": + if self._is_valid_regex(remote_resource_identifier): + self._assert_match_attribute( + attributes_dict, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, cloudformation_primary_identifier + ) + else: + self._assert_str_attribute( + attributes_dict, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, cloudformation_primary_identifier + ) # See comment above AWS_LOCAL_OPERATION self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, span_kind) @@ -562,6 +751,7 @@ def _assert_semantic_conventions_span_attributes( kwargs.get("remote_operation"), status_code, kwargs.get("request_specific_attributes", {}), + kwargs.get("response_specific_attributes", {}), ) # pylint: disable=unidiomatic-typecheck @@ -572,6 +762,7 @@ def _assert_semantic_conventions_attributes( operation: str, status_code: int, request_specific_attributes: dict, + response_specific_attributes: dict, ) -> None: attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) self._assert_str_attribute(attributes_dict, SpanAttributes.RPC_METHOD, operation) @@ -587,6 +778,15 @@ def _assert_semantic_conventions_attributes( self._assert_int_attribute(attributes_dict, key, value) else: self._assert_array_value_ddb_table_name(attributes_dict, key, value) + for key, value in response_specific_attributes.items(): + if self._is_valid_regex(value): + self._assert_match_attribute(attributes_dict, key, value) + elif isinstance(value, str): + self._assert_str_attribute(attributes_dict, key, value) + elif isinstance(value, int): + self._assert_int_attribute(attributes_dict, key, value) + else: + self._assert_array_value_ddb_table_name(attributes_dict, key, value) @override def _assert_metric_attributes( @@ -623,7 +823,10 @@ def _assert_metric_attributes( if remote_resource_type != "None": self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_TYPE, remote_resource_type) if remote_resource_identifier != "None": - self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + if self._is_valid_regex(remote_resource_identifier): + self._assert_match_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + else: + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) self.check_sum(metric_name, dependency_dp.sum, expected_sum) attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(service_dp.attributes) diff --git a/contract-tests/tests/test/amazon/utils/application_signals_constants.py b/contract-tests/tests/test/amazon/utils/application_signals_constants.py index 9f3a625ac..93b6e084c 100644 --- a/contract-tests/tests/test/amazon/utils/application_signals_constants.py +++ b/contract-tests/tests/test/amazon/utils/application_signals_constants.py @@ -10,6 +10,7 @@ FAULT_METRIC: str = "fault" # Attribute names +AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER: str = "aws.remote.resource.cfn.primary.identifier" AWS_LOCAL_SERVICE: str = "aws.local.service" AWS_LOCAL_OPERATION: str = "aws.local.operation" AWS_REMOTE_DB_USER: str = "aws.remote.db.user"