diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e438ce0b6..7c959bec2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +### Added +- `opentelemetry-instrumentation`: botocore: Add support for AWS Secrets Manager semantic convention attribute + ([#3765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3765)) + ## Version 1.37.0/0.58b0 (2025-09-11) ### Fixed diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py index 599be4236c..dd8ba24e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py @@ -35,6 +35,9 @@ def loader(): "bedrock-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"), "dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"), "lambda": _lazy_load(".lmbd", "_LambdaExtension"), + "secretsmanager": _lazy_load( + ".secretsmanager", "_SecretsManagerExtension" + ), "stepfunctions": _lazy_load(".sfns", "_StepFunctionsExtension"), "sns": _lazy_load(".sns", "_SnsExtension"), "sqs": _lazy_load(".sqs", "_SqsExtension"), diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/secretsmanager.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/secretsmanager.py new file mode 100644 index 0000000000..f1b1d8ba21 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/secretsmanager.py @@ -0,0 +1,45 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from opentelemetry.instrumentation.botocore.extensions.types import ( + _AttributeMapT, + _AwsSdkExtension, + _BotocoreInstrumentorContext, + _BotoResultT, +) +from opentelemetry.semconv._incubating.attributes.aws_attributes import ( + AWS_SECRETSMANAGER_SECRET_ARN, +) +from opentelemetry.trace.span import Span + + +class _SecretsManagerExtension(_AwsSdkExtension): + def extract_attributes(self, attributes: _AttributeMapT): + """ + SecretId is extracted if a secret ARN, the function extracts the attribute + 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 + + def on_success( + self, + span: Span, + result: _BotoResultT, + instrumentor_context: _BotocoreInstrumentorContext, + ): + secret_arn = result.get("ARN") + if secret_arn: + span.set_attribute(AWS_SECRETSMANAGER_SECRET_ARN, secret_arn) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_secretsmanager.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_secretsmanager.py new file mode 100644 index 0000000000..d2fe8deb91 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_secretsmanager.py @@ -0,0 +1,86 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import botocore.session +from moto import mock_aws + +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor +from opentelemetry.semconv._incubating.attributes.aws_attributes import ( + AWS_SECRETSMANAGER_SECRET_ARN, +) +from opentelemetry.test.test_base import TestBase + + +class TestSecretsManagerExtension(TestBase): + def setUp(self): + super().setUp() + BotocoreInstrumentor().instrument() + session = botocore.session.get_session() + session.set_credentials( + access_key="access-key", secret_key="secret-key" + ) + self.region = "us-west-2" + self.client = session.create_client( + "secretsmanager", region_name=self.region + ) + + def tearDown(self): + super().tearDown() + BotocoreInstrumentor().uninstrument() + + def create_secret_and_get_arn(self, name: str = "test-secret") -> str: + """ + Create a secret in mocked Secrets Manager and return its ARN. + """ + # Clear spans before creating secret for helper method + self.memory_exporter.clear() + response = self.client.create_secret( + Name=name, SecretString="test-secret-value" + ) + return response["ARN"] + + @mock_aws + def test_tag_resource_with_arn(self): + secret_arn = self.create_secret_and_get_arn() + + self.client.tag_resource( + SecretId=secret_arn, Tags=[{"Key": "Environment", "Value": "Test"}] + ) + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] # tag_resource span + self.assertEqual( + span.attributes[AWS_SECRETSMANAGER_SECRET_ARN], + secret_arn, + ) + + @mock_aws + def test_create_secret(self): + secret_name = "test-secret" + response = self.client.create_secret( + Name=secret_name, SecretString="test-secret-value" + ) + secret_arn = response["ARN"] + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] # create_secret span + # Should capture ARN from response + self.assertEqual( + span.attributes[AWS_SECRETSMANAGER_SECRET_ARN], + secret_arn, + )