Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Partial celery task time limit is not support
Python 3.8

**Steps to reproduce**
- Create celery task with only one of the time limit soft/hard
Or use this test to reproduce:

`instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py`

```python
    def test_set_attributes_partial_timelimit_hard_limit(self):
        context = {
            "correlation_id": "44b7f305",
            "delivery_info": {"eager": True},
            "eta": "soon",
            "expires": "later",
            "hostname": "localhost",
            "id": "44b7f305",
            "reply_to": "44b7f305",
            "retries": 4,
            "timelimit": ("now", None),
            "custom_meta": "custom_value",
            "routing_key": "celery",
        }
        span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext))
        utils.set_attributes_from_context(span, context)
        self.assertEqual(span.attributes.get("celery.timelimit"), ("now", ""))
```
**What is the expected behavior?**
The time limit that was specify should be instrumented 

**What is the actual behavior?**
Exception is raised and no time limit is instrumented
```
[__init__.py:_translate_attributes:164] _translate_key_values(key, value)
exception.trace.1	
[exporter.py:_translate_key_values:126] return KeyValue(key=key, value=_translate_value(value))
exception.trace.2	
[ exporter.py:_translate_value:104] array_value=ArrayValue(values=[_translate_value(v) for v in value])
exception.trace.3	
[ exporter.py:<listcomp>:104] array_value=ArrayValue(values=[_translate_value(v) for v in value])
exception.trace.4	
[ exporter.py:_translate_value:119] "Invalid type {} of value {}".format(type(value), value)
```
 



Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py
--- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py
+++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py
@@ -61,8 +61,11 @@ def set_attributes_from_context(span, context):
 
         # Skip `timelimit` if it is not set (it's default/unset value is a
         # tuple or a list of `None` values
-        if key == "timelimit" and value in [(None, None), [None, None]]:
-            continue
+        if key == "timelimit":
+            if value in [(None, None), [None, None]]:
+                continue
+            if None in value:
+                value = ["" if tl is None else tl for tl in value]
 
         # Skip `retries` if it's value is `0`
         if key == "retries" and value == 0:


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py
--- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py
+++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py
@@ -96,6 +96,46 @@ def test_set_attributes_not_recording(self):
         self.assertFalse(mock_span.set_attribute.called)
         self.assertFalse(mock_span.set_status.called)
 
+    def test_set_attributes_partial_timelimit_hard_limit(self):
+        # it should extract only relevant keys
+        context = {
+            "correlation_id": "44b7f305",
+            "delivery_info": {"eager": True},
+            "eta": "soon",
+            "expires": "later",
+            "hostname": "localhost",
+            "id": "44b7f305",
+            "reply_to": "44b7f305",
+            "retries": 4,
+            "timelimit": ("now", None),
+            "custom_meta": "custom_value",
+            "routing_key": "celery",
+        }
+        span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext))
+        utils.set_attributes_from_context(span, context)
+        self.assertEqual(span.attributes.get("celery.timelimit"), ("now", ""))
+
+    def test_set_attributes_partial_timelimit_soft_limit(self):
+        # it should extract only relevant keys
+        context = {
+            "correlation_id": "44b7f305",
+            "delivery_info": {"eager": True},
+            "eta": "soon",
+            "expires": "later",
+            "hostname": "localhost",
+            "id": "44b7f305",
+            "reply_to": "44b7f305",
+            "retries": 4,
+            "timelimit": (None, "later"),
+            "custom_meta": "custom_value",
+            "routing_key": "celery",
+        }
+        span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext))
+        utils.set_attributes_from_context(span, context)
+        self.assertEqual(
+            span.attributes.get("celery.timelimit"), ("", "later")
+        )
+
     def test_set_attributes_from_context_empty_keys(self):
         # it should not extract empty keys
         context = {