prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  DD_TAGS separator inconsistent with heroku-buildpack-datadog
  https://github.com/DataDog/heroku-buildpack-datadog sets `DD_TAGS` separated by spaces.
  
  However, dd-trace-py splits the tags by commas:
  
  https://github.com/DataDog/dd-trace-py/blob/master/ddtrace/utils/formats.py#L87-L116
  ```
  def parse_tags_str(tags_str):
      """Parse a string of tags typically provided via environment variables.
      The expected string is of the form::
          "key1:value1,key2:value2"
      :param tags_str: A string of the above form to parse tags from.
      :return: A dict containing the tags that were parsed.
      """
      parsed_tags = {}
      if not tags_str:
          return parsed_tags
  
      for tag in tags_str.split(","):
          try:
              key, value = tag.split(":", 1)
  
              # Validate the tag
              if key == "" or value == "" or value.endswith(":"):
                  raise ValueError
          except ValueError:
              log.error(
                  "Malformed tag in tag pair '%s' from tag string '%s'.",
                  tag,
                  tags_str,
              )
          else:
              parsed_tags[key] = value
  
      return parsed_tags
  ```
  
  This results in all of the tags being set as the value of the first tag.
  
  This looks to have been previously surfaced for the java tracer here:
  https://github.com/DataDog/heroku-buildpack-datadog/issues/187
  
  And was initially changed in the buildpack but was reverted and dd-trace-java updated to accommodate commas or spaces:
  https://github.com/DataDog/dd-trace-java/pull/2011
  
  ### Which version of dd-trace-py are you using?
  0.47.0
  
  ### How can we reproduce your problem?
  Perform a trace with `DD_TAGS` set by https://github.com/DataDog/heroku-buildpack-datadog.
  
  ### What is the result that you get?
  `dyno` tag contains all other tags.
  
  ### What is the result that you expected?
  Tags to be properly separated.
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `ddtrace/utils/formats.py`
  Content:
  ```
  1 import logging
  2 import os
  3 
  4 from .deprecation import deprecation
  5 
  6 
  7 log = logging.getLogger(__name__)
  8 
  9 
  10 def get_env(*parts, **kwargs):
  11     """Retrieves environment variables value for the given integration. It must be used
  12     for consistency between integrations. The implementation is backward compatible
  13     with legacy nomenclature:
  14 
  15     * `DATADOG_` is a legacy prefix with lower priority
  16     * `DD_` environment variables have the highest priority
  17     * the environment variable is built concatenating `integration` and `variable`
  18       arguments
  19     * return `default` otherwise
  20 
  21     :param parts: evironment variable parts that will be joined with ``_`` to generate the name
  22     :type parts: :obj:`str`
  23     :param kwargs: ``default`` is the only supported keyword argument which sets the default value
  24         if no environment variable is found
  25     :rtype: :obj:`str` | ``kwargs["default"]``
  26     :returns: The string environment variable value or the value of ``kwargs["default"]`` if not found
  27     """
  28     default = kwargs.get("default")
  29 
  30     key = "_".join(parts)
  31     key = key.upper()
  32     legacy_env = "DATADOG_{}".format(key)
  33     env = "DD_{}".format(key)
  34 
  35     value = os.getenv(env)
  36     legacy = os.getenv(legacy_env)
  37     if legacy:
  38         # Deprecation: `DATADOG_` variables are deprecated
  39         deprecation(
  40             name="DATADOG_",
  41             message="Use `DD_` prefix instead",
  42             version="1.0.0",
  43         )
  44 
  45     value = value or legacy
  46     return value if value else default
  47 
  48 
  49 def deep_getattr(obj, attr_string, default=None):
  50     """
  51     Returns the attribute of `obj` at the dotted path given by `attr_string`
  52     If no such attribute is reachable, returns `default`
  53 
  54     >>> deep_getattr(cass, 'cluster')
  55     <cassandra.cluster.Cluster object at 0xa20c350
  56 
  57     >>> deep_getattr(cass, 'cluster.metadata.partitioner')
  58     u'org.apache.cassandra.dht.Murmur3Partitioner'
  59 
  60     >>> deep_getattr(cass, 'i.dont.exist', default='default')
  61     'default'
  62     """
  63     attrs = attr_string.split(".")
  64     for attr in attrs:
  65         try:
  66             obj = getattr(obj, attr)
  67         except AttributeError:
  68             return default
  69 
  70     return obj
  71 
  72 
  73 def asbool(value):
  74     """Convert the given String to a boolean object.
  75 
  76     Accepted values are `True` and `1`.
  77     """
  78     if value is None:
  79         return False
  80 
  81     if isinstance(value, bool):
  82         return value
  83 
  84     return value.lower() in ("true", "1")
  85 
  86 
  87 def parse_tags_str(tags_str):
  88     """Parse a string of tags typically provided via environment variables.
  89 
  90     The expected string is of the form::
  91         "key1:value1,key2:value2"
  92 
  93     :param tags_str: A string of the above form to parse tags from.
  94     :return: A dict containing the tags that were parsed.
  95     """
  96     parsed_tags = {}
  97     if not tags_str:
  98         return parsed_tags
  99 
  100     for tag in tags_str.split(","):
  101         try:
  102             key, value = tag.split(":", 1)
  103 
  104             # Validate the tag
  105             if key == "" or value == "" or value.endswith(":"):
  106                 raise ValueError
  107         except ValueError:
  108             log.error(
  109                 "Malformed tag in tag pair '%s' from tag string '%s'.",
  110                 tag,
  111                 tags_str,
  112             )
  113         else:
  114             parsed_tags[key] = value
  115 
  116     return parsed_tags
  117
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/ddtrace/utils/formats.py b/ddtrace/utils/formats.py
  --- a/ddtrace/utils/formats.py
  +++ b/ddtrace/utils/formats.py
  @@ -1,9 +1,13 @@
   import logging
   import os
  +import re
   
   from .deprecation import deprecation
   
   
  +# Tags `key:value` must be separated by either comma or space
  +_TAGS_NOT_SEPARATED = re.compile(r":[^,\s]+:")
  +
   log = logging.getLogger(__name__)
   
   
  @@ -89,6 +93,7 @@
   
       The expected string is of the form::
           "key1:value1,key2:value2"
  +        "key1:value1 key2:value2"
   
       :param tags_str: A string of the above form to parse tags from.
       :return: A dict containing the tags that were parsed.
  @@ -97,7 +102,29 @@
       if not tags_str:
           return parsed_tags
   
  -    for tag in tags_str.split(","):
  +    if _TAGS_NOT_SEPARATED.search(tags_str):
  +        log.error("Malformed tag string with tags not separated by comma or space '%s'.", tags_str)
  +        return parsed_tags
  +
  +    # Identify separator based on which successfully identifies the correct
  +    # number of valid tags
  +    numtagseps = tags_str.count(":")
  +    for sep in [",", " "]:
  +        if sum(":" in _ for _ in tags_str.split(sep)) == numtagseps:
  +            break
  +    else:
  +        log.error(
  +            (
  +                "Failed to find separator for tag string: '%s'.\n"
  +                "Tag strings must be comma or space separated:\n"
  +                "  key1:value1,key2:value2\n"
  +                "  key1:value1 key2:value2"
  +            ),
  +            tags_str,
  +        )
  +        return parsed_tags
  +
  +    for tag in tags_str.split(sep):
           try:
               key, value = tag.split(":", 1)
  
verification_info: {"golden_diff": "diff --git a/ddtrace/utils/formats.py b/ddtrace/utils/formats.py\n--- a/ddtrace/utils/formats.py\n+++ b/ddtrace/utils/formats.py\n@@ -1,9 +1,13 @@\n import logging\n import os\n+import re\n \n from .deprecation import deprecation\n \n \n+# Tags `key:value` must be separated by either comma or space\n+_TAGS_NOT_SEPARATED = re.compile(r\":[^,\\s]+:\")\n+\n log = logging.getLogger(__name__)\n \n \n@@ -89,6 +93,7 @@\n \n     The expected string is of the form::\n         \"key1:value1,key2:value2\"\n+        \"key1:value1 key2:value2\"\n \n     :param tags_str: A string of the above form to parse tags from.\n     :return: A dict containing the tags that were parsed.\n@@ -97,7 +102,29 @@\n     if not tags_str:\n         return parsed_tags\n \n-    for tag in tags_str.split(\",\"):\n+    if _TAGS_NOT_SEPARATED.search(tags_str):\n+        log.error(\"Malformed tag string with tags not separated by comma or space '%s'.\", tags_str)\n+        return parsed_tags\n+\n+    # Identify separator based on which successfully identifies the correct\n+    # number of valid tags\n+    numtagseps = tags_str.count(\":\")\n+    for sep in [\",\", \" \"]:\n+        if sum(\":\" in _ for _ in tags_str.split(sep)) == numtagseps:\n+            break\n+    else:\n+        log.error(\n+            (\n+                \"Failed to find separator for tag string: '%s'.\\n\"\n+                \"Tag strings must be comma or space separated:\\n\"\n+                \"  key1:value1,key2:value2\\n\"\n+                \"  key1:value1 key2:value2\"\n+            ),\n+            tags_str,\n+        )\n+        return parsed_tags\n+\n+    for tag in tags_str.split(sep):\n         try:\n             key, value = tag.split(\":\", 1)\n", "issue": "DD_TAGS separator inconsistent with heroku-buildpack-datadog\nhttps://github.com/DataDog/heroku-buildpack-datadog sets `DD_TAGS` separated by spaces.\r\n\r\nHowever, dd-trace-py splits the tags by commas:\r\n\r\nhttps://github.com/DataDog/dd-trace-py/blob/master/ddtrace/utils/formats.py#L87-L116\r\n```\r\ndef parse_tags_str(tags_str):\r\n    \"\"\"Parse a string of tags typically provided via environment variables.\r\n    The expected string is of the form::\r\n        \"key1:value1,key2:value2\"\r\n    :param tags_str: A string of the above form to parse tags from.\r\n    :return: A dict containing the tags that were parsed.\r\n    \"\"\"\r\n    parsed_tags = {}\r\n    if not tags_str:\r\n        return parsed_tags\r\n\r\n    for tag in tags_str.split(\",\"):\r\n        try:\r\n            key, value = tag.split(\":\", 1)\r\n\r\n            # Validate the tag\r\n            if key == \"\" or value == \"\" or value.endswith(\":\"):\r\n                raise ValueError\r\n        except ValueError:\r\n            log.error(\r\n                \"Malformed tag in tag pair '%s' from tag string '%s'.\",\r\n                tag,\r\n                tags_str,\r\n            )\r\n        else:\r\n            parsed_tags[key] = value\r\n\r\n    return parsed_tags\r\n```\r\n\r\nThis results in all of the tags being set as the value of the first tag.\r\n\r\nThis looks to have been previously surfaced for the java tracer here:\r\nhttps://github.com/DataDog/heroku-buildpack-datadog/issues/187\r\n\r\nAnd was initially changed in the buildpack but was reverted and dd-trace-java updated to accommodate commas or spaces:\r\nhttps://github.com/DataDog/dd-trace-java/pull/2011\r\n\r\n### Which version of dd-trace-py are you using?\r\n0.47.0\r\n\r\n### How can we reproduce your problem?\r\nPerform a trace with `DD_TAGS` set by https://github.com/DataDog/heroku-buildpack-datadog.\r\n\r\n### What is the result that you get?\r\n`dyno` tag contains all other tags.\r\n\r\n### What is the result that you expected?\r\nTags to be properly separated.\r\n\n", "before_files": [{"content": "import logging\nimport os\n\nfrom .deprecation import deprecation\n\n\nlog = logging.getLogger(__name__)\n\n\ndef get_env(*parts, **kwargs):\n    \"\"\"Retrieves environment variables value for the given integration. It must be used\n    for consistency between integrations. The implementation is backward compatible\n    with legacy nomenclature:\n\n    * `DATADOG_` is a legacy prefix with lower priority\n    * `DD_` environment variables have the highest priority\n    * the environment variable is built concatenating `integration` and `variable`\n      arguments\n    * return `default` otherwise\n\n    :param parts: evironment variable parts that will be joined with ``_`` to generate the name\n    :type parts: :obj:`str`\n    :param kwargs: ``default`` is the only supported keyword argument which sets the default value\n        if no environment variable is found\n    :rtype: :obj:`str` | ``kwargs[\"default\"]``\n    :returns: The string environment variable value or the value of ``kwargs[\"default\"]`` if not found\n    \"\"\"\n    default = kwargs.get(\"default\")\n\n    key = \"_\".join(parts)\n    key = key.upper()\n    legacy_env = \"DATADOG_{}\".format(key)\n    env = \"DD_{}\".format(key)\n\n    value = os.getenv(env)\n    legacy = os.getenv(legacy_env)\n    if legacy:\n        # Deprecation: `DATADOG_` variables are deprecated\n        deprecation(\n            name=\"DATADOG_\",\n            message=\"Use `DD_` prefix instead\",\n            version=\"1.0.0\",\n        )\n\n    value = value or legacy\n    return value if value else default\n\n\ndef deep_getattr(obj, attr_string, default=None):\n    \"\"\"\n    Returns the attribute of `obj` at the dotted path given by `attr_string`\n    If no such attribute is reachable, returns `default`\n\n    >>> deep_getattr(cass, 'cluster')\n    <cassandra.cluster.Cluster object at 0xa20c350\n\n    >>> deep_getattr(cass, 'cluster.metadata.partitioner')\n    u'org.apache.cassandra.dht.Murmur3Partitioner'\n\n    >>> deep_getattr(cass, 'i.dont.exist', default='default')\n    'default'\n    \"\"\"\n    attrs = attr_string.split(\".\")\n    for attr in attrs:\n        try:\n            obj = getattr(obj, attr)\n        except AttributeError:\n            return default\n\n    return obj\n\n\ndef asbool(value):\n    \"\"\"Convert the given String to a boolean object.\n\n    Accepted values are `True` and `1`.\n    \"\"\"\n    if value is None:\n        return False\n\n    if isinstance(value, bool):\n        return value\n\n    return value.lower() in (\"true\", \"1\")\n\n\ndef parse_tags_str(tags_str):\n    \"\"\"Parse a string of tags typically provided via environment variables.\n\n    The expected string is of the form::\n        \"key1:value1,key2:value2\"\n\n    :param tags_str: A string of the above form to parse tags from.\n    :return: A dict containing the tags that were parsed.\n    \"\"\"\n    parsed_tags = {}\n    if not tags_str:\n        return parsed_tags\n\n    for tag in tags_str.split(\",\"):\n        try:\n            key, value = tag.split(\":\", 1)\n\n            # Validate the tag\n            if key == \"\" or value == \"\" or value.endswith(\":\"):\n                raise ValueError\n        except ValueError:\n            log.error(\n                \"Malformed tag in tag pair '%s' from tag string '%s'.\",\n                tag,\n                tags_str,\n            )\n        else:\n            parsed_tags[key] = value\n\n    return parsed_tags\n", "path": "ddtrace/utils/formats.py"}], "after_files": [{"content": "import logging\nimport os\nimport re\n\nfrom .deprecation import deprecation\n\n\n# Tags `key:value` must be separated by either comma or space\n_TAGS_NOT_SEPARATED = re.compile(r\":[^,\\s]+:\")\n\nlog = logging.getLogger(__name__)\n\n\ndef get_env(*parts, **kwargs):\n    \"\"\"Retrieves environment variables value for the given integration. It must be used\n    for consistency between integrations. The implementation is backward compatible\n    with legacy nomenclature:\n\n    * `DATADOG_` is a legacy prefix with lower priority\n    * `DD_` environment variables have the highest priority\n    * the environment variable is built concatenating `integration` and `variable`\n      arguments\n    * return `default` otherwise\n\n    :param parts: evironment variable parts that will be joined with ``_`` to generate the name\n    :type parts: :obj:`str`\n    :param kwargs: ``default`` is the only supported keyword argument which sets the default value\n        if no environment variable is found\n    :rtype: :obj:`str` | ``kwargs[\"default\"]``\n    :returns: The string environment variable value or the value of ``kwargs[\"default\"]`` if not found\n    \"\"\"\n    default = kwargs.get(\"default\")\n\n    key = \"_\".join(parts)\n    key = key.upper()\n    legacy_env = \"DATADOG_{}\".format(key)\n    env = \"DD_{}\".format(key)\n\n    value = os.getenv(env)\n    legacy = os.getenv(legacy_env)\n    if legacy:\n        # Deprecation: `DATADOG_` variables are deprecated\n        deprecation(\n            name=\"DATADOG_\",\n            message=\"Use `DD_` prefix instead\",\n            version=\"1.0.0\",\n        )\n\n    value = value or legacy\n    return value if value else default\n\n\ndef deep_getattr(obj, attr_string, default=None):\n    \"\"\"\n    Returns the attribute of `obj` at the dotted path given by `attr_string`\n    If no such attribute is reachable, returns `default`\n\n    >>> deep_getattr(cass, 'cluster')\n    <cassandra.cluster.Cluster object at 0xa20c350\n\n    >>> deep_getattr(cass, 'cluster.metadata.partitioner')\n    u'org.apache.cassandra.dht.Murmur3Partitioner'\n\n    >>> deep_getattr(cass, 'i.dont.exist', default='default')\n    'default'\n    \"\"\"\n    attrs = attr_string.split(\".\")\n    for attr in attrs:\n        try:\n            obj = getattr(obj, attr)\n        except AttributeError:\n            return default\n\n    return obj\n\n\ndef asbool(value):\n    \"\"\"Convert the given String to a boolean object.\n\n    Accepted values are `True` and `1`.\n    \"\"\"\n    if value is None:\n        return False\n\n    if isinstance(value, bool):\n        return value\n\n    return value.lower() in (\"true\", \"1\")\n\n\ndef parse_tags_str(tags_str):\n    \"\"\"Parse a string of tags typically provided via environment variables.\n\n    The expected string is of the form::\n        \"key1:value1,key2:value2\"\n        \"key1:value1 key2:value2\"\n\n    :param tags_str: A string of the above form to parse tags from.\n    :return: A dict containing the tags that were parsed.\n    \"\"\"\n    parsed_tags = {}\n    if not tags_str:\n        return parsed_tags\n\n    if _TAGS_NOT_SEPARATED.search(tags_str):\n        log.error(\"Malformed tag string with tags not separated by comma or space '%s'.\", tags_str)\n        return parsed_tags\n\n    # Identify separator based on which successfully identifies the correct\n    # number of valid tags\n    numtagseps = tags_str.count(\":\")\n    for sep in [\",\", \" \"]:\n        if sum(\":\" in _ for _ in tags_str.split(sep)) == numtagseps:\n            break\n    else:\n        log.error(\n            (\n                \"Failed to find separator for tag string: '%s'.\\n\"\n                \"Tag strings must be comma or space separated:\\n\"\n                \"  key1:value1,key2:value2\\n\"\n                \"  key1:value1 key2:value2\"\n            ),\n            tags_str,\n        )\n        return parsed_tags\n\n    for tag in tags_str.split(sep):\n        try:\n            key, value = tag.split(\":\", 1)\n\n            # Validate the tag\n            if key == \"\" or value == \"\" or value.endswith(\":\"):\n                raise ValueError\n        except ValueError:\n            log.error(\n                \"Malformed tag in tag pair '%s' from tag string '%s'.\",\n                tag,\n                tags_str,\n            )\n        else:\n            parsed_tags[key] = value\n\n    return parsed_tags\n", "path": "ddtrace/utils/formats.py"}]}