Repository: open-telemetry/opentelemetry-python

Issue:
Align SDK with resource detection spec
[Resource detection has been merged into the spec](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/sdk.md#detecting-resource-information-from-the-environment). The SDK should be aligned with the spec. This may already be the case.

**Is your feature request related to a problem?**
No

**Describe the solution you'd like**
The resource detection spec is flexible and doesn't prescribe anything. Currently, we have this code:
https://github.com/open-telemetry/opentelemetry-python/blob/c435600ea465b6de0c6f49a18ba3c95a4ead734e/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py#L65-L120

We could:
- Leave everything as it is. The spec doesn't say we need a `ResourceDetector` interface or `get_aggregated_resources()` nor does it say it shouldn't be there.
- Remove the `ResourceDetector` interface and `get_aggregated_resources()`
- Replace the `ResourceDetector` interface with a `Callable[[bool], Resource]` function param, since the spec only says
  > Resource detector packages MUST provide a method that returns a resource. 

Thoughts?


Discussion:
I like having the `ResourceDetector` interface, it gives users an idea of how to implement their own detector. 
Is `get_aggregated_resources()` something we expect users to call explictly? As a user, I'd expect to just instantiate my own `ResourceDetector` (s) and pass them into the `TracerProvider`. So instead of doing something like [this](https://github.com/open-telemetry/opentelemetry-python/blob/master/docs/examples/basic_tracer/resources.py#L10), maybe we can change `TracerProvider` to accept a sequence of `ResoureDetectors`, and then we merge them upon instantiation of the provider underneath (so we don't have to expose `get_aggregated_resources()` to the user.
The `Callable[[bool], Resource]` is a cool idea. I'd think they would have to depend on the SDK since they would be returning a `Resource`? 
> I'd think they would have to depend on the SDK since they would be returning a Resource?

Oh ya, my bad. Updated the issue description.

> maybe we can change `TracerProvider` to accept a sequence of `ResoureDetectors`, and then we merge them upon instantiation of the provider underneath

I like this interface better, not sure if that would be taking too much liberty interpreting the spec though. I think having a util to run the detectors in parallel is worth having, once way or another.
As discussed in the SIG meeting `get_aggregated_resources` can stay but the recommendation is to pass in merged resources, for which users can use get_aggregated_resources 
Cool, so the only thing remaining is calling and using the `OTELResourceDetector` in the providers

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
@@ -12,6 +12,71 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""
+This package implements `OpenTelemetry Resources
+<https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/sdk.md#resource-sdk>`_:
+
+    *A Resource is an immutable representation of the entity producing
+    telemetry. For example, a process producing telemetry that is running in
+    a container on Kubernetes has a Pod name, it is in a namespace and
+    possibly is part of a Deployment which also has a name. All three of
+    these attributes can be included in the Resource.*
+
+Resource objects are created with `Resource.create`, which accepts attributes
+(key-values). Resource attributes can also be passed at process invocation in
+the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should
+register your resource with the `opentelemetry.sdk.metrics.MeterProvider` and
+`opentelemetry.sdk.trace.TracerProvider` by passing them into their
+constructors. The `Resource` passed to a provider is available to the
+exporter, which can send on this information as it sees fit.
+
+.. code-block:: python
+
+    metrics.set_meter_provider(
+        MeterProvider(
+            resource=Resource.create({
+                "service.name": "shoppingcart",
+                "service.instance.id": "instance-12",
+            }),
+        ),
+    )
+    print(metrics.get_meter_provider().resource.attributes)
+
+    {'telemetry.sdk.language': 'python',
+    'telemetry.sdk.name': 'opentelemetry',
+    'telemetry.sdk.version': '0.13.dev0',
+    'service.name': 'shoppingcart',
+    'service.instance.id': 'instance-12'}
+
+Note that the OpenTelemetry project documents certain `"standard attributes"
+<https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/semantic_conventions/README.md>`_
+that have prescribed semantic meanings, for example ``service.name`` in the
+above example.
+
+.. envvar:: OTEL_RESOURCE_ATTRIBUTES
+
+The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource
+attributes to be passed to the SDK at process invocation. The attributes from
+:envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to
+`Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower*
+priority. Attributes should be in the format ``key1=value1,key2=value2``.
+Additional details are available `in the specification
+<https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable>`_.
+
+.. code-block:: console
+
+    $ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - <<EOF
+    import pprint
+    from opentelemetry.sdk.resources import Resource
+    pprint.pprint(Resource.create({"will_be_overridden": "bar"}).attributes)
+    EOF
+    {'service.name': 'shoppingcard',
+    'telemetry.sdk.language': 'python',
+    'telemetry.sdk.name': 'opentelemetry',
+    'telemetry.sdk.version': '0.13.dev0',
+    'will_be_overridden': 'bar'}
+ """
+
 import abc
 import concurrent.futures
 import logging
@@ -33,6 +98,7 @@
 OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution(
     "opentelemetry-sdk"
 ).version
+OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES"
 
 
 class Resource:
@@ -40,10 +106,12 @@ def __init__(self, attributes: Attributes):
         self._attributes = attributes.copy()
 
     @staticmethod
-    def create(attributes: Attributes) -> "Resource":
+    def create(attributes: typing.Optional[Attributes] = None) -> "Resource":
         if not attributes:
-            return _DEFAULT_RESOURCE
-        return _DEFAULT_RESOURCE.merge(Resource(attributes))
+            resource = _DEFAULT_RESOURCE
+        else:
+            resource = _DEFAULT_RESOURCE.merge(Resource(attributes))
+        return resource.merge(OTELResourceDetector().detect())
 
     @staticmethod
     def create_empty() -> "Resource":
@@ -92,7 +160,7 @@ def detect(self) -> "Resource":
 class OTELResourceDetector(ResourceDetector):
     # pylint: disable=no-self-use
     def detect(self) -> "Resource":
-        env_resources_items = os.environ.get("OTEL_RESOURCE_ATTRIBUTES")
+        env_resources_items = os.environ.get(OTEL_RESOURCE_ATTRIBUTES)
         env_resource_map = {}
         if env_resources_items:
             env_resource_map = {


Test patch:
diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py
--- a/opentelemetry-sdk/tests/metrics/test_metrics.py
+++ b/opentelemetry-sdk/tests/metrics/test_metrics.py
@@ -40,7 +40,7 @@ def test_resource_empty(self):
         meter_provider = metrics.MeterProvider()
         meter = meter_provider.get_meter(__name__)
         # pylint: disable=protected-access
-        self.assertIs(meter.resource, resources._DEFAULT_RESOURCE)
+        self.assertEqual(meter.resource, resources._DEFAULT_RESOURCE)
 
     def test_start_pipeline(self):
         exporter = mock.Mock()
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
@@ -22,6 +22,12 @@
 
 
 class TestResources(unittest.TestCase):
+    def setUp(self) -> None:
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = ""
+
+    def tearDown(self) -> None:
+        os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES)
+
     def test_create(self):
         attributes = {
             "service": "ui",
@@ -44,14 +50,22 @@ def test_create(self):
         self.assertIsInstance(resource, resources.Resource)
         self.assertEqual(resource.attributes, expected_attributes)
 
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value"
+        resource = resources.Resource.create(attributes)
+        self.assertIsInstance(resource, resources.Resource)
+        expected_with_envar = expected_attributes.copy()
+        expected_with_envar["key"] = "value"
+        self.assertEqual(resource.attributes, expected_with_envar)
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = ""
+
         resource = resources.Resource.create_empty()
-        self.assertIs(resource, resources._EMPTY_RESOURCE)
+        self.assertEqual(resource, resources._EMPTY_RESOURCE)
 
         resource = resources.Resource.create(None)
-        self.assertIs(resource, resources._DEFAULT_RESOURCE)
+        self.assertEqual(resource, resources._DEFAULT_RESOURCE)
 
         resource = resources.Resource.create({})
-        self.assertIs(resource, resources._DEFAULT_RESOURCE)
+        self.assertEqual(resource, resources._DEFAULT_RESOURCE)
 
     def test_resource_merge(self):
         left = resources.Resource({"service": "ui"})
@@ -184,36 +198,38 @@ def test_resource_detector_raise_error(self):
 
 class TestOTELResourceDetector(unittest.TestCase):
     def setUp(self) -> None:
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ""
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = ""
 
     def tearDown(self) -> None:
-        os.environ.pop("OTEL_RESOURCE_ATTRIBUTES")
+        os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES)
 
     def test_empty(self):
         detector = resources.OTELResourceDetector()
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ""
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = ""
         self.assertEqual(detector.detect(), resources.Resource.create_empty())
 
     def test_one(self):
         detector = resources.OTELResourceDetector()
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v"
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v"
         self.assertEqual(detector.detect(), resources.Resource({"k": "v"}))
 
     def test_one_with_whitespace(self):
         detector = resources.OTELResourceDetector()
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "    k  = v   "
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "    k  = v   "
         self.assertEqual(detector.detect(), resources.Resource({"k": "v"}))
 
     def test_multiple(self):
         detector = resources.OTELResourceDetector()
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v,k2=v2"
+        os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2"
         self.assertEqual(
             detector.detect(), resources.Resource({"k": "v", "k2": "v2"})
         )
 
     def test_multiple_with_whitespace(self):
         detector = resources.OTELResourceDetector()
-        os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "    k  = v  , k2   = v2 "
+        os.environ[
+            resources.OTEL_RESOURCE_ATTRIBUTES
+        ] = "    k  = v  , k2   = v2 "
         self.assertEqual(
             detector.detect(), resources.Resource({"k": "v", "k2": "v2"})
         )
diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py
--- a/opentelemetry-sdk/tests/trace/test_trace.py
+++ b/opentelemetry-sdk/tests/trace/test_trace.py
@@ -396,7 +396,7 @@ def test_default_span_resource(self):
         tracer = tracer_provider.get_tracer(__name__)
         span = tracer.start_span("root")
         # pylint: disable=protected-access
-        self.assertIs(span.resource, resources._DEFAULT_RESOURCE)
+        self.assertEqual(span.resource, resources._DEFAULT_RESOURCE)
 
     def test_span_context_remote_flag(self):
         tracer = new_tracer()