Repository: open-telemetry/opentelemetry-python

Issue:
Implement Process Resource detector
We currently support the [process runtime resource detector](https://github.com/open-telemetry/opentelemetry-python/pull/2660). Implement a resource detector for [process](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/process.md#process).


Discussion:
If you don't mind, can you assign it to me?

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
@@ -58,6 +58,7 @@
 import abc
 import concurrent.futures
 import logging
+import os
 import sys
 import typing
 from json import dumps
@@ -74,11 +75,15 @@
 from opentelemetry.util._importlib_metadata import entry_points, version
 from opentelemetry.util.types import AttributeValue
 
+try:
+    import psutil
+except ImportError:
+    psutil = None
+
 LabelValue = AttributeValue
 Attributes = typing.Dict[str, LabelValue]
 logger = logging.getLogger(__name__)
 
-
 CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER
 CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID
 CLOUD_REGION = ResourceAttributes.CLOUD_REGION
@@ -117,6 +122,7 @@
 OS_TYPE = ResourceAttributes.OS_TYPE
 OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION
 PROCESS_PID = ResourceAttributes.PROCESS_PID
+PROCESS_PARENT_PID = ResourceAttributes.PROCESS_PARENT_PID
 PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME
 PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH
 PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND
@@ -135,7 +141,6 @@
 TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION
 TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE
 
-
 _OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk")
 
 
@@ -180,7 +185,6 @@ def create(
             otel_experimental_resource_detectors.append("otel")
 
         for resource_detector in otel_experimental_resource_detectors:
-
             resource_detectors.append(
                 next(
                     iter(
@@ -337,14 +341,32 @@ def detect(self) -> "Resource":
                 else sys.version_info,
             )
         )
-
-        return Resource(
-            {
-                PROCESS_RUNTIME_DESCRIPTION: sys.version,
-                PROCESS_RUNTIME_NAME: sys.implementation.name,
-                PROCESS_RUNTIME_VERSION: _runtime_version,
-            }
-        )
+        _process_pid = os.getpid()
+        _process_executable_name = sys.executable
+        _process_executable_path = os.path.dirname(_process_executable_name)
+        _process_command = sys.argv[0]
+        _process_command_line = " ".join(sys.argv)
+        _process_command_args = sys.argv[1:]
+        resource_info = {
+            PROCESS_RUNTIME_DESCRIPTION: sys.version,
+            PROCESS_RUNTIME_NAME: sys.implementation.name,
+            PROCESS_RUNTIME_VERSION: _runtime_version,
+            PROCESS_PID: _process_pid,
+            PROCESS_EXECUTABLE_NAME: _process_executable_name,
+            PROCESS_EXECUTABLE_PATH: _process_executable_path,
+            PROCESS_COMMAND: _process_command,
+            PROCESS_COMMAND_LINE: _process_command_line,
+            PROCESS_COMMAND_ARGS: _process_command_args,
+        }
+        if hasattr(os, "getppid"):
+            # pypy3 does not have getppid()
+            resource_info[PROCESS_PARENT_PID] = os.getppid()
+
+        if psutil is not None:
+            process = psutil.Process()
+            resource_info[PROCESS_OWNER] = process.username()
+
+        return Resource(resource_info)
 
 
 def get_aggregated_resources(


Test patch:
diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py
--- a/opentelemetry-sdk/tests/resources/test_resources.py
+++ b/opentelemetry-sdk/tests/resources/test_resources.py
@@ -11,9 +11,8 @@
 # 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.
-
-# pylint: disable=protected-access
-
+import os
+import sys
 import unittest
 import uuid
 from logging import ERROR, WARNING
@@ -30,7 +29,14 @@
     _OPENTELEMETRY_SDK_VERSION,
     OTEL_RESOURCE_ATTRIBUTES,
     OTEL_SERVICE_NAME,
+    PROCESS_COMMAND,
+    PROCESS_COMMAND_ARGS,
+    PROCESS_COMMAND_LINE,
     PROCESS_EXECUTABLE_NAME,
+    PROCESS_EXECUTABLE_PATH,
+    PROCESS_OWNER,
+    PROCESS_PARENT_PID,
+    PROCESS_PID,
     PROCESS_RUNTIME_DESCRIPTION,
     PROCESS_RUNTIME_NAME,
     PROCESS_RUNTIME_VERSION,
@@ -45,6 +51,11 @@
     get_aggregated_resources,
 )
 
+try:
+    import psutil
+except ImportError:
+    psutil = None
+
 
 class TestResources(unittest.TestCase):
     def setUp(self) -> None:
@@ -524,6 +535,10 @@ def test_service_name_env_precedence(self):
             Resource({"service.name": "from-service-name"}),
         )
 
+    @patch(
+        "sys.argv",
+        ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"],
+    )
     def test_process_detector(self):
         initial_resource = Resource({"foo": "bar"})
         aggregated_resource = get_aggregated_resources(
@@ -543,8 +558,42 @@ def test_process_detector(self):
             aggregated_resource.attributes.keys(),
         )
 
-    def test_resource_detector_entry_points_default(self):
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_PID], os.getpid()
+        )
+        if hasattr(os, "getppid"):
+            self.assertEqual(
+                aggregated_resource.attributes[PROCESS_PARENT_PID],
+                os.getppid(),
+            )
+
+        if psutil is not None:
+            self.assertEqual(
+                aggregated_resource.attributes[PROCESS_OWNER],
+                psutil.Process().username(),
+            )
 
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_EXECUTABLE_NAME],
+            sys.executable,
+        )
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_EXECUTABLE_PATH],
+            os.path.dirname(sys.executable),
+        )
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_COMMAND], sys.argv[0]
+        )
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_COMMAND_LINE],
+            " ".join(sys.argv),
+        )
+        self.assertEqual(
+            aggregated_resource.attributes[PROCESS_COMMAND_ARGS],
+            tuple(sys.argv[1:]),
+        )
+
+    def test_resource_detector_entry_points_default(self):
         resource = Resource({}).create()
 
         self.assertEqual(
@@ -644,7 +693,9 @@ def test_resource_detector_entry_points_otel(self):
                 resource.attributes["telemetry.sdk.name"], "opentelemetry"
             )
             self.assertEqual(
-                resource.attributes["service.name"], "unknown_service"
+                resource.attributes["service.name"],
+                "unknown_service:"
+                + resource.attributes["process.executable.name"],
             )
             self.assertEqual(resource.attributes["a"], "b")
             self.assertEqual(resource.attributes["c"], "d")