Skip to content

Commit e16dc48

Browse files
authored
sdk: Implement basic os resource detector (#3992)
1 parent d981cf1 commit e16dc48

File tree

4 files changed

+154
-11
lines changed

4 files changed

+154
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Drop Final annotation from Enum in semantic conventions
1515
([#4085](https://github.com/open-telemetry/opentelemetry-python/pull/4085))
1616
- Update log export example to not use root logger ([#4090](https://github.com/open-telemetry/opentelemetry-python/pull/4090))
17+
- sdk: Add OS resource detector
18+
([#3992](https://github.com/open-telemetry/opentelemetry-python/pull/3992))
1719

1820
## Version 1.26.0/0.47b0 (2024-07-25)
1921

@@ -1633,4 +1635,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16331635
([#3778](https://github.com/open-telemetry/opentelemetry-python/pull/3778))
16341636
- Fix license field in pyproject.toml files
16351637
([#3803](https://github.com/open-telemetry/opentelemetry-python/pull/3803))
1636-

opentelemetry-sdk/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter"
6767
[project.entry-points.opentelemetry_resource_detector]
6868
otel = "opentelemetry.sdk.resources:OTELResourceDetector"
6969
process = "opentelemetry.sdk.resources:ProcessResourceDetector"
70+
os = "opentelemetry.sdk.resources:OsResourceDetector"
7071

7172
[project.urls]
7273
Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk"

opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import concurrent.futures
6060
import logging
6161
import os
62+
import platform
6263
import sys
6364
import typing
6465
from json import dumps
@@ -125,8 +126,9 @@
125126
KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME
126127
KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID
127128
KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME
128-
OS_TYPE = ResourceAttributes.OS_TYPE
129129
OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION
130+
OS_TYPE = ResourceAttributes.OS_TYPE
131+
OS_VERSION = ResourceAttributes.OS_VERSION
130132
PROCESS_PID = ResourceAttributes.PROCESS_PID
131133
PROCESS_PARENT_PID = ResourceAttributes.PROCESS_PARENT_PID
132134
PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME
@@ -182,16 +184,17 @@ def create(
182184
if not attributes:
183185
attributes = {}
184186

185-
resource_detectors: List[ResourceDetector] = []
186-
187-
resource = _DEFAULT_RESOURCE
188-
189-
otel_experimental_resource_detectors = environ.get(
190-
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "otel"
191-
).split(",")
187+
otel_experimental_resource_detectors = {"otel"}.union(
188+
{
189+
otel_experimental_resource_detector.strip()
190+
for otel_experimental_resource_detector in environ.get(
191+
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, ""
192+
).split(",")
193+
if otel_experimental_resource_detector
194+
}
195+
)
192196

193-
if "otel" not in otel_experimental_resource_detectors:
194-
otel_experimental_resource_detectors.append("otel")
197+
resource_detectors: List[ResourceDetector] = []
195198

196199
resource_detector: str
197200
for resource_detector in otel_experimental_resource_detectors:
@@ -384,6 +387,90 @@ def detect(self) -> "Resource":
384387
return Resource(resource_info) # type: ignore
385388

386389

390+
class OsResourceDetector(ResourceDetector):
391+
"""Detect os resources based on `Operating System conventions <https://opentelemetry.io/docs/specs/semconv/resource/os/>`_."""
392+
393+
def detect(self) -> "Resource":
394+
"""Returns a resource with with ``os.type`` and ``os.version``.
395+
396+
Python's platform library
397+
~~~~~~~~~~~~~~~~~~~~~~~~~
398+
399+
To grab this information, Python's ``platform`` does not return what a
400+
user might expect it to. Below is a breakdown of its return values in
401+
different operating systems.
402+
403+
.. code-block:: python
404+
:caption: Linux
405+
406+
>>> platform.system()
407+
'Linux'
408+
>>> platform.release()
409+
'6.5.0-35-generic'
410+
>>> platform.version()
411+
'#35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2'
412+
413+
.. code-block:: python
414+
:caption: MacOS
415+
416+
>>> platform.system()
417+
'Darwin'
418+
>>> platform.release()
419+
'23.0.0'
420+
>>> platform.version()
421+
'Darwin Kernel Version 23.0.0: Fri Sep 15 14:42:57 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T8112'
422+
423+
.. code-block:: python
424+
:caption: Windows
425+
426+
>>> platform.system()
427+
'Windows'
428+
>>> platform.release()
429+
'2022Server'
430+
>>> platform.version()
431+
'10.0.20348'
432+
433+
.. code-block:: python
434+
:caption: FreeBSD
435+
436+
>>> platform.system()
437+
'FreeBSD'
438+
>>> platform.release()
439+
'14.1-RELEASE'
440+
>>> platform.version()
441+
'FreeBSD 14.1-RELEASE releng/14.1-n267679-10e31f0946d8 GENERIC'
442+
443+
.. code-block:: python
444+
:caption: Solaris
445+
446+
>>> platform.system()
447+
'SunOS'
448+
>>> platform.release()
449+
'5.11'
450+
>>> platform.version()
451+
'11.4.0.15.0'
452+
453+
"""
454+
455+
os_type = platform.system().lower()
456+
os_version = platform.release()
457+
458+
# See docstring
459+
if os_type == "windows":
460+
os_version = platform.version()
461+
# Align SunOS with conventions
462+
elif os_type == "sunos":
463+
os_type = "solaris"
464+
os_version = platform.version()
465+
466+
return Resource(
467+
{
468+
OS_TYPE: os_type,
469+
OS_VERSION: os_version,
470+
}
471+
)
472+
473+
387474
def get_aggregated_resources(
388475
detectors: typing.List["ResourceDetector"],
389476
initial_resource: typing.Optional[Resource] = None,

opentelemetry-sdk/tests/resources/test_resources.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
_DEFAULT_RESOURCE,
2929
_EMPTY_RESOURCE,
3030
_OPENTELEMETRY_SDK_VERSION,
31+
OS_TYPE,
32+
OS_VERSION,
3133
OTEL_RESOURCE_ATTRIBUTES,
3234
OTEL_SERVICE_NAME,
3335
PROCESS_COMMAND,
@@ -45,6 +47,7 @@
4547
TELEMETRY_SDK_LANGUAGE,
4648
TELEMETRY_SDK_NAME,
4749
TELEMETRY_SDK_VERSION,
50+
OsResourceDetector,
4851
OTELResourceDetector,
4952
ProcessResourceDetector,
5053
Resource,
@@ -673,6 +676,24 @@ def test_resource_detector_entry_points_non_default(self):
673676
self.assertEqual(resource.attributes["a"], "b")
674677
self.assertEqual(resource.schema_url, "")
675678

679+
@patch.dict(
680+
environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: ""}, clear=True
681+
)
682+
def test_resource_detector_entry_points_empty(self):
683+
resource = Resource({}).create()
684+
self.assertEqual(
685+
resource.attributes["telemetry.sdk.language"], "python"
686+
)
687+
688+
@patch.dict(
689+
environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "os"}, clear=True
690+
)
691+
def test_resource_detector_entry_points_os(self):
692+
resource = Resource({}).create()
693+
694+
self.assertIn(OS_TYPE, resource.attributes)
695+
self.assertIn(OS_VERSION, resource.attributes)
696+
676697
def test_resource_detector_entry_points_otel(self):
677698
"""
678699
Test that OTELResourceDetector-resource-generated attributes are
@@ -723,3 +744,36 @@ def test_resource_detector_entry_points_otel(self):
723744
)
724745
self.assertIn(PROCESS_RUNTIME_VERSION, resource.attributes.keys())
725746
self.assertEqual(resource.schema_url, "")
747+
748+
@patch("platform.system", lambda: "Linux")
749+
@patch("platform.release", lambda: "666.5.0-35-generic")
750+
def test_os_detector_linux(self):
751+
resource = get_aggregated_resources(
752+
[OsResourceDetector()],
753+
Resource({}),
754+
)
755+
756+
self.assertEqual(resource.attributes[OS_TYPE], "linux")
757+
self.assertEqual(resource.attributes[OS_VERSION], "666.5.0-35-generic")
758+
759+
@patch("platform.system", lambda: "Windows")
760+
@patch("platform.version", lambda: "10.0.666")
761+
def test_os_detector_windows(self):
762+
resource = get_aggregated_resources(
763+
[OsResourceDetector()],
764+
Resource({}),
765+
)
766+
767+
self.assertEqual(resource.attributes[OS_TYPE], "windows")
768+
self.assertEqual(resource.attributes[OS_VERSION], "10.0.666")
769+
770+
@patch("platform.system", lambda: "SunOS")
771+
@patch("platform.version", lambda: "666.4.0.15.0")
772+
def test_os_detector_solaris(self):
773+
resource = get_aggregated_resources(
774+
[OsResourceDetector()],
775+
Resource({}),
776+
)
777+
778+
self.assertEqual(resource.attributes[OS_TYPE], "solaris")
779+
self.assertEqual(resource.attributes[OS_VERSION], "666.4.0.15.0")

0 commit comments

Comments
 (0)