Repository: open-telemetry/opentelemetry-python

Issue:
Resource aggregator method is missing Default Resource Bizmetric Attributes
**Describe your environment**

As of OTel Python 1.4

**Steps to reproduce**

If you want to use custom `ResourceDetector`s, we recommend you use the `get_aggregated_resources` method because it will automatically merge your custom `ResourceDetector` with the `OTELResourceDetector` (used to read `ResourceDetector` attributes from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` environment variables).

However, if you do that, you miss out on the "business metrics" attributes that get created with `Resource.create()`

https://github.com/open-telemetry/opentelemetry-python/blob/39927787cc911af8567731728a37afc24f935d1e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L246-L252

`Resource.create()` actually uses `OTELResourceDetector` too. So if you try to get the bizmetrics by doing `get_aggregated_resources([ MyCustomResourceDetector(), Resource.create() ])`, you will get `OTELResourceDetector` twice. (Once from `Resource.create()` and once from `get_aggregated_resources`. See the code here:

https://github.com/open-telemetry/opentelemetry-python/blob/39927787cc911af8567731728a37afc24f935d1e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L168-L170

https://github.com/open-telemetry/opentelemetry-python/blob/39927787cc911af8567731728a37afc24f935d1e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L295

**What is the expected behavior?**
Bizmetrics should be on _every_ span which uses the aggregated Resource from `get_aggregated_resources`

**What is the actual behavior?**
Using `get_aggregated_resources` as we recommend causes bizmetrics to be  missed.

**Additional context**

The most obvious solutions would be to
* (Option 1) Remove `OTELResourceDetector` from `get_aggregated_resources`'s default implementation so users can add either `OTELResourceDetector` or `Resource.create()` depending on what they want
* (Option 2) Replace `OTELResourceDetector` in `get_aggregated_resources` with `Resource.create()` so spans always get bizmetrics by default

Since we already 1.0+, I recommend Option 2. This will just "Add" attributes and won't break anyone using this method who expects `OTELResourceDetector` attributes to be there when they use `get_aggregated_resources`. They will just get bizmetric attributes for free.



Discussion:
Option 2 would require a one line fix:


```python3
    detectors = [OTELResourceDetector()] + detectors
```

becomes:

```python3
    detectors = [Resource.create()] + detectors
```
> * (Option 1) Remove `OTELResourceDetector` from `get_aggregated_resources`'s default implementation so users can add either `OTELResourceDetector` or `Resource.create()` depending on what they want

This can already be done by passing `Resource.create()` here, right?
https://github.com/open-telemetry/opentelemetry-python/blob/39927787cc911af8567731728a37afc24f935d1e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L284

For Option 2, you could just change the default for `initial_resource` to be `_DEFAULT_RESOURCE`?


> This can already be done by passing Resource.create() here, right?

Yes but it will duplicated adding `OTELResourceDetector()`.

> For Option 2, you could just change the default for initial_resource to be _DEFAULT_RESOURCE?

Yes that's a good point! But we would miss out of the other stuff that `Resource.create()` does automatically (like setting the service name to `unknown_service` if no service name is provided and which [is required by the spec](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions#service)):

https://github.com/open-telemetry/opentelemetry-python/blob/39927787cc911af8567731728a37afc24f935d1e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L171-L181

Also I don't think it would be an issue, but if we every treated `initial_resource` as a mutable argument then multiple calls to this method `get_aggregated_resource` would modify the same object right? It's that [Python gotcha with parameter initializers](https://stackoverflow.com/q/1132941/7460739). (The example uses a list but I think it applies to this object of type `Resource` too?). Again, shouldn't be an issue because we don't modify it, but it's probably safer to leave it as `None`?
I don't think `detectors = [Resource.create()] + detectors` would work because `Resource.create()` doesn't return a detector.

Can we just include `_DEFAULT_RESOURCE` in `OTELResourceDetector` and then modify `Resource.create()` to only use `OTELResourceDetector` by default?

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
@@ -291,15 +291,14 @@ def get_aggregated_resources(
     :param timeout: Number of seconds to wait for each detector to return
     :return:
     """
-    final_resource = initial_resource or _EMPTY_RESOURCE
-    detectors = [OTELResourceDetector()] + detectors
+    detectors_merged_resource = initial_resource or Resource.create()
 
     with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
         futures = [executor.submit(detector.detect) for detector in detectors]
         for detector_ind, future in enumerate(futures):
             detector = detectors[detector_ind]
             try:
-                detected_resources = future.result(timeout=timeout)
+                detected_resource = future.result(timeout=timeout)
             # pylint: disable=broad-except
             except Exception as ex:
                 if detector.raise_on_error:
@@ -307,7 +306,10 @@ def get_aggregated_resources(
                 logger.warning(
                     "Exception %s in detector %s, ignoring", ex, detector
                 )
-                detected_resources = _EMPTY_RESOURCE
+                detected_resource = _EMPTY_RESOURCE
             finally:
-                final_resource = final_resource.merge(detected_resources)
-    return final_resource
+                detectors_merged_resource = detectors_merged_resource.merge(
+                    detected_resource
+                )
+
+    return detectors_merged_resource


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
@@ -230,9 +230,18 @@ def test_invalid_resource_attribute_values(self):
 
     def test_aggregated_resources_no_detectors(self):
         aggregated_resources = resources.get_aggregated_resources([])
-        self.assertEqual(aggregated_resources, resources.Resource.get_empty())
+        self.assertEqual(
+            aggregated_resources,
+            resources._DEFAULT_RESOURCE.merge(
+                resources.Resource(
+                    {resources.SERVICE_NAME: "unknown_service"}, ""
+                )
+            ),
+        )
 
-    def test_aggregated_resources_with_static_resource(self):
+    def test_aggregated_resources_with_default_destroying_static_resource(
+        self,
+    ):
         static_resource = resources.Resource({"static_key": "static_value"})
 
         self.assertEqual(
@@ -280,13 +289,19 @@ def test_aggregated_resources_multiple_detectors(self):
             resources.get_aggregated_resources(
                 [resource_detector1, resource_detector2, resource_detector3]
             ),
-            resources.Resource(
-                {
-                    "key1": "value1",
-                    "key2": "try_to_overwrite_existing_value",
-                    "key3": "try_to_overwrite_existing_value",
-                    "key4": "value4",
-                }
+            resources._DEFAULT_RESOURCE.merge(
+                resources.Resource(
+                    {resources.SERVICE_NAME: "unknown_service"}, ""
+                )
+            ).merge(
+                resources.Resource(
+                    {
+                        "key1": "value1",
+                        "key2": "try_to_overwrite_existing_value",
+                        "key3": "try_to_overwrite_existing_value",
+                        "key4": "value4",
+                    }
+                )
             ),
         )
 
@@ -321,9 +336,15 @@ def test_aggregated_resources_different_schema_urls(self):
             resources.get_aggregated_resources(
                 [resource_detector1, resource_detector2]
             ),
-            resources.Resource(
-                {"key1": "value1", "key2": "value2", "key3": "value3"},
-                "url1",
+            resources._DEFAULT_RESOURCE.merge(
+                resources.Resource(
+                    {resources.SERVICE_NAME: "unknown_service"}, ""
+                )
+            ).merge(
+                resources.Resource(
+                    {"key1": "value1", "key2": "value2", "key3": "value3"},
+                    "url1",
+                )
             ),
         )
         with self.assertLogs(level=ERROR) as log_entry:
@@ -331,8 +352,14 @@ def test_aggregated_resources_different_schema_urls(self):
                 resources.get_aggregated_resources(
                     [resource_detector2, resource_detector3]
                 ),
-                resources.Resource(
-                    {"key2": "value2", "key3": "value3"}, "url1"
+                resources._DEFAULT_RESOURCE.merge(
+                    resources.Resource(
+                        {resources.SERVICE_NAME: "unknown_service"}, ""
+                    )
+                ).merge(
+                    resources.Resource(
+                        {"key2": "value2", "key3": "value3"}, "url1"
+                    )
                 ),
             )
             self.assertIn("url1", log_entry.output[0])
@@ -347,14 +374,20 @@ def test_aggregated_resources_different_schema_urls(self):
                         resource_detector1,
                     ]
                 ),
-                resources.Resource(
-                    {
-                        "key1": "value1",
-                        "key2": "try_to_overwrite_existing_value",
-                        "key3": "try_to_overwrite_existing_value",
-                        "key4": "value4",
-                    },
-                    "url1",
+                resources._DEFAULT_RESOURCE.merge(
+                    resources.Resource(
+                        {resources.SERVICE_NAME: "unknown_service"}, ""
+                    )
+                ).merge(
+                    resources.Resource(
+                        {
+                            "key1": "value1",
+                            "key2": "try_to_overwrite_existing_value",
+                            "key3": "try_to_overwrite_existing_value",
+                            "key4": "value4",
+                        },
+                        "url1",
+                    )
                 ),
             )
             self.assertIn("url1", log_entry.output[0])
@@ -366,7 +399,11 @@ def test_resource_detector_ignore_error(self):
         resource_detector.raise_on_error = False
         self.assertEqual(
             resources.get_aggregated_resources([resource_detector]),
-            resources.Resource.get_empty(),
+            resources._DEFAULT_RESOURCE.merge(
+                resources.Resource(
+                    {resources.SERVICE_NAME: "unknown_service"}, ""
+                )
+            ),
         )
 
     def test_resource_detector_raise_error(self):