Repository: open-telemetry/opentelemetry-python

Issue:
Attributes immutability should be consistent
Currently this is done either using a mapping proxy or a copy of attributes, let's be consistent.


Discussion:

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py
--- a/opentelemetry-api/src/opentelemetry/trace/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py
@@ -82,6 +82,9 @@
 from typing import Iterator, Optional, Sequence, cast
 
 from opentelemetry import context as context_api
+from opentelemetry.attributes import (  # type: ignore
+    _create_immutable_attributes,
+)
 from opentelemetry.context.context import Context
 from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
 from opentelemetry.trace.propagation import (
@@ -126,7 +129,7 @@ def attributes(self) -> types.Attributes:
 
 
 class Link(_LinkBase):
-    """A link to a `Span`.
+    """A link to a `Span`. The attributes of a Link are immutable.
 
     Args:
         context: `SpanContext` of the `Span` to link to.
@@ -139,7 +142,9 @@ def __init__(
         attributes: types.Attributes = None,
     ) -> None:
         super().__init__(context)
-        self._attributes = attributes
+        self._attributes = _create_immutable_attributes(
+            attributes
+        )  # type: types.Attributes
 
     @property
     def attributes(self) -> types.Attributes:
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
@@ -64,7 +64,10 @@
 
 import pkg_resources
 
-from opentelemetry.attributes import _filter_attributes
+from opentelemetry.attributes import (
+    _create_immutable_attributes,
+    _filter_attributes,
+)
 from opentelemetry.sdk.environment_variables import (
     OTEL_RESOURCE_ATTRIBUTES,
     OTEL_SERVICE_NAME,
@@ -145,7 +148,7 @@ def __init__(
         self, attributes: Attributes, schema_url: typing.Optional[str] = None
     ):
         _filter_attributes(attributes)
-        self._attributes = attributes.copy()
+        self._attributes = _create_immutable_attributes(attributes)
         if schema_url is None:
             schema_url = ""
         self._schema_url = schema_url
@@ -187,7 +190,7 @@ def get_empty() -> "Resource":
 
     @property
     def attributes(self) -> Attributes:
-        return self._attributes.copy()
+        return self._attributes
 
     @property
     def schema_url(self) -> str:
@@ -210,7 +213,7 @@ def merge(self, other: "Resource") -> "Resource":
         Returns:
             The newly-created Resource.
         """
-        merged_attributes = self.attributes
+        merged_attributes = self.attributes.copy()
         merged_attributes.update(other.attributes)
 
         if self.schema_url == "":
@@ -239,7 +242,7 @@ def __eq__(self, other: object) -> bool:
 
     def __hash__(self):
         return hash(
-            f"{dumps(self._attributes, sort_keys=True)}|{self._schema_url}"
+            f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}"
         )
 
 
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
@@ -296,7 +296,8 @@ def attributes(self) -> types.Attributes:
 
 
 class Event(EventBase):
-    """A text annotation with a set of attributes.
+    """A text annotation with a set of attributes. The attributes of an event
+    are immutable.
 
     Args:
         name: Name of the event.
@@ -456,7 +457,7 @@ def to_json(self, indent=4):
         f_span["attributes"] = self._format_attributes(self._attributes)
         f_span["events"] = self._format_events(self._events)
         f_span["links"] = self._format_links(self._links)
-        f_span["resource"] = self._resource.attributes
+        f_span["resource"] = self._format_attributes(self._resource.attributes)
 
         return json.dumps(f_span, indent=indent)
 


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
@@ -188,7 +188,8 @@ def test_immutability(self):
         resource = resources.Resource.create(attributes)
         self.assertEqual(resource.attributes, attributes_copy)
 
-        resource.attributes["has_bugs"] = False
+        with self.assertRaises(TypeError):
+            resource.attributes["has_bugs"] = False
         self.assertEqual(resource.attributes, attributes_copy)
 
         attributes["cost"] = 999.91
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
@@ -805,7 +805,7 @@ def test_links(self):
             self.assertEqual(
                 root.links[0].context.span_id, other_context1.span_id
             )
-            self.assertEqual(root.links[0].attributes, None)
+            self.assertEqual(0, len(root.links[0].attributes))
             self.assertEqual(
                 root.links[1].context.trace_id, other_context2.trace_id
             )
@@ -814,6 +814,9 @@ def test_links(self):
             )
             self.assertEqual(root.links[1].attributes, {"name": "neighbor"})
 
+            with self.assertRaises(TypeError):
+                root.links[1].attributes["name"] = "new_neighbour"
+
     def test_update_name(self):
         with self.tracer.start_as_current_span("root") as root:
             # name