Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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,
)