Skip to content

Commit 6f3aead

Browse files
authoredSep 3, 2023
Unwrap Celery's ExceptionInfo (#1863)
* Unwrap `ExceptionInfo` and `ExceptionWithTraceback` Instead of reporting the `ExceptionInfo` and `ExceptionWithTraceback` wrappers raised by Celery, report the exceptions that they wrap. This ensures that the exception in the OpenTelemetry span has a type and traceback that are meaningful and relevant to the developer. * Fix typo The exception is expected, not excepted. Well, I guess it is also excepted, because it's an exception, but you get what I mean. * Reformat file with `black` Reformat the `__init__.py` file in the Celery instrumentation using `black`, fixing a CI linter error. * Address review feedback Use the VERSION attribute exposed by Billiard to decide whether to import ExceptionWithTraceback. Add a test for a failing task and check that the exceptions' type and message are preserved. * Amend ExceptionWithTraceback import
1 parent a02d98c commit 6f3aead

File tree

6 files changed

+103
-4
lines changed

6 files changed

+103
-4
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
([#1889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1889))
1414
- Fixed union typing error not compatible with Python 3.7 introduced in `opentelemetry-util-http`, fix tests introduced by patch related to sanitize method for wsgi
1515
([#1913](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1913))
16+
- `opentelemetry-instrumentation-celery` Unwrap Celery's `ExceptionInfo` errors and report the actual exception that was raised. ([#1863](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1863))
1617

1718
### Added
1819

‎instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def add(x, y):
6363
from timeit import default_timer
6464
from typing import Collection, Iterable
6565

66+
from billiard.einfo import ExceptionInfo
6667
from celery import signals # pylint: disable=no-name-in-module
6768

6869
from opentelemetry import trace
@@ -75,6 +76,13 @@ def add(x, y):
7576
from opentelemetry.propagators.textmap import Getter
7677
from opentelemetry.semconv.trace import SpanAttributes
7778
from opentelemetry.trace.status import Status, StatusCode
79+
from billiard import VERSION
80+
81+
82+
if VERSION >= (4, 0, 1):
83+
from billiard.einfo import ExceptionWithTraceback
84+
else:
85+
ExceptionWithTraceback = None
7886

7987
logger = logging.getLogger(__name__)
8088

@@ -271,6 +279,18 @@ def _trace_failure(*args, **kwargs):
271279
return
272280

273281
if ex is not None:
282+
# Unwrap the actual exception wrapped by billiard's
283+
# `ExceptionInfo` and `ExceptionWithTraceback`.
284+
if isinstance(ex, ExceptionInfo) and ex.exception is not None:
285+
ex = ex.exception
286+
287+
if (
288+
ExceptionWithTraceback is not None
289+
and isinstance(ex, ExceptionWithTraceback)
290+
and ex.exc is not None
291+
):
292+
ex = ex.exc
293+
274294
status_kwargs["description"] = str(ex)
275295
span.record_exception(ex)
276296
span.set_status(Status(**status_kwargs))

‎instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import logging
1616

1717
from celery import registry # pylint: disable=no-name-in-module
18+
from billiard import VERSION
1819

1920
from opentelemetry.semconv.trace import SpanAttributes
2021

‎instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ class Config:
2424
app.config_from_object(Config)
2525

2626

27+
class CustomError(Exception):
28+
pass
29+
30+
2731
@app.task
2832
def task_add(num_a, num_b):
2933
return num_a + num_b
34+
35+
36+
@app.task
37+
def task_raises():
38+
raise CustomError("The task failed!")

‎instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py

+70-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from opentelemetry.instrumentation.celery import CeleryInstrumentor
1919
from opentelemetry.semconv.trace import SpanAttributes
2020
from opentelemetry.test.test_base import TestBase
21-
from opentelemetry.trace import SpanKind
21+
from opentelemetry.trace import SpanKind, StatusCode
2222

23-
from .celery_test_tasks import app, task_add
23+
from .celery_test_tasks import app, task_add, task_raises
2424

2525

2626
class TestCeleryInstrumentation(TestBase):
@@ -66,6 +66,10 @@ def test_task(self):
6666
},
6767
)
6868

69+
self.assertEqual(consumer.status.status_code, StatusCode.UNSET)
70+
71+
self.assertEqual(0, len(consumer.events))
72+
6973
self.assertEqual(
7074
producer.name, "apply_async/tests.celery_test_tasks.task_add"
7175
)
@@ -84,6 +88,70 @@ def test_task(self):
8488
self.assertEqual(consumer.parent.span_id, producer.context.span_id)
8589
self.assertEqual(consumer.context.trace_id, producer.context.trace_id)
8690

91+
def test_task_raises(self):
92+
CeleryInstrumentor().instrument()
93+
94+
result = task_raises.delay()
95+
96+
timeout = time.time() + 60 * 1 # 1 minutes from now
97+
while not result.ready():
98+
if time.time() > timeout:
99+
break
100+
time.sleep(0.05)
101+
102+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
103+
self.assertEqual(len(spans), 2)
104+
105+
consumer, producer = spans
106+
107+
self.assertEqual(
108+
consumer.name, "run/tests.celery_test_tasks.task_raises"
109+
)
110+
self.assertEqual(consumer.kind, SpanKind.CONSUMER)
111+
self.assertSpanHasAttributes(
112+
consumer,
113+
{
114+
"celery.action": "run",
115+
"celery.state": "FAILURE",
116+
SpanAttributes.MESSAGING_DESTINATION: "celery",
117+
"celery.task_name": "tests.celery_test_tasks.task_raises",
118+
},
119+
)
120+
121+
self.assertEqual(consumer.status.status_code, StatusCode.ERROR)
122+
123+
self.assertEqual(1, len(consumer.events))
124+
event = consumer.events[0]
125+
126+
self.assertIn(SpanAttributes.EXCEPTION_STACKTRACE, event.attributes)
127+
128+
self.assertEqual(
129+
event.attributes[SpanAttributes.EXCEPTION_TYPE], "CustomError"
130+
)
131+
132+
self.assertEqual(
133+
event.attributes[SpanAttributes.EXCEPTION_MESSAGE],
134+
"The task failed!",
135+
)
136+
137+
self.assertEqual(
138+
producer.name, "apply_async/tests.celery_test_tasks.task_raises"
139+
)
140+
self.assertEqual(producer.kind, SpanKind.PRODUCER)
141+
self.assertSpanHasAttributes(
142+
producer,
143+
{
144+
"celery.action": "apply_async",
145+
"celery.task_name": "tests.celery_test_tasks.task_raises",
146+
SpanAttributes.MESSAGING_DESTINATION_KIND: "queue",
147+
SpanAttributes.MESSAGING_DESTINATION: "celery",
148+
},
149+
)
150+
151+
self.assertNotEqual(consumer.parent, producer.context)
152+
self.assertEqual(consumer.parent.span_id, producer.context.span_id)
153+
self.assertEqual(consumer.context.trace_id, producer.context.trace_id)
154+
87155
def test_uninstrument(self):
88156
CeleryInstrumentor().instrument()
89157
CeleryInstrumentor().uninstrument()

‎tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def fn_exception():
279279
assert len(span.events) == 1
280280
event = span.events[0]
281281
assert event.name == "exception"
282-
assert event.attributes[SpanAttributes.EXCEPTION_TYPE] == "ExceptionInfo"
282+
assert event.attributes[SpanAttributes.EXCEPTION_TYPE] == "Exception"
283283
assert SpanAttributes.EXCEPTION_MESSAGE in event.attributes
284284
assert (
285285
span.attributes.get(SpanAttributes.MESSAGING_MESSAGE_ID)
@@ -420,7 +420,7 @@ def run(self):
420420
assert "Task class is failing" in span.status.description
421421

422422

423-
def test_class_task_exception_excepted(celery_app, memory_exporter):
423+
def test_class_task_exception_expected(celery_app, memory_exporter):
424424
class BaseTask(celery_app.Task):
425425
throws = (MyException,)
426426

0 commit comments

Comments
 (0)
Please sign in to comment.