Repository: open-telemetry/opentelemetry-python

Issue:
SDK should populate telemetry.sdk.* resource attributes
As per the [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/semantic_conventions/README.md#telemetry-sdk), the SDK must set the `opentelemetry.sdk.name` to `opentelemetry`. It would be nice if the other `telemetry.sdk.*` values were also set.

See discussion here on implementation details: https://github.com/open-telemetry/opentelemetry-python/issues/932


Discussion:

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py
@@ -478,7 +478,7 @@ class MeterProvider(metrics_api.MeterProvider):
     def __init__(
         self,
         stateful=True,
-        resource: Resource = Resource.create_empty(),
+        resource: Resource = Resource.create({}),
         shutdown_on_exit: bool = True,
     ):
         self.stateful = stateful
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
@@ -19,11 +19,22 @@
 import typing
 from json import dumps
 
+import pkg_resources
+
 LabelValue = typing.Union[str, bool, int, float]
 Labels = typing.Dict[str, LabelValue]
 logger = logging.getLogger(__name__)
 
 
+TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language"
+TELEMETRY_SDK_NAME = "telemetry.sdk.name"
+TELEMETRY_SDK_VERSION = "telemetry.sdk.version"
+
+OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution(
+    "opentelemetry-sdk"
+).version
+
+
 class Resource:
     def __init__(self, labels: Labels):
         self._labels = labels.copy()
@@ -31,8 +42,8 @@ def __init__(self, labels: Labels):
     @staticmethod
     def create(labels: Labels) -> "Resource":
         if not labels:
-            return _EMPTY_RESOURCE
-        return Resource(labels)
+            return _DEFAULT_RESOURCE
+        return _DEFAULT_RESOURCE.merge(Resource(labels))
 
     @staticmethod
     def create_empty() -> "Resource":
@@ -60,6 +71,13 @@ def __hash__(self):
 
 
 _EMPTY_RESOURCE = Resource({})
+_DEFAULT_RESOURCE = Resource(
+    {
+        TELEMETRY_SDK_LANGUAGE: "python",
+        TELEMETRY_SDK_NAME: "opentelemetry",
+        TELEMETRY_SDK_VERSION: OPENTELEMETRY_SDK_VERSION,
+    }
+)
 
 
 class ResourceDetector(abc.ABC):
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
@@ -364,7 +364,7 @@ def __init__(
         parent: Optional[trace_api.SpanContext] = None,
         sampler: Optional[sampling.Sampler] = None,
         trace_config: None = None,  # TODO
-        resource: Resource = Resource.create_empty(),
+        resource: Resource = Resource.create({}),
         attributes: types.Attributes = None,  # TODO
         events: Sequence[Event] = None,  # TODO
         links: Sequence[trace_api.Link] = (),
@@ -821,7 +821,7 @@ class TracerProvider(trace_api.TracerProvider):
     def __init__(
         self,
         sampler: sampling.Sampler = sampling.DEFAULT_ON,
-        resource: Resource = Resource.create_empty(),
+        resource: Resource = Resource.create({}),
         shutdown_on_exit: bool = True,
         active_span_processor: Union[
             SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor


Test patch:
diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
--- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
+++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
@@ -215,9 +215,11 @@ def test_translate_to_jaeger(self):
         otel_spans[0].end(end_time=end_times[0])
 
         otel_spans[1].start(start_time=start_times[1])
+        otel_spans[1].resource = Resource({})
         otel_spans[1].end(end_time=end_times[1])
 
         otel_spans[2].start(start_time=start_times[2])
+        otel_spans[2].resource = Resource({})
         otel_spans[2].end(end_time=end_times[2])
 
         # pylint: disable=protected-access
diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
--- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
+++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
@@ -165,6 +165,7 @@ def test_export(self):
         ]
 
         otel_spans[0].start(start_time=start_times[0])
+        otel_spans[0].resource = Resource({})
         # added here to preserve order
         otel_spans[0].set_attribute("key_bool", False)
         otel_spans[0].set_attribute("key_string", "hello_world")
@@ -185,6 +186,7 @@ def test_export(self):
         otel_spans[2].end(end_time=end_times[2])
 
         otel_spans[3].start(start_time=start_times[3])
+        otel_spans[3].resource = Resource({})
         otel_spans[3].end(end_time=end_times[3])
 
         service_name = "test-service"
@@ -295,6 +297,7 @@ def test_zero_padding(self):
         )
 
         otel_span.start(start_time=start_time)
+        otel_span.resource = Resource({})
         otel_span.end(end_time=end_time)
 
         service_name = "test-service"
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._EMPTY_RESOURCE)
+        self.assertIs(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
@@ -30,18 +30,28 @@ def test_create(self):
             "cost": 112.12,
         }
 
+        expected_labels = {
+            "service": "ui",
+            "version": 1,
+            "has_bugs": True,
+            "cost": 112.12,
+            resources.TELEMETRY_SDK_NAME: "opentelemetry",
+            resources.TELEMETRY_SDK_LANGUAGE: "python",
+            resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION,
+        }
+
         resource = resources.Resource.create(labels)
         self.assertIsInstance(resource, resources.Resource)
-        self.assertEqual(resource.labels, labels)
+        self.assertEqual(resource.labels, expected_labels)
 
         resource = resources.Resource.create_empty()
         self.assertIs(resource, resources._EMPTY_RESOURCE)
 
         resource = resources.Resource.create(None)
-        self.assertIs(resource, resources._EMPTY_RESOURCE)
+        self.assertIs(resource, resources._DEFAULT_RESOURCE)
 
         resource = resources.Resource.create({})
-        self.assertIs(resource, resources._EMPTY_RESOURCE)
+        self.assertIs(resource, resources._DEFAULT_RESOURCE)
 
     def test_resource_merge(self):
         left = resources.Resource({"service": "ui"})
@@ -75,7 +85,14 @@ def test_immutability(self):
             "cost": 112.12,
         }
 
+        default_labels = {
+            resources.TELEMETRY_SDK_NAME: "opentelemetry",
+            resources.TELEMETRY_SDK_LANGUAGE: "python",
+            resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION,
+        }
+
         labels_copy = labels.copy()
+        labels_copy.update(default_labels)
 
         resource = resources.Resource.create(labels)
         self.assertEqual(resource.labels, labels_copy)
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
@@ -20,7 +20,7 @@
 
 from opentelemetry import trace as trace_api
 from opentelemetry.sdk import resources, trace
-from opentelemetry.sdk.trace import sampling
+from opentelemetry.sdk.trace import Resource, sampling
 from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
 from opentelemetry.trace.status import StatusCanonicalCode
 from opentelemetry.util import time_ns
@@ -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._EMPTY_RESOURCE)
+        self.assertIs(span.resource, resources._DEFAULT_RESOURCE)
 
     def test_span_context_remote_flag(self):
         tracer = new_tracer()
@@ -922,6 +922,7 @@ def test_to_json(self):
             trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED),
         )
         span = trace.Span("span-name", context)
+        span.resource = Resource({})
 
         self.assertEqual(
             span.to_json(),