diff --git a/ddtrace/contrib/trace_utils.py b/ddtrace/contrib/trace_utils.py index ace93ec06f..1fd05e57d8 100644 --- a/ddtrace/contrib/trace_utils.py +++ b/ddtrace/contrib/trace_utils.py @@ -144,7 +144,7 @@ def _store_headers(headers, span, integration_config, request_or_response): return for header_name, header_value in headers.items(): - """config._header_tag_name gets an element of the dictionary in config.http._header_tags + """config._header_tag_name gets an element of the dictionary in config.trace_http_header_tags which gets the value from DD_TRACE_HEADER_TAGS environment variable.""" tag_name = integration_config._header_tag_name(header_name) if tag_name is None: diff --git a/ddtrace/internal/utils/formats.py b/ddtrace/internal/utils/formats.py index 778c01f279..e9e3b85f43 100644 --- a/ddtrace/internal/utils/formats.py +++ b/ddtrace/internal/utils/formats.py @@ -70,6 +70,8 @@ def parse_tags_str(tags_str): The expected string is of the form:: "key1:value1,key2:value2" "key1:value1 key2:value2" + "key1,key2" + "key1 key2" :param tags_str: A string of the above form to parse tags from. :return: A dict containing the tags that were parsed. @@ -86,10 +88,14 @@ def parse_tags(tags): for tag in tags: key, sep, value = tag.partition(":") - if not sep or not key or "," in key: + if not key.strip() or "," in key or (sep and not value): invalids.append(tag) - else: + elif sep: + # parse key:val,key2:value2 parsed_tags.append((key, value)) + else: + # parse key,key2 + parsed_tags.append((key, "")) return parsed_tags, invalids diff --git a/docs/configuration.rst b/docs/configuration.rst index 62cb907353..b95ed0639f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -31,6 +31,8 @@ The following environment variables for the tracer are supported: DD_TAGS: description: | Set global tags to be attached to every span. Value must be either comma or space separated. e.g. ``key1:value1,key2:value2`` or ``key1:value key2:value2``. + + If a tag value is not supplied the value will be an empty string. e.g. ``key1,key2`` or ``key1 key2``. version_added: v0.38.0: Comma separated support added v0.48.0: Space separated support added @@ -290,9 +292,11 @@ The following environment variables for the tracer are supported: DD_TRACE_HEADER_TAGS: description: | - A map of case-insensitive header keys to tag names. Automatically applies matching header values as tags on root spans. + A map of case-insensitive http headers to tag names. Automatically applies matching header values as tags on request and response spans. For example if + ``DD_TRACE_HEADER_TAGS=User-Agent:http.useragent,content-type:http.content_type``. The value of the header will be stored in tags with the name ``http.useragent`` and ``http.content_type``. - For example, ``User-Agent:http.useragent,content-type:http.content_type``. + If a tag name is not supplied the header name will be used. For example if + ``DD_TRACE_HEADER_TAGS=User-Agent,content-type``. The value of http header will be stored in tags with the names ``http..headers.user-agent`` and ``http..headers.content-type``. DD_TRACE_API_VERSION: default: | diff --git a/releasenotes/notes/list_dd_header_tags-f8a9ce1daa2a1cf6.yaml b/releasenotes/notes/list_dd_header_tags-f8a9ce1daa2a1cf6.yaml new file mode 100644 index 0000000000..44a81c9b11 --- /dev/null +++ b/releasenotes/notes/list_dd_header_tags-f8a9ce1daa2a1cf6.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + tracing: Updates ``DD_HEADER_TAGS`` and ``DD_TAGS`` to support the following formats: + ``key1,key2,key3``, ``key1:val,key2:val,key3:val3``, ``key1:val key2:val key3:val3``, and ``key1 key2 key3``. + Key value pairs that do not match an expected format will be logged and ignored by the tracer. diff --git a/tests/internal/test_settings.py b/tests/internal/test_settings.py index 0ad706b395..1f3ecc42a0 100644 --- a/tests/internal/test_settings.py +++ b/tests/internal/test_settings.py @@ -103,6 +103,17 @@ def _deleted_rc_config(): "expected": {"trace_http_header_tags": {"header": "value"}}, "expected_source": {"trace_http_header_tags": "code"}, }, + { + "env": {"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1,X-Header-Tag-2,X-Header-Tag-3:specific_tag3"}, + "expected": { + "trace_http_header_tags": { + "X-Header-Tag-1": "", + "X-Header-Tag-2": "", + "X-Header-Tag-3": "specific_tag3", + } + }, + "expected_source": {"trace_http_header_tags": "env_var"}, + }, { "env": {"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2"}, "rc": { diff --git a/tests/tracer/test_trace_utils.py b/tests/tracer/test_trace_utils.py index 380198c268..fae2256f42 100644 --- a/tests/tracer/test_trace_utils.py +++ b/tests/tracer/test_trace_utils.py @@ -306,6 +306,47 @@ def test_ext_service(int_config, pin, config_val, default, expected): assert trace_utils.ext_service(pin, int_config.myint, default) == expected +@pytest.mark.subprocess( + parametrize={ + "DD_TRACE_HEADER_TAGS": ["header1 header2 header3:third-header", "header1,header2,header3:third-header"] + } +) +def test_set_http_meta_with_http_header_tags_config(): + from ddtrace import config + from ddtrace._trace.span import Span + from ddtrace.contrib.trace_utils import set_http_meta + + assert config.trace_http_header_tags == { + "header1": "", + "header2": "", + "header3": "third-header", + }, config.trace_http_header_tags + integration_config = config.new_integration + assert integration_config.is_header_tracing_configured + + # test request headers + request_span = Span(name="new_integration.request") + set_http_meta( + request_span, + integration_config, + request_headers={"header1": "value1", "header2": "value2", "header3": "value3"}, + ) + assert request_span.get_tag("http.request.headers.header1") == "value1" + assert request_span.get_tag("http.request.headers.header2") == "value2" + assert request_span.get_tag("third-header") == "value3" + + # test response headers + response_span = Span(name="new_integration.response") + set_http_meta( + response_span, + integration_config, + response_headers={"header1": "value1", "header2": "value2", "header3": "value3"}, + ) + assert response_span.get_tag("http.response.headers.header1") == "value1" + assert response_span.get_tag("http.response.headers.header2") == "value2" + assert response_span.get_tag("third-header") == "value3" + + @pytest.mark.parametrize("appsec_enabled", [False, True]) @pytest.mark.parametrize("span_type", [SpanTypes.WEB, SpanTypes.HTTP, None]) @pytest.mark.parametrize( diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 61f526c7fa..4771258e39 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -979,11 +979,11 @@ def test_dd_tags(self): assert self.tracer._tags.get("key1") == "value1" assert self.tracer._tags.get("key2") == "value2" - @run_in_subprocess(env_overrides=dict(DD_TAGS="key1:value1,key2:value2,key3")) + @run_in_subprocess(env_overrides=dict(DD_TAGS="key1:value1,key2:value2, key3")) def test_dd_tags_invalid(self): assert self.tracer._tags.get("key1") assert self.tracer._tags.get("key2") - assert self.tracer._tags.get("key3") is None + assert not self.tracer._tags.get("key3") @run_in_subprocess(env_overrides=dict(DD_TAGS="service:mysvc,env:myenv,version:myvers")) def test_tags_from_DD_TAGS(self): diff --git a/tests/tracer/test_utils.py b/tests/tracer/test_utils.py index a5cee9f284..d1c2533fcc 100644 --- a/tests/tracer/test_utils.py +++ b/tests/tracer/test_utils.py @@ -56,33 +56,46 @@ def test_asbool(self): ("key:val,key2:val2,key3:1234.23", dict(key="val", key2="val2", key3="1234.23"), None), ("key:val key2:val2 key3:1234.23", dict(key="val", key2="val2", key3="1234.23"), None), ("key: val", dict(key=" val"), None), - ("key key: val", {"key key": " val"}, None), + ( + "key key: val", + {"key": "", "val": ""}, + [mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key key: val")], + ), ("key: val,key2:val2", dict(key=" val", key2="val2"), None), (" key: val,key2:val2", {"key": " val", "key2": "val2"}, None), - ("key key2:val1", {"key key2": "val1"}, None), + ("key key2:val1", {"key": "", "key2": "val1"}, None), ("key:val key2:val:2", {"key": "val", "key2": "val:2"}, None), ( "key:val,key2:val2 key3:1234.23", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key:val,key2:val2 key3:1234.23")], ), - ("key:val key2:val2 key3: ", dict(key="val", key2="val2", key3=""), None), + ( + "key:val key2:val2 key3: ", + {"key": "val", "key2": "val2"}, + [mock.call(_LOG_ERROR_MALFORMED_TAG, "key3:", "key:val key2:val2 key3:")], + ), ( "key:val key2:val 2", - dict(key="val", key2="val"), - [mock.call(_LOG_ERROR_MALFORMED_TAG, "2", "key:val key2:val 2")], + {"2": "", "key": "val", "key2": "val"}, + None, ), ( "key: val key2:val2 key3:val3", - {"key": "", "key2": "val2", "key3": "val3"}, - [mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3")], + {"key2": "val2", "key3": "val3", "val": ""}, + [mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key: val key2:val2 key3:val3")], + ), + ( + "key:,key3:val1,", + {"key3": "val1"}, + [mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key:,key3:val1")], ), - ("key:,key3:val1,", dict(key3="val1", key=""), None), (",", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "")]), (":,:", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, ":,:")]), - ("key,key2:val1", {"key2": "val1"}, [mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")]), + ("key,key2:val1", {"key": "", "key2": "val1"}, None), ("key2:val1:", {"key2": "val1:"}, None), - ("key,key2,key3", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key,key2,key3")]), + ("key,key2,key3", {"key": "", "key2": "", "key3": ""}, None), + ("key key2 key3", {"key": "", "key2": "", "key3": ""}, None), ("foo:bar,foo:baz", dict(foo="baz"), None), ("hash:asd url:https://github.com/foo/bar", dict(hash="asd", url="https://github.com/foo/bar"), None), ],