Skip to content

Commit 5c04ce4

Browse files
authored
set the span id as parent id when errors happen in spans (#669)
* set the span id as parent id when errors happen in spans * update changelog
1 parent d5df762 commit 5c04ce4

File tree

6 files changed

+88
-11
lines changed

6 files changed

+88
-11
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ https://github.com/elastic/apm-agent-python/compare/v5.3.1\...master[Check the d
2929
===== New Features
3030
3131
* Added support for W3C `traceparent` and `tracestate` headers {pull}660[#660]
32+
* use Span ID as parent ID in errors if an error happens inside a span {pull}669[#669]
3233
3334
3435
[[release-notes-5.x]]

elasticapm/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def _build_msg_for_logging(
361361
Captures, processes and serializes an event into a dict object
362362
"""
363363
transaction = execution_context.get_transaction()
364+
span = execution_context.get_span()
364365
if transaction:
365366
transaction_context = deepcopy(transaction.context)
366367
else:
@@ -453,7 +454,8 @@ def _build_msg_for_logging(
453454
if transaction:
454455
if transaction.trace_parent:
455456
event_data["trace_id"] = transaction.trace_parent.trace_id
456-
event_data["parent_id"] = transaction.id
457+
# parent id might already be set in the handler
458+
event_data.setdefault("parent_id", span.id if span else transaction.id)
457459
event_data["transaction_id"] = transaction.id
458460
event_data["transaction"] = {"sampled": transaction.is_sampled, "type": transaction.transaction_type}
459461

elasticapm/contrib/asyncio/traces.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import functools
3232

33-
from elasticapm.traces import capture_span, error_logger, execution_context
33+
from elasticapm.traces import DroppedSpan, capture_span, error_logger, execution_context
3434
from elasticapm.utils import get_name_from_func
3535

3636

@@ -64,6 +64,12 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
6464
transaction = execution_context.get_transaction()
6565
if transaction and transaction.is_sampled:
6666
try:
67-
transaction.end_span(self.skip_frames)
67+
span = transaction.end_span(self.skip_frames)
68+
if exc_val and not isinstance(span, DroppedSpan):
69+
try:
70+
exc_val._elastic_apm_span_id = span.id
71+
except AttributeError:
72+
# could happen if the exception has __slots__
73+
pass
6874
except LookupError:
6975
error_logger.info("ended non-existing span %s of type %s", self.name, self.type)

elasticapm/events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ def capture(client, exc_info=None, **kwargs):
143143
"stacktrace": frames,
144144
},
145145
}
146+
if hasattr(exc_value, "_elastic_apm_span_id"):
147+
data["parent_id"] = exc_value._elastic_apm_span_id
148+
del exc_value._elastic_apm_span_id
146149
if compat.PY3:
147150
depth = kwargs.get("_exc_chain_depth", 0)
148151
if depth > EXCEPTION_CHAIN_MAX_DEPTH:

elasticapm/traces.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,9 +611,16 @@ def __enter__(self):
611611

612612
def __exit__(self, exc_type, exc_val, exc_tb):
613613
transaction = execution_context.get_transaction()
614+
614615
if transaction and transaction.is_sampled:
615616
try:
616-
transaction.end_span(self.skip_frames, duration=self.duration)
617+
span = transaction.end_span(self.skip_frames, duration=self.duration)
618+
if exc_val and not isinstance(span, DroppedSpan):
619+
try:
620+
exc_val._elastic_apm_span_id = span.id
621+
except AttributeError:
622+
# could happen if the exception has __slots__
623+
pass
617624
except LookupError:
618625
logger.info("ended non-existing span %s of type %s", self.name, self.type)
619626

tests/client/exception_tests.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,19 +228,77 @@ def test_collect_source_errors(elasticapm_client):
228228
assert "post_context" not in in_app_frame, in_app_frame_context
229229

230230

231-
def test_transaction_data_is_attached_to_errors(elasticapm_client):
231+
def test_transaction_data_is_attached_to_errors_no_transaction(elasticapm_client):
232232
elasticapm_client.capture_message("noid")
233233
elasticapm_client.begin_transaction("test")
234-
elasticapm_client.capture_message("id")
235-
transaction = elasticapm_client.end_transaction("test", "test")
234+
elasticapm_client.end_transaction("test", "test")
236235
elasticapm_client.capture_message("noid")
237236

238237
errors = elasticapm_client.events[ERROR]
239238
assert "transaction_id" not in errors[0]
240-
assert errors[1]["transaction_id"] == transaction.id
241-
assert errors[1]["transaction"]["sampled"]
242-
assert errors[1]["transaction"]["type"] == "test"
243-
assert "transaction_id" not in errors[2]
239+
assert "transaction_id" not in errors[1]
240+
241+
242+
def test_transaction_data_is_attached_to_errors_message_outside_span(elasticapm_client):
243+
elasticapm_client.begin_transaction("test")
244+
elasticapm_client.capture_message("outside_span")
245+
transaction = elasticapm_client.end_transaction("test", "test")
246+
247+
error = elasticapm_client.events[ERROR][0]
248+
assert error["transaction_id"] == transaction.id
249+
assert error["parent_id"] == transaction.id
250+
assert error["transaction"]["sampled"]
251+
assert error["transaction"]["type"] == "test"
252+
253+
254+
def test_transaction_data_is_attached_to_errors_message_in_span(elasticapm_client):
255+
elasticapm_client.begin_transaction("test")
256+
257+
with elasticapm.capture_span("in_span_handler_test") as span_obj:
258+
elasticapm_client.capture_message("in_span")
259+
260+
transaction = elasticapm_client.end_transaction("test", "test")
261+
262+
error = elasticapm_client.events[ERROR][0]
263+
264+
assert error["transaction_id"] == transaction.id
265+
assert error["parent_id"] == span_obj.id
266+
assert error["transaction"]["sampled"]
267+
assert error["transaction"]["type"] == "test"
268+
269+
270+
def test_transaction_data_is_attached_to_errors_exc_handled_in_span(elasticapm_client):
271+
elasticapm_client.begin_transaction("test")
272+
with elasticapm.capture_span("in_span_handler_test") as span_obj:
273+
try:
274+
assert False
275+
except AssertionError:
276+
elasticapm_client.capture_exception()
277+
transaction = elasticapm_client.end_transaction("test", "test")
278+
279+
error = elasticapm_client.events[ERROR][0]
280+
281+
assert error["transaction_id"] == transaction.id
282+
assert error["parent_id"] == span_obj.id
283+
assert error["transaction"]["sampled"]
284+
assert error["transaction"]["type"] == "test"
285+
286+
287+
def test_transaction_data_is_attached_to_errors_exc_handled_outside_span(elasticapm_client):
288+
elasticapm_client.begin_transaction("test")
289+
try:
290+
with elasticapm.capture_span("out_of_span_handler_test") as span_obj:
291+
assert False
292+
except AssertionError:
293+
elasticapm_client.capture_exception()
294+
transaction = elasticapm_client.end_transaction("test", "test")
295+
296+
error = elasticapm_client.events[ERROR][0]
297+
298+
assert error["transaction_id"] == transaction.id
299+
assert error["parent_id"] == span_obj.id
300+
assert error["transaction"]["sampled"]
301+
assert error["transaction"]["type"] == "test"
244302

245303

246304
def test_transaction_context_is_used_in_errors(elasticapm_client):

0 commit comments

Comments
 (0)