Skip to content

Commit 3486631

Browse files
authored
Merge branch 'main' into cbeauchesne/crash-investigation
2 parents 79ce71f + 3e1a49a commit 3486631

File tree

16 files changed

+168
-22
lines changed

16 files changed

+168
-22
lines changed

.gitlab/benchmarks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ variables:
3333
UPSTREAM_COMMIT_SHA: $CI_COMMIT_SHA # The commit revision the project is built for.
3434
KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: dd-trace-py
3535
FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true"
36+
CARGO_NET_GIT_FETCH_WITH_CLI: "true" # use system git binary to pull git dependencies
3637

3738
benchmarks-pr-comment:
3839
stage: benchmarks-pr-comment

ddtrace/_trace/_span_link.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,10 @@ def to_dict(self):
109109
d["flags"] = self.flags
110110

111111
return d
112+
113+
def __str__(self) -> str:
114+
attrs_str = ",".join([f"{k}:{v}" for k, v in self.attributes.items()])
115+
return (
116+
f"trace_id={self.trace_id} span_id={self.span_id} attributes={attrs_str} "
117+
f"tracestate={self.tracestate} flags={self.flags} dropped_attributes={self._dropped_attributes}"
118+
)

ddtrace/_trace/span.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def __dict__(self):
7272
d["attributes"] = self.attributes
7373
return d
7474

75+
def __str__(self):
76+
attrs_str = ",".join([f"{k}:{v}" for k, v in self.attributes.items()])
77+
return f"name={self.name} time={self.time_unix_nano} attributes={attrs_str}"
78+
7579

7680
log = get_logger(__name__)
7781

@@ -569,6 +573,8 @@ def _pprint(self) -> str:
569573
("error", self.error),
570574
("tags", dict(sorted(self._meta.items()))),
571575
("metrics", dict(sorted(self._metrics.items()))),
576+
("links", ", ".join([str(link) for _, link in self._links.items()])),
577+
("events", ", ".join([str(e) for e in self._events])),
572578
]
573579
return " ".join(
574580
# use a large column width to keep pprint output on one line

ddtrace/contrib/trace_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def _store_headers(headers, span, integration_config, request_or_response):
144144
return
145145

146146
for header_name, header_value in headers.items():
147-
"""config._header_tag_name gets an element of the dictionary in config.http._header_tags
147+
"""config._header_tag_name gets an element of the dictionary in config.trace_http_header_tags
148148
which gets the value from DD_TRACE_HEADER_TAGS environment variable."""
149149
tag_name = integration_config._header_tag_name(header_name)
150150
if tag_name is None:

ddtrace/internal/utils/formats.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def parse_tags_str(tags_str):
7070
The expected string is of the form::
7171
"key1:value1,key2:value2"
7272
"key1:value1 key2:value2"
73+
"key1,key2"
74+
"key1 key2"
7375
7476
:param tags_str: A string of the above form to parse tags from.
7577
:return: A dict containing the tags that were parsed.
@@ -86,10 +88,14 @@ def parse_tags(tags):
8688

8789
for tag in tags:
8890
key, sep, value = tag.partition(":")
89-
if not sep or not key or "," in key:
91+
if not key.strip() or "," in key or (sep and not value):
9092
invalids.append(tag)
91-
else:
93+
elif sep:
94+
# parse key:val,key2:value2
9295
parsed_tags.append((key, value))
96+
else:
97+
# parse key,key2
98+
parsed_tags.append((key, ""))
9399

94100
return parsed_tags, invalids
95101

ddtrace/settings/crashtracker.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
from ddtrace.internal.utils.formats import parse_tags_str
66

77

8+
resolver_default = "full"
9+
10+
811
def _derive_stacktrace_resolver(config: "CrashtrackerConfig") -> t.Optional[str]:
912
resolver = str(config._stacktrace_resolver or "")
1013
resolver = resolver.lower()
14+
if resolver == "none":
15+
return None
1116
if resolver in ("fast", "full", "safe"):
1217
return resolver
13-
return None
18+
19+
# Invalid values should degrade to the default
20+
return resolver_default
1421

1522

1623
def _check_for_crashtracker_available() -> bool:
@@ -75,10 +82,10 @@ class CrashtrackerConfig(En):
7582
_stacktrace_resolver = En.v(
7683
t.Optional[str],
7784
"stacktrace_resolver",
78-
default=None,
85+
default=resolver_default,
7986
help_type="String",
8087
help="How to collect native stack traces during a crash, if at all. Accepted values are 'none', 'fast',"
81-
" 'safe', and 'full'. The default value is 'none' (no stack traces).",
88+
" 'safe', and 'full'. The default value is '" + resolver_default + "'.",
8289
)
8390
stacktrace_resolver = En.d(t.Optional[str], _derive_stacktrace_resolver)
8491

docs/configuration.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ The following environment variables for the tracer are supported:
3131
DD_TAGS:
3232
description: |
3333
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``.
34+
35+
If a tag value is not supplied the value will be an empty string. e.g. ``key1,key2`` or ``key1 key2``.
3436
version_added:
3537
v0.38.0: Comma separated support added
3638
v0.48.0: Space separated support added
@@ -290,9 +292,11 @@ The following environment variables for the tracer are supported:
290292

291293
DD_TRACE_HEADER_TAGS:
292294
description: |
293-
A map of case-insensitive header keys to tag names. Automatically applies matching header values as tags on root spans.
295+
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
296+
``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``.
294297

295-
For example, ``User-Agent:http.useragent,content-type:http.content_type``.
298+
If a tag name is not supplied the header name will be used. For example if
299+
``DD_TRACE_HEADER_TAGS=User-Agent,content-type``. The value of http header will be stored in tags with the names ``http.<response/request>.headers.user-agent`` and ``http.<response/request>.headers.content-type``.
296300

297301
DD_TRACE_API_VERSION:
298302
default: |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
tracing: Updates ``DD_HEADER_TAGS`` and ``DD_TAGS`` to support the following formats:
5+
``key1,key2,key3``, ``key1:val,key2:val,key3:val3``, ``key1:val key2:val key3:val3``, and ``key1 key2 key3``.
6+
Key value pairs that do not match an expected format will be logged and ignored by the tracer.

src/core/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ pyo3-build-config = "0.21.2"
2121
name = "_core"
2222
path = "lib.rs"
2323
crate-type = ["cdylib"]
24+
25+
26+
[net]
27+
# Use git binary from the system instead of the built-in git client
28+
# "Setting this to true can be helpful if you have special authentication requirements that Cargo does not support."
29+
git-fetch-with-cli = true

tests/internal/crashtracker/test_crashtracker.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,9 @@ def test_crashtracker_preload_default(ddtrace_run_python_code_in_subprocess):
306306
conn = utils.listen_get_conn(sock)
307307
assert conn
308308
data = utils.conn_to_bytes(conn)
309+
conn.close()
309310
assert data
311+
assert b"string_at" in data
310312

311313

312314
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Linux only")
@@ -359,7 +361,35 @@ def test_crashtracker_auto_default(run_python_code_in_subprocess):
359361
conn = utils.listen_get_conn(sock)
360362
assert conn
361363
data = utils.conn_to_bytes(conn)
364+
conn.close()
365+
assert data
366+
assert b"string_at" in data
367+
368+
369+
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Linux only")
370+
def test_crashtracker_auto_nostack(run_python_code_in_subprocess):
371+
# Setup the listening socket before we open ddtrace
372+
port, sock = utils.crashtracker_receiver_bind()
373+
assert sock
374+
375+
# Call the program
376+
env = os.environ.copy()
377+
env["DD_TRACE_AGENT_URL"] = "http://localhost:%d" % port
378+
env["DD_CRASHTRACKER_STACKTRACE_RESOLVER"] = "none"
379+
stdout, stderr, exitcode, _ = run_python_code_in_subprocess(auto_code, env=env)
380+
381+
# Check for expected exit condition
382+
assert not stdout
383+
assert not stderr
384+
assert exitcode == -11
385+
386+
# Wait for the connection
387+
conn = utils.listen_get_conn(sock)
388+
assert conn
389+
data = utils.conn_to_bytes(conn)
390+
conn.close()
362391
assert data
392+
assert b"string_at" not in data
363393

364394

365395
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Linux only")

tests/internal/test_settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ def _deleted_rc_config():
103103
"expected": {"trace_http_header_tags": {"header": "value"}},
104104
"expected_source": {"trace_http_header_tags": "code"},
105105
},
106+
{
107+
"env": {"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1,X-Header-Tag-2,X-Header-Tag-3:specific_tag3"},
108+
"expected": {
109+
"trace_http_header_tags": {
110+
"X-Header-Tag-1": "",
111+
"X-Header-Tag-2": "",
112+
"X-Header-Tag-3": "specific_tag3",
113+
}
114+
},
115+
"expected_source": {"trace_http_header_tags": "env_var"},
116+
},
106117
{
107118
"env": {"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2"},
108119
"rc": {

tests/telemetry/test_writer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time):
133133
{"name": "crashtracking_available", "origin": "unknown", "value": sys.platform == "linux"},
134134
{"name": "crashtracking_debug_url", "origin": "unknown", "value": "None"},
135135
{"name": "crashtracking_enabled", "origin": "unknown", "value": sys.platform == "linux"},
136-
{"name": "crashtracking_stacktrace_resolver", "origin": "unknown", "value": "None"},
136+
{"name": "crashtracking_stacktrace_resolver", "origin": "unknown", "value": "full"},
137137
{"name": "crashtracking_started", "origin": "unknown", "value": False},
138138
{"name": "crashtracking_stderr_filename", "origin": "unknown", "value": "None"},
139139
{"name": "crashtracking_stdout_filename", "origin": "unknown", "value": "None"},
@@ -323,7 +323,7 @@ def test_app_started_event_configuration_override(
323323
{"name": "crashtracking_available", "origin": "unknown", "value": sys.platform == "linux"},
324324
{"name": "crashtracking_debug_url", "origin": "unknown", "value": "None"},
325325
{"name": "crashtracking_enabled", "origin": "unknown", "value": sys.platform == "linux"},
326-
{"name": "crashtracking_stacktrace_resolver", "origin": "unknown", "value": "None"},
326+
{"name": "crashtracking_stacktrace_resolver", "origin": "unknown", "value": "full"},
327327
{"name": "crashtracking_started", "origin": "unknown", "value": sys.platform == "linux"},
328328
{"name": "crashtracking_stderr_filename", "origin": "unknown", "value": "None"},
329329
{"name": "crashtracking_stdout_filename", "origin": "unknown", "value": "None"},

tests/tracer/test_span.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,9 @@ def test_span_pprint():
695695
root = Span("test.span", service="s", resource="r", span_type=SpanTypes.WEB)
696696
root.set_tag("t", "v")
697697
root.set_metric("m", 1.0)
698+
root._add_event("message", {"importance": 10}, 16789898242)
699+
root.set_link(trace_id=99, span_id=10, attributes={"link.name": "s1_to_s2", "link.kind": "scheduled_by"})
700+
698701
root.finish()
699702
actual = root._pprint()
700703
assert "name='test.span'" in actual
@@ -704,6 +707,11 @@ def test_span_pprint():
704707
assert "error=0" in actual
705708
assert "tags={'t': 'v'}" in actual
706709
assert "metrics={'m': 1.0}" in actual
710+
assert "events='name=message time=16789898242 attributes=importance:10'" in actual
711+
assert (
712+
"links='trace_id=99 span_id=10 attributes=link.name:s1_to_s2,link.kind:scheduled_by "
713+
"tracestate=None flags=None dropped_attributes=0'" in actual
714+
)
707715
assert re.search("id=[0-9]+", actual) is not None
708716
assert re.search("trace_id=[0-9]+", actual) is not None
709717
assert "parent_id=None" in actual

tests/tracer/test_trace_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,47 @@ def test_ext_service(int_config, pin, config_val, default, expected):
306306
assert trace_utils.ext_service(pin, int_config.myint, default) == expected
307307

308308

309+
@pytest.mark.subprocess(
310+
parametrize={
311+
"DD_TRACE_HEADER_TAGS": ["header1 header2 header3:third-header", "header1,header2,header3:third-header"]
312+
}
313+
)
314+
def test_set_http_meta_with_http_header_tags_config():
315+
from ddtrace import config
316+
from ddtrace._trace.span import Span
317+
from ddtrace.contrib.trace_utils import set_http_meta
318+
319+
assert config.trace_http_header_tags == {
320+
"header1": "",
321+
"header2": "",
322+
"header3": "third-header",
323+
}, config.trace_http_header_tags
324+
integration_config = config.new_integration
325+
assert integration_config.is_header_tracing_configured
326+
327+
# test request headers
328+
request_span = Span(name="new_integration.request")
329+
set_http_meta(
330+
request_span,
331+
integration_config,
332+
request_headers={"header1": "value1", "header2": "value2", "header3": "value3"},
333+
)
334+
assert request_span.get_tag("http.request.headers.header1") == "value1"
335+
assert request_span.get_tag("http.request.headers.header2") == "value2"
336+
assert request_span.get_tag("third-header") == "value3"
337+
338+
# test response headers
339+
response_span = Span(name="new_integration.response")
340+
set_http_meta(
341+
response_span,
342+
integration_config,
343+
response_headers={"header1": "value1", "header2": "value2", "header3": "value3"},
344+
)
345+
assert response_span.get_tag("http.response.headers.header1") == "value1"
346+
assert response_span.get_tag("http.response.headers.header2") == "value2"
347+
assert response_span.get_tag("third-header") == "value3"
348+
349+
309350
@pytest.mark.parametrize("appsec_enabled", [False, True])
310351
@pytest.mark.parametrize("span_type", [SpanTypes.WEB, SpanTypes.HTTP, None])
311352
@pytest.mark.parametrize(

tests/tracer/test_tracer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -979,11 +979,11 @@ def test_dd_tags(self):
979979
assert self.tracer._tags.get("key1") == "value1"
980980
assert self.tracer._tags.get("key2") == "value2"
981981

982-
@run_in_subprocess(env_overrides=dict(DD_TAGS="key1:value1,key2:value2,key3"))
982+
@run_in_subprocess(env_overrides=dict(DD_TAGS="key1:value1,key2:value2, key3"))
983983
def test_dd_tags_invalid(self):
984984
assert self.tracer._tags.get("key1")
985985
assert self.tracer._tags.get("key2")
986-
assert self.tracer._tags.get("key3") is None
986+
assert not self.tracer._tags.get("key3")
987987

988988
@run_in_subprocess(env_overrides=dict(DD_TAGS="service:mysvc,env:myenv,version:myvers"))
989989
def test_tags_from_DD_TAGS(self):

tests/tracer/test_utils.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,46 @@ def test_asbool(self):
5656
("key:val,key2:val2,key3:1234.23", dict(key="val", key2="val2", key3="1234.23"), None),
5757
("key:val key2:val2 key3:1234.23", dict(key="val", key2="val2", key3="1234.23"), None),
5858
("key: val", dict(key=" val"), None),
59-
("key key: val", {"key key": " val"}, None),
59+
(
60+
"key key: val",
61+
{"key": "", "val": ""},
62+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key key: val")],
63+
),
6064
("key: val,key2:val2", dict(key=" val", key2="val2"), None),
6165
(" key: val,key2:val2", {"key": " val", "key2": "val2"}, None),
62-
("key key2:val1", {"key key2": "val1"}, None),
66+
("key key2:val1", {"key": "", "key2": "val1"}, None),
6367
("key:val key2:val:2", {"key": "val", "key2": "val:2"}, None),
6468
(
6569
"key:val,key2:val2 key3:1234.23",
6670
dict(),
6771
[mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key:val,key2:val2 key3:1234.23")],
6872
),
69-
("key:val key2:val2 key3: ", dict(key="val", key2="val2", key3=""), None),
73+
(
74+
"key:val key2:val2 key3: ",
75+
{"key": "val", "key2": "val2"},
76+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key3:", "key:val key2:val2 key3:")],
77+
),
7078
(
7179
"key:val key2:val 2",
72-
dict(key="val", key2="val"),
73-
[mock.call(_LOG_ERROR_MALFORMED_TAG, "2", "key:val key2:val 2")],
80+
{"2": "", "key": "val", "key2": "val"},
81+
None,
7482
),
7583
(
7684
"key: val key2:val2 key3:val3",
77-
{"key": "", "key2": "val2", "key3": "val3"},
78-
[mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3")],
85+
{"key2": "val2", "key3": "val3", "val": ""},
86+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key: val key2:val2 key3:val3")],
87+
),
88+
(
89+
"key:,key3:val1,",
90+
{"key3": "val1"},
91+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key:,key3:val1")],
7992
),
80-
("key:,key3:val1,", dict(key3="val1", key=""), None),
8193
(",", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "")]),
8294
(":,:", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, ":,:")]),
83-
("key,key2:val1", {"key2": "val1"}, [mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")]),
95+
("key,key2:val1", {"key": "", "key2": "val1"}, None),
8496
("key2:val1:", {"key2": "val1:"}, None),
85-
("key,key2,key3", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key,key2,key3")]),
97+
("key,key2,key3", {"key": "", "key2": "", "key3": ""}, None),
98+
("key key2 key3", {"key": "", "key2": "", "key3": ""}, None),
8699
("foo:bar,foo:baz", dict(foo="baz"), None),
87100
("hash:asd url:https://github.com/foo/bar", dict(hash="asd", url="https://github.com/foo/bar"), None),
88101
],

0 commit comments

Comments
 (0)