Repository: open-telemetry/opentelemetry-python

Issue:
null attributes that have been previously set results in the attribute being cleared and the key being dropped from the attribute set
To be compliant with the tracing spec: null attributes that have been previously set results in the attribute being cleared and the key being dropped from the attribute set

https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#set-attributes


Discussion:
Looks like the spec moved the relevant details here: https://github.com/open-telemetry/opentelemetry-specification/blob/671f5abe0eb99451f86df6da8aa7cd4b14d22633/specification/common/common.md#attributes
Hi there!

I would like to get started contributing on the repo. This is what I understood from the specs and the issue:
- An attribute's value should never be `None`
- Adding an attribute with a `None` value should be a no-op or fire a warning
- The scope of this issue is to implement the following mechanism: modifying the value of an existing attribute from a valid value to `None` should pop the attribute

Can you confirm that I understood correctly? If yes, I'm down to write a PR :slightly_smiling_face: 
> * An attribute's value should never be `None`

See below.

> * Adding an attribute with a `None` value should be a no-op or fire a warning

Possibly. Maybe we can just let this occur and then the exporter deals with the value accordingly. Should be fine to implement it as a no-op though.

> * The scope of this issue is to implement the following mechanism: modifying the value of an existing attribute from a valid value to `None` should pop the attribute

Yes.

Thanks for your answer :). Gonna work on this and get back to you soon
Looks like the spec has changed regarding that topic: https://github.com/open-telemetry/opentelemetry-specification/pull/992 defines updating an attribute value to `null` to be undefined behavior (disc here https://github.com/open-telemetry/opentelemetry-specification/issues/797). In the current implementation, this is a no-op. According to [this comment](https://github.com/open-telemetry/opentelemetry-specification/pull/992#issuecomment-697924962), it is acceptable.
I suggest to keep the code as is, ditch the PR and close the issue. What do you think?
@LetzNico 
I believe your understanding is correct, we can leave the no-ops for setting attributes to `None`.

However, I think due to [this logic](https://github.com/open-telemetry/opentelemetry-python/blob/04792d56f196c16d2a0f02f829e259b9ba133887/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L304), we still have the issue of `None` attributes in a `Sequence` type being dropped.

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py
--- a/opentelemetry-api/src/opentelemetry/util/types.py
+++ b/opentelemetry-api/src/opentelemetry/util/types.py
@@ -20,10 +20,10 @@
     bool,
     int,
     float,
-    Sequence[str],
-    Sequence[bool],
-    Sequence[int],
-    Sequence[float],
+    Sequence[Union[None, str]],
+    Sequence[Union[None, bool]],
+    Sequence[Union[None, int]],
+    Sequence[Union[None, float]],
 ]
 Attributes = Optional[Mapping[str, AttributeValue]]
 AttributesFormatter = Callable[[], Attributes]
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
@@ -308,34 +308,44 @@ def attributes(self) -> types.Attributes:
 def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
     """Checks if attribute value is valid.
 
-    An attribute value is valid if it is one of the valid types. If the value
-    is a sequence, it is only valid if all items in the sequence are of valid
-    type, not a sequence, and are of the same type.
+    An attribute value is valid if it is one of the valid types.
+    If the value is a sequence, it is only valid if all items in the sequence:
+      - are of the same valid type or None
+      - are not a sequence
     """
 
     if isinstance(value, Sequence):
         if len(value) == 0:
             return True
 
-        first_element_type = type(value[0])
-
-        if first_element_type not in VALID_ATTR_VALUE_TYPES:
-            logger.warning(
-                "Invalid type %s in attribute value sequence. Expected one of "
-                "%s or a sequence of those types",
-                first_element_type.__name__,
-                [valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES],
-            )
-            return False
-
-        for element in list(value)[1:]:
-            if not isinstance(element, first_element_type):
+        sequence_first_valid_type = None
+        for element in value:
+            if element is None:
+                continue
+            element_type = type(element)
+            if element_type not in VALID_ATTR_VALUE_TYPES:
+                logger.warning(
+                    "Invalid type %s in attribute value sequence. Expected one of "
+                    "%s or None",
+                    element_type.__name__,
+                    [
+                        valid_type.__name__
+                        for valid_type in VALID_ATTR_VALUE_TYPES
+                    ],
+                )
+                return False
+            # The type of the sequence must be homogeneous. The first non-None
+            # element determines the type of the sequence
+            if sequence_first_valid_type is None:
+                sequence_first_valid_type = element_type
+            elif not isinstance(element, sequence_first_valid_type):
                 logger.warning(
                     "Mixed types %s and %s in attribute value sequence",
-                    first_element_type.__name__,
+                    sequence_first_valid_type.__name__,
                     type(element).__name__,
                 )
                 return False
+
     elif not isinstance(value, VALID_ATTR_VALUE_TYPES):
         logger.warning(
             "Invalid type %s for attribute value. Expected one of %s or a "


Test patch:
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
@@ -564,6 +564,14 @@ def test_check_attribute_helper(self):
         self.assertTrue(trace._is_valid_attribute_value("hi"))
         self.assertTrue(trace._is_valid_attribute_value(3.4))
         self.assertTrue(trace._is_valid_attribute_value(15))
+        # None in sequences are valid
+        self.assertTrue(trace._is_valid_attribute_value(["A", None, None]))
+        self.assertTrue(
+            trace._is_valid_attribute_value(["A", None, None, "B"])
+        )
+        self.assertTrue(trace._is_valid_attribute_value([None, None]))
+        self.assertFalse(trace._is_valid_attribute_value(["A", None, 1]))
+        self.assertFalse(trace._is_valid_attribute_value([None, "A", None, 1]))
 
     def test_sampling_attributes(self):
         sampling_attributes = {