Skip to content

Commit a7f7b68

Browse files
fix(internal) cdef Python exceptions being ignored (#3008) (#3014)
* fix(internal) cdef Python exceptions being ignored In a cdef function `raise` statements will create a Python exception, add it to the exception stack, and then immediately return. However, the caller will not explicitly check to see if any exception has occurred unless an exception return value has been provided. Until now these cdef functions when they encountered a `raise` would push the Python exception, and then return the default `int` value of `0`. Which based on how we use the return values `0` means success. This means that anytime we encounter an exception in our encoder we potentially ignore any exceptions and still append the partially encoded trace into the buffer. With this change we are standardizing around `except? -1` which tells the caller that their _might_ be a Python exception if the return value is `-1`. We do this because the `msgpack_*` functions we call will also return `-1` on error, but will not create a Python exception. * add release note * Update integration tests * remove unused import * set file encoding * Update releasenotes/notes/fix-encoding-exceptions-c12900b38741d2bf.yaml Co-authored-by: Tahir H. Butt <[email protected]> Co-authored-by: Tahir H. Butt <[email protected]> (cherry picked from commit 1b6cec4) Co-authored-by: Brett Langdon <[email protected]>
1 parent 9342b58 commit a7f7b68

8 files changed

+97
-74
lines changed

ddtrace/internal/_encoding.pyx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ cdef inline int pack_bytes(msgpack_packer *pk, char *bs, Py_ssize_t l):
7878
return ret
7979

8080

81-
cdef inline int pack_number(msgpack_packer *pk, object n):
81+
cdef inline int pack_number(msgpack_packer *pk, object n) except? -1:
8282
if n is None:
8383
return msgpack_pack_nil(pk)
8484

@@ -101,7 +101,7 @@ cdef inline int pack_number(msgpack_packer *pk, object n):
101101
raise TypeError("Unhandled numeric type: %r" % type(n))
102102

103103

104-
cdef inline int pack_text(msgpack_packer *pk, object text):
104+
cdef inline int pack_text(msgpack_packer *pk, object text) except? -1:
105105
cdef Py_ssize_t L
106106
cdef int ret
107107

@@ -422,7 +422,7 @@ cdef class MsgpackEncoderBase(BufferedEncoder):
422422
cdef void * get_dd_origin_ref(self, str dd_origin):
423423
raise NotImplementedError()
424424

425-
cdef inline int _pack_trace(self, list trace):
425+
cdef inline int _pack_trace(self, list trace) except? -1:
426426
cdef int ret
427427
cdef Py_ssize_t L
428428
cdef void * dd_origin = NULL
@@ -482,7 +482,7 @@ cdef class MsgpackEncoderBase(BufferedEncoder):
482482
cpdef flush(self):
483483
raise NotImplementedError()
484484

485-
cdef int pack_span(self, object span, void *dd_origin):
485+
cdef int pack_span(self, object span, void *dd_origin) except? -1:
486486
raise NotImplementedError()
487487

488488

@@ -496,7 +496,7 @@ cdef class MsgpackEncoderV03(MsgpackEncoderBase):
496496
cdef void * get_dd_origin_ref(self, str dd_origin):
497497
return string_to_buff(dd_origin)
498498

499-
cdef inline int _pack_meta(self, object meta, char *dd_origin):
499+
cdef inline int _pack_meta(self, object meta, char *dd_origin) except? -1:
500500
cdef Py_ssize_t L
501501
cdef int ret
502502
cdef dict d
@@ -524,7 +524,7 @@ cdef class MsgpackEncoderV03(MsgpackEncoderBase):
524524

525525
raise TypeError("Unhandled meta type: %r" % type(meta))
526526

527-
cdef inline int _pack_metrics(self, object metrics):
527+
cdef inline int _pack_metrics(self, object metrics) except? -1:
528528
cdef Py_ssize_t L
529529
cdef int ret
530530
cdef dict d
@@ -546,7 +546,7 @@ cdef class MsgpackEncoderV03(MsgpackEncoderBase):
546546

547547
raise TypeError("Unhandled metrics type: %r" % type(metrics))
548548

549-
cdef int pack_span(self, object span, void *dd_origin):
549+
cdef int pack_span(self, object span, void *dd_origin) except? -1:
550550
cdef int ret
551551
cdef Py_ssize_t L
552552
cdef int has_span_type
@@ -662,7 +662,7 @@ cdef class MsgpackEncoderV05(MsgpackEncoderBase):
662662
cdef void * get_dd_origin_ref(self, str dd_origin):
663663
return <void *> PyLong_AsLong(self._st._index(dd_origin))
664664

665-
cdef int pack_span(self, object span, void *dd_origin):
665+
cdef int pack_span(self, object span, void *dd_origin) except? -1:
666666
cdef int ret
667667

668668
ret = msgpack_pack_array(&self.pk, 12)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
Fix handling of Python exceptions during trace encoding. The tracer will no longer silently fail to encode invalid span data and instead log an exception.

tests/integration/test_encoding.py

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import pytest
66

77
from ddtrace import Tracer
8-
from ddtrace.internal import agent
98

109

1110
AGENT_VERSION = os.environ.get("AGENT_VERSION")
@@ -64,68 +63,3 @@ def test_trace_with_metrics_accepted_by_agent(self, metrics):
6463
tracer.shutdown()
6564
log.warning.assert_not_called()
6665
log.error.assert_not_called()
67-
68-
69-
@pytest.mark.skipif(AGENT_VERSION == "testagent", reason="Test agent doesn't return 400 response for bad trace")
70-
class TestTraceRejectedByAgent:
71-
def _assert_bad_trace_refused_by_agent(self, mock_log):
72-
"""Assert that agent refused a bad trace via log call."""
73-
calls = [
74-
mock.call(
75-
"failed to send traces to Datadog Agent at %s: HTTP error status %s, reason %s",
76-
"{}/{}/traces".format(agent.get_trace_url(), "v0.4"),
77-
400,
78-
"Bad Request",
79-
)
80-
]
81-
mock_log.error.assert_has_calls(calls)
82-
83-
def test_wrong_span_name_type_refused_by_agent(self):
84-
"""Span names should be a text type."""
85-
tracer = Tracer()
86-
with mock.patch("ddtrace.internal.writer.log") as log:
87-
with tracer.trace(123):
88-
pass
89-
tracer.shutdown()
90-
91-
self._assert_bad_trace_refused_by_agent(log)
92-
93-
@pytest.mark.parametrize(
94-
"meta",
95-
[
96-
({"env": "my-env", "tag1": "some_str_1", "tag2": "some_str_2", "tag3": [1, 2, 3]}),
97-
({"env": "test-env", b"tag1": {"wrong_type": True}, b"tag2": "some_str_2", b"tag3": "some_str_3"}),
98-
({"env": "my-test-env", u"😐": "some_str_1", b"tag2": "some_str_2", "unicode": 12345}),
99-
],
100-
)
101-
def test_trace_with_wrong_meta_types_refused_by_agent(self, meta):
102-
tracer = Tracer()
103-
with mock.patch("ddtrace.internal.writer.log") as log:
104-
with tracer.trace("root") as root:
105-
root.meta = meta
106-
for _ in range(499):
107-
with tracer.trace("child") as child:
108-
child.meta = meta
109-
tracer.shutdown()
110-
111-
self._assert_bad_trace_refused_by_agent(log)
112-
113-
@pytest.mark.parametrize(
114-
"metrics",
115-
[
116-
({"num1": 12345, "num2": 53421, "num3": 1, "num4": "not-a-number"}),
117-
({b"num1": 123.45, b"num2": [1, 2, 3], b"num3": 11.0, b"num4": 1.20}),
118-
({u"😐": "123.45", b"num2": "1", "num3": {"is_number": False}, "num4": "12345"}),
119-
],
120-
)
121-
def test_trace_with_wrong_metrics_types_refused_by_agent(self, metrics):
122-
tracer = Tracer()
123-
with mock.patch("ddtrace.internal.writer.log") as log:
124-
with tracer.trace("root") as root:
125-
root.metrics = metrics
126-
for _ in range(499):
127-
with tracer.trace("child") as child:
128-
child.metrics = metrics
129-
tracer.shutdown()
130-
131-
self._assert_bad_trace_refused_by_agent(log)

tests/integration/test_integration_snapshots.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
# -*- coding: utf-8 -*-
12
import multiprocessing
23

4+
import mock
35
import pytest
46

57
from ddtrace import Tracer
@@ -221,3 +223,53 @@ def task2(tracer):
221223
p.start()
222224
p.join()
223225
tracer.shutdown()
226+
227+
228+
@snapshot()
229+
def test_wrong_span_name_type_not_sent():
230+
"""Span names should be a text type."""
231+
tracer = Tracer()
232+
with mock.patch("ddtrace.span.log") as log:
233+
with tracer.trace(123):
234+
pass
235+
log.exception.assert_called_once_with("error closing trace")
236+
237+
238+
@pytest.mark.parametrize(
239+
"meta",
240+
[
241+
({"env": "my-env", "tag1": "some_str_1", "tag2": "some_str_2", "tag3": [1, 2, 3]}),
242+
({"env": "test-env", b"tag1": {"wrong_type": True}, b"tag2": "some_str_2", b"tag3": "some_str_3"}),
243+
({"env": "my-test-env", u"😐": "some_str_1", b"tag2": "some_str_2", "unicode": 12345}),
244+
],
245+
)
246+
@snapshot()
247+
def test_trace_with_wrong_meta_types_not_sent(meta):
248+
tracer = Tracer()
249+
with mock.patch("ddtrace.span.log") as log:
250+
with tracer.trace("root") as root:
251+
root.meta = meta
252+
for _ in range(499):
253+
with tracer.trace("child") as child:
254+
child.meta = meta
255+
log.exception.assert_called_once_with("error closing trace")
256+
257+
258+
@pytest.mark.parametrize(
259+
"metrics",
260+
[
261+
({"num1": 12345, "num2": 53421, "num3": 1, "num4": "not-a-number"}),
262+
({b"num1": 123.45, b"num2": [1, 2, 3], b"num3": 11.0, b"num4": 1.20}),
263+
({u"😐": "123.45", b"num2": "1", "num3": {"is_number": False}, "num4": "12345"}),
264+
],
265+
)
266+
@snapshot()
267+
def test_trace_with_wrong_metrics_types_not_sent(metrics):
268+
tracer = Tracer()
269+
with mock.patch("ddtrace.span.log") as log:
270+
with tracer.trace("root") as root:
271+
root.metrics = metrics
272+
for _ in range(499):
273+
with tracer.trace("child") as child:
274+
child.metrics = metrics
275+
log.exception.assert_called_once_with("error closing trace")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

tests/tracer/test_encoders.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,33 @@ def test_list_string_table():
565565
string_table_test(t)
566566

567567
assert list(t) == ["", "foobar", "foobaz"]
568+
569+
570+
@pytest.mark.parametrize(
571+
"data",
572+
[
573+
{"trace_id": "trace_id"},
574+
{"span_id": "span_id"},
575+
{"parent_id": "parent_id"},
576+
{"service": True},
577+
{"resource": 50},
578+
{"name": [1, 2, 3]},
579+
{"start_ns": "start_time"},
580+
{"duration_ns": "duration_time"},
581+
{"span_type": 100},
582+
{"meta": {"num": 100}},
583+
{"metrics": {"key": "value"}},
584+
],
585+
)
586+
def test_encoding_invalid_data(data):
587+
encoder = MsgpackEncoderV03(1 << 20, 1 << 20)
588+
589+
span = Span(tracer=None, name="test")
590+
for key, value in data.items():
591+
setattr(span, key, value)
592+
593+
trace = [span]
594+
with pytest.raises(TypeError):
595+
encoder.put(trace)
596+
597+
assert encoder.encode() is None

0 commit comments

Comments
 (0)