From 831b76ba5f89ff51749439effe0426b0d62dafd7 Mon Sep 17 00:00:00 2001 From: Michael He <53622546+yiyuan-he@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:45:49 -0700 Subject: [PATCH] feat: Support a list of new AWS resources for AWS Python SDK (#265) ### *Description of changes:* These changes add auto-instrumentation support for the following AWS resources. Additionally, a new attribute for `aws.remote.resource.cfn.primary.identifier` is also added for each new resource. - Populate `aws.sns.topic.arn` in Span by extracting `TopicArn` from the request body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html - The CFN Id should be the ARN of the Topic. - Populate `aws.secretsmanager.secret.arn` in Span by extracting `ARN` from response body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html - The CFN Id should be the ARN of the Secret. - Populate `aws.stepfunctions.state_machine.arn` in Span by extracting `stateMachineArn` from the request body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html - The CFN Id should be the ARN of the State Machine. - Populate `aws.stepfunctions.activity.arn` in Span by extracting `activityArn` from the request body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-activity.html - The CFN Id should be the ARN of the Activity. - Populate `aws.lambda.function.name` in Span by extracting `FunctionName` from the request body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html - The CFN Id should be the ARN of the Lambda Function. - We populate `aws.lambda.function.arn` in Span by extraction `FunctionArn` from the response body because users are allowed to pass in both the name and ARN in the request body to retrieve the Lambda Function. - Populate `aws.lambda.resource_mapping.id` in Span by extracting `UUID` from the request body. - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html - The CFN Id should be the UUID of the Event Source Mapping. ### *Test Plan:* Set up a client-server with auto-instrumentation to verify that the correct span data is being generated. ![sns_topic_arn_span_data_verification](https://github.com/user-attachments/assets/f744ef66-169e-4056-9151-eb17d74258c8) ![secretsmanager_secret_arn_span_data_verification](https://github.com/user-attachments/assets/ee3cd0d6-3ecd-4f55-8471-b69eea4a369a) ![sfn_statemachine_arn_span_data_verification](https://github.com/user-attachments/assets/cd857153-c61c-4368-9d9f-eb91a90dc0f3) ![sfn_activity_arn_span_data_verification](https://github.com/user-attachments/assets/6e5d20d3-00c4-4dc2-a692-91e26e1f13f3) ![lambda_function_name_span_data_verification](https://github.com/user-attachments/assets/d019c94a-b944-4fa3-be62-b80ab3209899) ![lambda_event_source_mapping_uuid_span_data_verification](https://github.com/user-attachments/assets/968f0488-9440-457a-828e-1a1e4e6f2c5f) Unit Tests for Instrumentation Patches and AWS Metric Attribute Generators. ![instrumentation_patch_unit_test_verification](https://github.com/user-attachments/assets/5322a47e-dc7e-4b67-9c6f-158cffde6787) ![aws_metric_attribute_generator_unit_test_verification](https://github.com/user-attachments/assets/8672e0d6-eb05-424c-9005-281c239053c0) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- .../distro/_aws_attribute_keys.py | 7 + .../distro/_aws_metric_attribute_generator.py | 47 +++++- .../distro/patches/_botocore_patches.py | 134 +++++++++++++++++- .../test_aws_metric_attribute_generator.py | 65 +++++++++ .../distro/test_instrumentation_patch.py | 79 +++++++++++ 5 files changed, 330 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py index 6ecd8a846..855b9c42a 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py @@ -23,3 +23,10 @@ AWS_BEDROCK_KNOWLEDGE_BASE_ID: str = "aws.bedrock.knowledge_base.id" AWS_BEDROCK_AGENT_ID: str = "aws.bedrock.agent.id" AWS_BEDROCK_GUARDRAIL_ID: str = "aws.bedrock.guardrail.id" +AWS_SECRETSMANAGER_SECRET_ARN: str = "aws.secretsmanager.secret.arn" +AWS_SNS_TOPIC_ARN: str = "aws.sns.topic.arn" +AWS_STEPFUNCTIONS_STATEMACHINE_ARN: str = "aws.stepfunctions.state_machine.arn" +AWS_STEPFUNCTIONS_ACTIVITY_ARN: str = "aws.stepfunctions.activity.arn" +AWS_LAMBDA_FUNCTION_NAME: str = "aws.lambda.function.name" +AWS_LAMBDA_RESOURCEMAPPING_ID: str = "aws.lambda.resource_mapping.id" +AWS_LAMBDA_FUNCTION_ARN: str = "aws.lambda.function.arn" 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 efa8661c1..40a4fd46b 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 @@ -12,6 +12,9 @@ AWS_BEDROCK_KNOWLEDGE_BASE_ID, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, AWS_KINESIS_STREAM_NAME, + AWS_LAMBDA_FUNCTION_ARN, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_RESOURCEMAPPING_ID, AWS_LOCAL_OPERATION, AWS_LOCAL_SERVICE, AWS_REMOTE_DB_USER, @@ -19,9 +22,13 @@ AWS_REMOTE_RESOURCE_IDENTIFIER, AWS_REMOTE_RESOURCE_TYPE, AWS_REMOTE_SERVICE, + AWS_SECRETSMANAGER_SECRET_ARN, + AWS_SNS_TOPIC_ARN, AWS_SPAN_KIND, AWS_SQS_QUEUE_NAME, AWS_SQS_QUEUE_URL, + AWS_STEPFUNCTIONS_ACTIVITY_ARN, + AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) from amazon.opentelemetry.distro._aws_span_processing_util import ( GEN_AI_REQUEST_MODEL, @@ -88,6 +95,10 @@ _NORMALIZED_SQS_SERVICE_NAME: str = "AWS::SQS" _NORMALIZED_BEDROCK_SERVICE_NAME: str = "AWS::Bedrock" _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME: str = "AWS::BedrockRuntime" +_NORMALIZED_SECRETSMANAGER_SERVICE_NAME: str = "AWS::SecretsManager" +_NORMALIZED_SNS_SERVICE_NAME: str = "AWS::SNS" +_NORMALIZED_STEPFUNCTIONS_SERVICE_NAME: str = "AWS::StepFunctions" +_NORMALIZED_LAMBDA_SERVICE_NAME: str = "AWS::Lambda" _DB_CONNECTION_STRING_TYPE: str = "DB::Connection" # Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. @@ -309,6 +320,9 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str "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, } return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name) return service_name @@ -359,7 +373,7 @@ def _generate_remote_operation(span: ReadableSpan) -> str: return remote_operation -# pylint: disable=too-many-branches +# pylint: disable=too-many-branches,too-many-statements def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttributes) -> None: """ Remote resource attributes {@link AwsAttributeKeys#AWS_REMOTE_RESOURCE_TYPE} and {@link @@ -416,6 +430,37 @@ 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 is_key_present(span, AWS_SECRETSMANAGER_SECRET_ARN): + remote_resource_type = _NORMALIZED_SECRETSMANAGER_SERVICE_NAME + "::Secret" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_SECRETSMANAGER_SECRET_ARN)).split( + ":" + )[-1] + cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_SECRETSMANAGER_SECRET_ARN)) + elif is_key_present(span, AWS_SNS_TOPIC_ARN): + remote_resource_type = _NORMALIZED_SNS_SERVICE_NAME + "::Topic" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_SNS_TOPIC_ARN)).split(":")[-1] + cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_SNS_TOPIC_ARN)) + elif is_key_present(span, AWS_STEPFUNCTIONS_STATEMACHINE_ARN): + remote_resource_type = _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME + "::StateMachine" + remote_resource_identifier = _escape_delimiters( + span.attributes.get(AWS_STEPFUNCTIONS_STATEMACHINE_ARN) + ).split(":")[-1] + cloudformation_primary_identifier = _escape_delimiters( + span.attributes.get(AWS_STEPFUNCTIONS_STATEMACHINE_ARN) + ) + elif is_key_present(span, AWS_STEPFUNCTIONS_ACTIVITY_ARN): + remote_resource_type = _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME + "::Activity" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN)).split( + ":" + )[-1] + cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN)) + elif is_key_present(span, AWS_LAMBDA_RESOURCEMAPPING_ID): + remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_RESOURCEMAPPING_ID)) + elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME): + remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)) + cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)) elif is_db_span(span): remote_resource_type = _DB_CONNECTION_STRING_TYPE remote_resource_identifier = _get_db_connection(span) 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 d635f7d1b..069e39c87 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 @@ -5,8 +5,15 @@ from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_KINESIS_STREAM_NAME, + AWS_LAMBDA_FUNCTION_ARN, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_RESOURCEMAPPING_ID, + AWS_SECRETSMANAGER_SECRET_ARN, + AWS_SNS_TOPIC_ARN, AWS_SQS_QUEUE_NAME, AWS_SQS_QUEUE_URL, + AWS_STEPFUNCTIONS_ACTIVITY_ARN, + AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) from amazon.opentelemetry.distro.patches._bedrock_patches import ( # noqa # pylint: disable=unused-import _BedrockAgentExtension, @@ -15,9 +22,12 @@ _BedrockRuntimeExtension, ) from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS +from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension +from opentelemetry.instrumentation.botocore.extensions.sns import _SnsExtension from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension -from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension +from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension, _BotoResultT from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.span import Span def _apply_botocore_instrumentation_patches() -> None: @@ -29,6 +39,100 @@ def _apply_botocore_instrumentation_patches() -> None: _apply_botocore_s3_patch() _apply_botocore_sqs_patch() _apply_botocore_bedrock_patch() + _apply_botocore_secretsmanager_patch() + _apply_botocore_sns_patch() + _apply_botocore_stepfunctions_patch() + _apply_botocore_lambda_patch() + + +def _apply_botocore_lambda_patch() -> None: + """Botocore instrumentation patch for Lambda + + This patch adds an extension to the upstream's list of known extensions for Lambda. + Extensions allow for custom logic for adding service-specific information to spans, + such as attributes. Specifically, we are adding logic to add the + `aws.lambda.function.name` and `aws.lambda.resource_mapping.id` attributes + + Sidenote: There exists SpanAttributes.FAAS_INVOKED_NAME for invoke operations + in upstream. However, we want to cover more operations to extract 'FunctionName', + so we define `aws.lambda.function.name` separately. Additionally, this helps + us maintain naming consistency with the other AWS resources. + """ + old_extract_attributes = _LambdaExtension.extract_attributes + + def patch_extract_attributes(self, attributes: _AttributeMapT): + old_extract_attributes(self, attributes) + # This param can be passed as an arn or a name. We standardize it to be the name. + function_name_param = self._call_context.params.get("FunctionName") + if function_name_param: + function_name = function_name_param + if function_name_param.startswith("arn:aws:lambda:"): + function_name = function_name_param.split(":")[-1] + attributes[AWS_LAMBDA_FUNCTION_NAME] = function_name + resource_mapping_id = self._call_context.params.get("UUID") + if resource_mapping_id: + attributes[AWS_LAMBDA_RESOURCEMAPPING_ID] = resource_mapping_id + + old_on_success = _LambdaExtension.on_success + + def patch_on_success(self, span: Span, result: _BotoResultT): + old_on_success(self, span, result) + lambda_configuration = result.get("Configuration", {}) + function_arn = lambda_configuration.get("FunctionArn") + if function_arn: + span.set_attribute(AWS_LAMBDA_FUNCTION_ARN, function_arn) + + _LambdaExtension.extract_attributes = patch_extract_attributes + _LambdaExtension.on_success = patch_on_success + + +def _apply_botocore_stepfunctions_patch() -> None: + """Botocore instrumentation patch for StepFunctions + + This patch adds an extension to the upstream's list of known extensions for + StepFunctions. Extensions allow for custom logic for adding service-specific + information to spans, such as attributes. Specifically, we are adding logic + to add the `aws.stepfunctions.state_machine.arn` and `aws.stepfunctions.activity.arn` + attributes, to be used to generate RemoteTarget and achieve partity with the + Java instrumentation. + """ + _KNOWN_EXTENSIONS["stepfunctions"] = _lazy_load(".", "_StepFunctionsExtension") + + +def _apply_botocore_sns_patch() -> None: + """Botocore instrumentation patch for SNS + + This patch adds an extension to the upstream's list of known extensions for SNS. + Extensions allow for custom logic for adding service-specific information to + spans, such as attributes. Specifically, we are adding logic to add the + `aws.sns.topic.arn` attribute, to be used to generate RemoteTarget and achieve + parity with the Java instrumentation. + + Sidenote: There exists SpanAttributes.MESSAGING_DESTINATION_NAME in the upstream + logic that we could re-purpose here. We do not use it here to maintain consistent + naming patterns with other AWS resources. + """ + old_extract_attributes = _SnsExtension.extract_attributes + + def patch_extract_attributes(self, attributes: _AttributeMapT): + old_extract_attributes(self, attributes) + topic_arn = self._call_context.params.get("TopicArn") + if topic_arn: + attributes[AWS_SNS_TOPIC_ARN] = topic_arn + + _SnsExtension.extract_attributes = patch_extract_attributes + + +def _apply_botocore_secretsmanager_patch() -> None: + """Botocore instrumentation patch for SecretsManager + + This patch adds an extension to the upstream's list of known extension for SecretsManager. + Extensions allow for custom logic for adding service-specific information to spans, such as + attributes. Specifically, we are adding logic to add the `aws.secretsmanager.secret.arn` + attribute, to be used to generate RemoteTarget and achieve parity with the Java + instrumentation. + """ + _KNOWN_EXTENSIONS["secretsmanager"] = _lazy_load(".", "_SecretsManagerExtension") def _apply_botocore_kinesis_patch() -> None: @@ -108,6 +212,34 @@ def loader(): # END The OpenTelemetry Authors code +class _StepFunctionsExtension(_AwsSdkExtension): + def extract_attributes(self, attributes: _AttributeMapT): + state_machine_arn = self._call_context.params.get("stateMachineArn") + if state_machine_arn: + attributes[AWS_STEPFUNCTIONS_STATEMACHINE_ARN] = state_machine_arn + activity_arn = self._call_context.params.get("activityArn") + if activity_arn: + attributes[AWS_STEPFUNCTIONS_ACTIVITY_ARN] = activity_arn + + +class _SecretsManagerExtension(_AwsSdkExtension): + def extract_attributes(self, attributes: _AttributeMapT): + """ + SecretId can be secret name or secret arn, the function extracts attributes + only if the SecretId parameter is provided as an arn which starts with + `arn:aws:secretsmanager:` + """ + secret_id = self._call_context.params.get("SecretId") + if secret_id and secret_id.startswith("arn:aws:secretsmanager:"): + attributes[AWS_SECRETSMANAGER_SECRET_ARN] = secret_id + + # pylint: disable=no-self-use + def on_success(self, span: Span, result: _BotoResultT): + secret_arn = result.get("ARN") + if secret_arn: + span.set_attribute(AWS_SECRETSMANAGER_SECRET_ARN, secret_arn) + + class _S3Extension(_AwsSdkExtension): def extract_attributes(self, attributes: _AttributeMapT): bucket_name = self._call_context.params.get("Bucket") 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 a9ff24698..f9da8bb9a 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 @@ -14,6 +14,8 @@ AWS_BEDROCK_KNOWLEDGE_BASE_ID, AWS_CONSUMER_PARENT_SPAN_KIND, AWS_KINESIS_STREAM_NAME, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_RESOURCEMAPPING_ID, AWS_LOCAL_OPERATION, AWS_LOCAL_SERVICE, AWS_REMOTE_DB_USER, @@ -21,9 +23,13 @@ AWS_REMOTE_RESOURCE_IDENTIFIER, AWS_REMOTE_RESOURCE_TYPE, AWS_REMOTE_SERVICE, + AWS_SECRETSMANAGER_SECRET_ARN, + AWS_SNS_TOPIC_ARN, AWS_SPAN_KIND, AWS_SQS_QUEUE_NAME, AWS_SQS_QUEUE_URL, + AWS_STEPFUNCTIONS_ACTIVITY_ARN, + AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) from amazon.opentelemetry.distro._aws_metric_attribute_generator import _AwsMetricAttributeGenerator from amazon.opentelemetry.distro._aws_span_processing_util import GEN_AI_REQUEST_MODEL @@ -877,6 +883,9 @@ 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("Secrets Manager", "AWS::SecretsManager") + self.validate_aws_sdk_service_normalization("SNS", "AWS::SNS") + self.validate_aws_sdk_service_normalization("SFN", "AWS::StepFunctions") def validate_aws_sdk_service_normalization(self, service_name: str, expected_remote_service: str): self._mock_attribute([SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE], ["aws-api", service_name]) @@ -1093,6 +1102,62 @@ def test_sdk_client_span_with_remote_resource_attributes(self): self._validate_remote_resource_attributes("AWS::Bedrock::Model", "test.service_^^id") self._mock_attribute([GEN_AI_REQUEST_MODEL], [None]) + # Validate behaviour of AWS_SECRETSMANAGER_SECRET_ARN attribute, then remove it. + self._mock_attribute( + [AWS_SECRETSMANAGER_SECRET_ARN], + ["arn:aws:secretsmanager:us-east-1:123456789012:secret:secret_name-lERW9H"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::SecretsManager::Secret", "secret_name-lERW9H") + self._mock_attribute([AWS_SECRETSMANAGER_SECRET_ARN], [None]) + + # Validate behaviour of AWS_SNS_TOPIC_ARN attribute, then remove it. + self._mock_attribute([AWS_SNS_TOPIC_ARN], ["arn:aws:sns:us-west-2:012345678901:test_topic"], keys, values) + self._validate_remote_resource_attributes("AWS::SNS::Topic", "test_topic") + self._mock_attribute([AWS_SNS_TOPIC_ARN], [None]) + + # Validate behaviour of AWS_STEPFUNCTIONS_STATEMACHINE_ARN attribute, then remove it. + self._mock_attribute( + [AWS_STEPFUNCTIONS_STATEMACHINE_ARN], + ["arn:aws:states:us-east-1:123456789012:stateMachine:test_state_machine"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::StepFunctions::StateMachine", "test_state_machine") + self._mock_attribute([AWS_STEPFUNCTIONS_STATEMACHINE_ARN], [None]) + + # Validate behaviour of AWS_STEPFUNCTIONS_ACTIVITY_ARN attribute, then remove it. + self._mock_attribute( + [AWS_STEPFUNCTIONS_ACTIVITY_ARN], + ["arn:aws:states:us-east-1:007003123456789012:activity:testActivity"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::StepFunctions::Activity", "testActivity") + self._mock_attribute([AWS_STEPFUNCTIONS_ACTIVITY_ARN], [None]) + + # Validate behaviour of AWS_LAMBDA_FUNCTION_NAME attribute, then remove it. + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], ["aws_lambda_function_name"], keys, values) + self._validate_remote_resource_attributes("AWS::Lambda::Function", "aws_lambda_function_name") + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], [None]) + + # Validate behaviour of AWS_LAMBDA_RESOURCEMAPPING_ID attribute, then remove it. + self._mock_attribute([AWS_LAMBDA_RESOURCEMAPPING_ID], ["aws_event_source_mapping_id"], keys, values) + self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id") + self._mock_attribute([AWS_LAMBDA_RESOURCEMAPPING_ID], [None]) + + # Validate behaviour of both AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_RESOURCE_MAPPING_ID, + # then remove it. + self._mock_attribute( + [AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_RESOURCEMAPPING_ID], + ["aws_lambda_function_name", "aws_event_source_mapping_id"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id") + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_RESOURCEMAPPING_ID], [None, None]) + self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None]) def test_client_db_span_with_remote_resource_attributes(self): diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 16004c726..b27d5e799 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -26,6 +26,12 @@ _BEDROCK_KNOWLEDGEBASE_ID: str = "KnowledgeBaseId" _GEN_AI_SYSTEM: str = "aws_bedrock" _GEN_AI_REQUEST_MODEL: str = "genAiReuqestModelId" +_SECRET_ARN: str = "arn:aws:secretsmanager:us-west-2:000000000000:secret:testSecret-ABCDEF" +_TOPIC_ARN: str = "topicArn" +_STATE_MACHINE_ARN: str = "arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine" +_ACTIVITY_ARN: str = "arn:aws:states:us-east-1:007003123456789012:activity:testActivity" +_LAMBDA_FUNCTION_NAME: str = "lambdaFunctionName" +_LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" # Patch names GET_DISTRIBUTION_PATCH: str = ( @@ -141,6 +147,18 @@ def _test_unpatched_botocore_instrumentation(self): # BedrockRuntime self.assertFalse("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") + # SecretsManager + self.assertFalse("secretsmanager" in _KNOWN_EXTENSIONS, "Upstream has added a SecretsManager extension") + + # SNS + self.assertTrue("sns" in _KNOWN_EXTENSIONS, "Upstream has removed the SNS extension") + + # StepFunctions + self.assertFalse("stepfunctions" in _KNOWN_EXTENSIONS, "Upstream has added a StepFunctions extension") + + # Lambda + self.assertTrue("lambda" in _KNOWN_EXTENSIONS, "Upstream has removed the Lambda extension") + def _test_unpatched_gevent_instrumentation(self): self.assertFalse(gevent.monkey.is_module_patched("os"), "gevent os module has been patched") self.assertFalse(gevent.monkey.is_module_patched("thread"), "gevent thread module has been patched") @@ -200,6 +218,37 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(bedrock_runtime_attributes["gen_ai.system"], _GEN_AI_SYSTEM) self.assertEqual(bedrock_runtime_attributes["gen_ai.request.model"], _GEN_AI_REQUEST_MODEL) + # SecretsManager + self.assertTrue("secretsmanager" in _KNOWN_EXTENSIONS) + secretsmanager_attributes: Dict[str, str] = _do_extract_secretsmanager_attributes() + self.assertTrue("aws.secretsmanager.secret.arn" in secretsmanager_attributes) + self.assertEqual(secretsmanager_attributes["aws.secretsmanager.secret.arn"], _SECRET_ARN) + secretsmanager_success_attributes: Dict[str, str] = _do_on_success_secretsmanager() + self.assertTrue("aws.secretsmanager.secret.arn" in secretsmanager_success_attributes) + self.assertEqual(secretsmanager_success_attributes["aws.secretsmanager.secret.arn"], _SECRET_ARN) + + # SNS + self.assertTrue("sns" in _KNOWN_EXTENSIONS) + sns_attributes: Dict[str, str] = _do_extract_sns_attributes() + self.assertTrue("aws.sns.topic.arn" in sns_attributes) + self.assertEqual(sns_attributes["aws.sns.topic.arn"], _TOPIC_ARN) + + # StepFunctions + self.assertTrue("stepfunctions" in _KNOWN_EXTENSIONS) + stepfunctions_attributes: Dict[str, str] = _do_extract_stepfunctions_attributes() + self.assertTrue("aws.stepfunctions.state_machine.arn" in stepfunctions_attributes) + self.assertEqual(stepfunctions_attributes["aws.stepfunctions.state_machine.arn"], _STATE_MACHINE_ARN) + self.assertTrue("aws.stepfunctions.activity.arn" in stepfunctions_attributes) + self.assertEqual(stepfunctions_attributes["aws.stepfunctions.activity.arn"], _ACTIVITY_ARN) + + # Lambda + self.assertTrue("lambda" in _KNOWN_EXTENSIONS) + lambda_attributes: Dict[str, str] = _do_extract_lambda_attributes() + self.assertTrue("aws.lambda.function.name" in lambda_attributes) + self.assertEqual(lambda_attributes["aws.lambda.function.name"], _LAMBDA_FUNCTION_NAME) + self.assertTrue("aws.lambda.resource_mapping.id" in lambda_attributes) + self.assertEqual(lambda_attributes["aws.lambda.resource_mapping.id"], _LAMBDA_SOURCE_MAPPING_ID) + def _test_patched_gevent_os_ssl_instrumentation(self): # Only ssl and os module should have been patched since the environment variable was set to 'os, ssl' self.assertTrue(gevent.monkey.is_module_patched("ssl"), "gevent ssl module has not been patched") @@ -358,6 +407,36 @@ def _do_on_success_bedrock(service, operation=None) -> Dict[str, str]: return _do_on_success(service, result, operation) +def _do_extract_secretsmanager_attributes() -> Dict[str, str]: + service_name: str = "secretsmanager" + params: Dict[str, str] = {"SecretId": _SECRET_ARN} + return _do_extract_attributes(service_name, params) + + +def _do_on_success_secretsmanager() -> Dict[str, str]: + service_name: str = "secretsmanager" + result: Dict[str, Any] = {"ARN": _SECRET_ARN} + return _do_on_success(service_name, result) + + +def _do_extract_sns_attributes() -> Dict[str, str]: + service_name: str = "sns" + params: Dict[str, str] = {"TopicArn": _TOPIC_ARN} + return _do_extract_attributes(service_name, params) + + +def _do_extract_stepfunctions_attributes() -> Dict[str, str]: + service_name: str = "stepfunctions" + params: Dict[str, str] = {"stateMachineArn": _STATE_MACHINE_ARN, "activityArn": _ACTIVITY_ARN} + return _do_extract_attributes(service_name, params) + + +def _do_extract_lambda_attributes() -> Dict[str, str]: + service_name: str = "lambda" + params: Dict[str, str] = {"FunctionName": _LAMBDA_FUNCTION_NAME, "UUID": _LAMBDA_SOURCE_MAPPING_ID} + return _do_extract_attributes(service_name, params) + + def _do_extract_attributes(service_name: str, params: Dict[str, Any], operation: str = None) -> Dict[str, str]: mock_call_context: MagicMock = MagicMock() mock_call_context.params = params