From 4bfa280ec0c6228513c6a00edeb36e364f5e9b21 Mon Sep 17 00:00:00 2001 From: Raphael Manke Date: Mon, 9 Feb 2026 20:48:28 +0100 Subject: [PATCH 1/2] feat(lambda): read cloud.account.id from symlink in Lambda resource detector Read /tmp/.otel-account-id via os.readlink() and set the cloud.account.id resource attribute when the symlink exists. Silently skip when the symlink is absent (OSError caught). --- .../sdk/extension/aws/resource/_lambda.py | 47 ++++--- .../tests/resource/test__lambda.py | 115 ++++++++++++++++++ 2 files changed, 143 insertions(+), 19 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/_lambda.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/_lambda.py index 6f8fe68048..9b40afe1f0 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/_lambda.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/_lambda.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import os from os import environ from opentelemetry.sdk.resources import Resource, ResourceDetector @@ -24,6 +25,8 @@ logger = logging.getLogger(__name__) +_ACCOUNT_ID_SYMLINK_PATH = "/tmp/.otel-account-id" + class AwsLambdaResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS @@ -34,25 +37,31 @@ class AwsLambdaResourceDetector(ResourceDetector): def detect(self) -> "Resource": try: - return Resource( - { - ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, - ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_LAMBDA.value, - ResourceAttributes.CLOUD_REGION: environ["AWS_REGION"], - ResourceAttributes.FAAS_NAME: environ[ - "AWS_LAMBDA_FUNCTION_NAME" - ], - ResourceAttributes.FAAS_VERSION: environ[ - "AWS_LAMBDA_FUNCTION_VERSION" - ], - ResourceAttributes.FAAS_INSTANCE: environ[ - "AWS_LAMBDA_LOG_STREAM_NAME" - ], - ResourceAttributes.FAAS_MAX_MEMORY: int( - environ["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] - ), - } - ) + attributes = { + ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, + ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_LAMBDA.value, + ResourceAttributes.CLOUD_REGION: environ["AWS_REGION"], + ResourceAttributes.FAAS_NAME: environ[ + "AWS_LAMBDA_FUNCTION_NAME" + ], + ResourceAttributes.FAAS_VERSION: environ[ + "AWS_LAMBDA_FUNCTION_VERSION" + ], + ResourceAttributes.FAAS_INSTANCE: environ[ + "AWS_LAMBDA_LOG_STREAM_NAME" + ], + ResourceAttributes.FAAS_MAX_MEMORY: int( + environ["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] + ), + } + + try: + account_id = os.readlink(_ACCOUNT_ID_SYMLINK_PATH) + attributes[ResourceAttributes.CLOUD_ACCOUNT_ID] = account_id + except OSError: + pass + + return Resource(attributes) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py index e183525e49..02b75f804f 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import tempfile import unittest from collections import OrderedDict from unittest.mock import patch from opentelemetry.sdk.extension.aws.resource._lambda import ( # pylint: disable=no-name-in-module AwsLambdaResourceDetector, + _ACCOUNT_ID_SYMLINK_PATH, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, @@ -61,3 +64,115 @@ def test_simple_create(self): self.assertDictEqual( actual.attributes.copy(), OrderedDict(MockLambdaResourceAttributes) ) + + @patch.dict( + "os.environ", + { + "AWS_REGION": MockLambdaResourceAttributes[ + ResourceAttributes.CLOUD_REGION + ], + "AWS_LAMBDA_FUNCTION_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_NAME + ], + "AWS_LAMBDA_FUNCTION_VERSION": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_VERSION + ], + "AWS_LAMBDA_LOG_STREAM_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_INSTANCE + ], + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": f"{MockLambdaResourceAttributes[ResourceAttributes.FAAS_MAX_MEMORY]}", + }, + clear=True, + ) + def test_account_id_from_symlink(self): + """When the account ID symlink exists, cloud.account.id is set.""" + symlink_path = None + try: + tmpdir = tempfile.mkdtemp() + symlink_path = os.path.join(tmpdir, ".otel-account-id") + os.symlink("123456789012", symlink_path) + with patch( + "opentelemetry.sdk.extension.aws.resource._lambda._ACCOUNT_ID_SYMLINK_PATH", + symlink_path, + ): + actual = AwsLambdaResourceDetector().detect() + self.assertEqual( + actual.attributes[ResourceAttributes.CLOUD_ACCOUNT_ID], + "123456789012", + ) + finally: + if symlink_path and os.path.islink(symlink_path): + os.unlink(symlink_path) + if tmpdir: + os.rmdir(tmpdir) + + @patch.dict( + "os.environ", + { + "AWS_REGION": MockLambdaResourceAttributes[ + ResourceAttributes.CLOUD_REGION + ], + "AWS_LAMBDA_FUNCTION_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_NAME + ], + "AWS_LAMBDA_FUNCTION_VERSION": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_VERSION + ], + "AWS_LAMBDA_LOG_STREAM_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_INSTANCE + ], + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": f"{MockLambdaResourceAttributes[ResourceAttributes.FAAS_MAX_MEMORY]}", + }, + clear=True, + ) + def test_account_id_missing_symlink(self): + """When the symlink does not exist, cloud.account.id is absent and no exception is raised.""" + with patch( + "opentelemetry.sdk.extension.aws.resource._lambda._ACCOUNT_ID_SYMLINK_PATH", + "/tmp/.otel-account-id-nonexistent", + ): + actual = AwsLambdaResourceDetector().detect() + self.assertNotIn( + ResourceAttributes.CLOUD_ACCOUNT_ID, actual.attributes + ) + + @patch.dict( + "os.environ", + { + "AWS_REGION": MockLambdaResourceAttributes[ + ResourceAttributes.CLOUD_REGION + ], + "AWS_LAMBDA_FUNCTION_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_NAME + ], + "AWS_LAMBDA_FUNCTION_VERSION": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_VERSION + ], + "AWS_LAMBDA_LOG_STREAM_NAME": MockLambdaResourceAttributes[ + ResourceAttributes.FAAS_INSTANCE + ], + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": f"{MockLambdaResourceAttributes[ResourceAttributes.FAAS_MAX_MEMORY]}", + }, + clear=True, + ) + def test_account_id_preserves_leading_zeros(self): + """Leading zeros in the account ID are preserved (treated as string).""" + symlink_path = None + try: + tmpdir = tempfile.mkdtemp() + symlink_path = os.path.join(tmpdir, ".otel-account-id") + os.symlink("000123456789", symlink_path) + with patch( + "opentelemetry.sdk.extension.aws.resource._lambda._ACCOUNT_ID_SYMLINK_PATH", + symlink_path, + ): + actual = AwsLambdaResourceDetector().detect() + self.assertEqual( + actual.attributes[ResourceAttributes.CLOUD_ACCOUNT_ID], + "000123456789", + ) + finally: + if symlink_path and os.path.islink(symlink_path): + os.unlink(symlink_path) + if tmpdir: + os.rmdir(tmpdir) From c57e1095af53992119331345b702325d8888dbcd Mon Sep 17 00:00:00 2001 From: Raphael Manke Date: Mon, 9 Feb 2026 21:54:29 +0100 Subject: [PATCH 2/2] docs: add CHANGELOG entry for cloud.account.id symlink --- CHANGELOG.md | 2 ++ sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f649e3886..12356bebc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `opentelemetry-sdk-extension-aws`: Read `cloud.account.id` from symlink created by the OTel Lambda Extension in the Lambda resource detector + ([#4183](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4183)) - `opentelemetry-instrumentation-asgi`: Add exemplars for `http.server.request.duration` and `http.server.duration` metrics ([#3739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3739)) - `opentelemetry-instrumentation-wsgi`: Add exemplars for `http.server.request.duration` and `http.server.duration` metrics diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md b/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md index a66805003c..1ffc86373d 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md +++ b/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Read `cloud.account.id` from symlink created by the OTel Lambda Extension in the Lambda resource detector + ([#4183](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4183)) + ## Version 2.1.0 (2024-12-24) - Make ec2 resource detector silent when loaded outside AWS