Repository: DataDog/dd-trace-py

Issue:
Celery tags include a raw dictionary that is difficult to use in APM
<!--
Thanks for taking the time for reporting an issue!

Before reporting an issue on dd-trace-py, please be sure to provide all
necessary information.

If you're hitting a bug, make sure that you're using the latest version of this
library.
-->

### Summary of problem
When using celery, context is automatically added to spans as tags from various argument calls. One of the tags for celery is `delivery_info`, which is a dict that provides valuable routing data. This data is difficult to use currently since we can't create facets or filters in APM with the raw dictionary as a string. (Image below)

I expect the dict values should be added as separate tag data, such that `celery.delivery_info.exchange`, `celery.delivery_info.priority`, etc are all separate tags.

<img width="727" alt="image" src="https://user-images.githubusercontent.com/1430167/206885304-9b20bdc5-097a-4e60-ad4e-8b29063092ef.png">


### Which version of dd-trace-py are you using?
1.4.0


### Which version of pip are you using?
22.3


### Which libraries and their versions are you using?

<details>
  <summary>`pip freeze`</summary>
  celery==5.2.2
</details>

### How can we reproduce your problem?
Run celery with ddtrace with celery integration enabled and observe the resulting tags on the `celery.run` span.


### What is the result that you get?
`celery.run` span has a tag:value pair roughly matching the following (dict is a string repr) `celery.delivery_info: "{'exchange':'', 'routing_key':'queue_name', ...}"`

### What is the result that you expected?
`celery.run` span should have tag:value pairs for each child key in the dictionary
`celery.delivery_info.exchange:''`
`celery.delivery_info.routing_key:'queue_name'`


Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/celery/utils.py b/ddtrace/contrib/celery/utils.py
--- a/ddtrace/contrib/celery/utils.py
+++ b/ddtrace/contrib/celery/utils.py
@@ -2,6 +2,7 @@
 from typing import Dict
 from weakref import WeakValueDictionary
 
+from ddtrace.contrib.trace_utils import set_flattened_tags
 from ddtrace.span import Span
 
 from .constants import CTX_KEY
@@ -33,27 +34,37 @@
 )
 
 
+def should_skip_context_value(key, value):
+    # type: (str, Any) -> bool
+    # Skip this key if it is not set
+    if value is None or value == "":
+        return True
+
+    # Skip `timelimit` if it is not set (its default/unset value is a
+    # tuple or a list of `None` values
+    if key == "timelimit" and all(_ is None for _ in value):
+        return True
+
+    # Skip `retries` if its value is `0`
+    if key == "retries" and value == 0:
+        return True
+
+    return False
+
+
 def set_tags_from_context(span, context):
     # type: (Span, Dict[str, Any]) -> None
     """Helper to extract meta values from a Celery Context"""
 
+    context_tags = []
     for key, tag_name in TAG_KEYS:
         value = context.get(key)
-
-        # Skip this key if it is not set
-        if value is None or value == "":
-            continue
-
-        # Skip `timelimit` if it is not set (its default/unset value is a
-        # tuple or a list of `None` values
-        if key == "timelimit" and all(_ is None for _ in value):
+        if should_skip_context_value(key, value):
             continue
 
-        # Skip `retries` if its value is `0`
-        if key == "retries" and value == 0:
-            continue
+        context_tags.append((tag_name, value))
 
-        span.set_tag(tag_name, value)
+    set_flattened_tags(span, context_tags)
 
 
 def attach_span(task, task_id, span, is_publish=False):


Test patch:
diff --git a/tests/contrib/celery/test_utils.py b/tests/contrib/celery/test_utils.py
--- a/tests/contrib/celery/test_utils.py
+++ b/tests/contrib/celery/test_utils.py
@@ -19,7 +19,7 @@ def test_tags_from_context(self):
         # it should extract only relevant keys
         context = {
             "correlation_id": "44b7f305",
-            "delivery_info": '{"eager": "True"}',
+            "delivery_info": {"eager": "True", "priority": "0", "int_zero": 0},
             "eta": "soon",
             "expires": "later",
             "hostname": "localhost",
@@ -36,7 +36,9 @@ def test_tags_from_context(self):
         metrics = span.get_metrics()
         sentinel = object()
         assert metas["celery.correlation_id"] == "44b7f305"
-        assert metas["celery.delivery_info"] == '{"eager": "True"}'
+        assert metas["celery.delivery_info.eager"] == "True"
+        assert metas["celery.delivery_info.priority"] == "0"
+        assert metrics["celery.delivery_info.int_zero"] == 0
         assert metas["celery.eta"] == "soon"
         assert metas["celery.expires"] == "later"
         assert metas["celery.hostname"] == "localhost"