From e16dc48681d166e7ab0ee45f3ee33c37e6f918d6 Mon Sep 17 00:00:00 2001 From: Zirak Date: Thu, 1 Aug 2024 21:05:44 +0000 Subject: [PATCH] sdk: Implement basic os resource detector (#3992) --- CHANGELOG.md | 3 +- opentelemetry-sdk/pyproject.toml | 1 + .../opentelemetry/sdk/resources/__init__.py | 107 ++++++++++++++++-- .../tests/resources/test_resources.py | 54 +++++++++ 4 files changed, 154 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2fc5dc3be..f4fb7ec4899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Drop Final annotation from Enum in semantic conventions ([#4085](https://github.com/open-telemetry/opentelemetry-python/pull/4085)) - Update log export example to not use root logger ([#4090](https://github.com/open-telemetry/opentelemetry-python/pull/4090)) +- sdk: Add OS resource detector + ([#3992](https://github.com/open-telemetry/opentelemetry-python/pull/3992)) ## Version 1.26.0/0.47b0 (2024-07-25) @@ -1633,4 +1635,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3778](https://github.com/open-telemetry/opentelemetry-python/pull/3778)) - Fix license field in pyproject.toml files ([#3803](https://github.com/open-telemetry/opentelemetry-python/pull/3803)) - diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 53742078526..d5f40054c98 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -67,6 +67,7 @@ console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" [project.entry-points.opentelemetry_resource_detector] otel = "opentelemetry.sdk.resources:OTELResourceDetector" process = "opentelemetry.sdk.resources:ProcessResourceDetector" +os = "opentelemetry.sdk.resources:OsResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index bfb43fae038..0ebd42349c4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -59,6 +59,7 @@ import concurrent.futures import logging import os +import platform import sys import typing from json import dumps @@ -125,8 +126,9 @@ KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME -OS_TYPE = ResourceAttributes.OS_TYPE OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION +OS_TYPE = ResourceAttributes.OS_TYPE +OS_VERSION = ResourceAttributes.OS_VERSION PROCESS_PID = ResourceAttributes.PROCESS_PID PROCESS_PARENT_PID = ResourceAttributes.PROCESS_PARENT_PID PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME @@ -182,16 +184,17 @@ def create( if not attributes: attributes = {} - resource_detectors: List[ResourceDetector] = [] - - resource = _DEFAULT_RESOURCE - - otel_experimental_resource_detectors = environ.get( - OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "otel" - ).split(",") + otel_experimental_resource_detectors = {"otel"}.union( + { + otel_experimental_resource_detector.strip() + for otel_experimental_resource_detector in environ.get( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "" + ).split(",") + if otel_experimental_resource_detector + } + ) - if "otel" not in otel_experimental_resource_detectors: - otel_experimental_resource_detectors.append("otel") + resource_detectors: List[ResourceDetector] = [] resource_detector: str for resource_detector in otel_experimental_resource_detectors: @@ -384,6 +387,90 @@ def detect(self) -> "Resource": return Resource(resource_info) # type: ignore +class OsResourceDetector(ResourceDetector): + """Detect os resources based on `Operating System conventions `_.""" + + def detect(self) -> "Resource": + """Returns a resource with with ``os.type`` and ``os.version``. + + Python's platform library + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + To grab this information, Python's ``platform`` does not return what a + user might expect it to. Below is a breakdown of its return values in + different operating systems. + + .. code-block:: python + :caption: Linux + + >>> platform.system() + 'Linux' + >>> platform.release() + '6.5.0-35-generic' + >>> platform.version() + '#35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2' + + .. code-block:: python + :caption: MacOS + + >>> platform.system() + 'Darwin' + >>> platform.release() + '23.0.0' + >>> platform.version() + 'Darwin Kernel Version 23.0.0: Fri Sep 15 14:42:57 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T8112' + + .. code-block:: python + :caption: Windows + + >>> platform.system() + 'Windows' + >>> platform.release() + '2022Server' + >>> platform.version() + '10.0.20348' + + .. code-block:: python + :caption: FreeBSD + + >>> platform.system() + 'FreeBSD' + >>> platform.release() + '14.1-RELEASE' + >>> platform.version() + 'FreeBSD 14.1-RELEASE releng/14.1-n267679-10e31f0946d8 GENERIC' + + .. code-block:: python + :caption: Solaris + + >>> platform.system() + 'SunOS' + >>> platform.release() + '5.11' + >>> platform.version() + '11.4.0.15.0' + + """ + + os_type = platform.system().lower() + os_version = platform.release() + + # See docstring + if os_type == "windows": + os_version = platform.version() + # Align SunOS with conventions + elif os_type == "sunos": + os_type = "solaris" + os_version = platform.version() + + return Resource( + { + OS_TYPE: os_type, + OS_VERSION: os_version, + } + ) + + def get_aggregated_resources( detectors: typing.List["ResourceDetector"], initial_resource: typing.Optional[Resource] = None, diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 8a42d0c6d0f..70e90864a53 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -28,6 +28,8 @@ _DEFAULT_RESOURCE, _EMPTY_RESOURCE, _OPENTELEMETRY_SDK_VERSION, + OS_TYPE, + OS_VERSION, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, PROCESS_COMMAND, @@ -45,6 +47,7 @@ TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, + OsResourceDetector, OTELResourceDetector, ProcessResourceDetector, Resource, @@ -673,6 +676,24 @@ def test_resource_detector_entry_points_non_default(self): self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.schema_url, "") + @patch.dict( + environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: ""}, clear=True + ) + def test_resource_detector_entry_points_empty(self): + resource = Resource({}).create() + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + + @patch.dict( + environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "os"}, clear=True + ) + def test_resource_detector_entry_points_os(self): + resource = Resource({}).create() + + self.assertIn(OS_TYPE, resource.attributes) + self.assertIn(OS_VERSION, resource.attributes) + def test_resource_detector_entry_points_otel(self): """ Test that OTELResourceDetector-resource-generated attributes are @@ -723,3 +744,36 @@ def test_resource_detector_entry_points_otel(self): ) self.assertIn(PROCESS_RUNTIME_VERSION, resource.attributes.keys()) self.assertEqual(resource.schema_url, "") + + @patch("platform.system", lambda: "Linux") + @patch("platform.release", lambda: "666.5.0-35-generic") + def test_os_detector_linux(self): + resource = get_aggregated_resources( + [OsResourceDetector()], + Resource({}), + ) + + self.assertEqual(resource.attributes[OS_TYPE], "linux") + self.assertEqual(resource.attributes[OS_VERSION], "666.5.0-35-generic") + + @patch("platform.system", lambda: "Windows") + @patch("platform.version", lambda: "10.0.666") + def test_os_detector_windows(self): + resource = get_aggregated_resources( + [OsResourceDetector()], + Resource({}), + ) + + self.assertEqual(resource.attributes[OS_TYPE], "windows") + self.assertEqual(resource.attributes[OS_VERSION], "10.0.666") + + @patch("platform.system", lambda: "SunOS") + @patch("platform.version", lambda: "666.4.0.15.0") + def test_os_detector_solaris(self): + resource = get_aggregated_resources( + [OsResourceDetector()], + Resource({}), + ) + + self.assertEqual(resource.attributes[OS_TYPE], "solaris") + self.assertEqual(resource.attributes[OS_VERSION], "666.4.0.15.0")