Skip to content

Commit 51b5753

Browse files
authored
fix(celery): coerce non-string values in CeleryGetter.get() (#4360)
* fix(celery): coerce non-string values in CeleryGetter.get() Celery's Context copies all message properties as instance attributes via __dict__.update(), including non-string values like timelimit (tuple of ints) and priority (int). The TextMapPropagator contract requires string values from Getter.get(), but CeleryGetter returned these non-string values as-is, causing TraceState.from_header() to crash with TypeError when calling re.split() on an int. Coerce all non-string values to strings before returning them. See #4359 * Add CHANGELOG entry for CeleryGetter fix * Apply ruff formatting
1 parent 9b5fdd0 commit 51b5753

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5151

5252
### Fixed
5353

54+
- `opentelemetry-instrumentation-celery`: Coerce non-string values to strings in `CeleryGetter.get()` to prevent `TypeError` in `TraceState.from_header()` when Celery request attributes contain ints
55+
([#4360](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4360))
5456
- `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes`
5557
([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339))
5658
- `opentelemetry-instrumentation-confluent-kafka`: Skip `recv` span creation when `poll()` returns no message or `consume()` returns an empty list, avoiding empty spans on idle polls

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,18 @@ def get(self, carrier, key):
104104
value = getattr(carrier, key, None)
105105
if value is None:
106106
return None
107-
if isinstance(value, str) or not isinstance(value, Iterable):
107+
# Celery's Context copies all message properties as instance
108+
# attributes, including non-string values like timelimit (tuple
109+
# of ints). The TextMapPropagator contract requires string
110+
# values, so coerce anything that isn't already a string.
111+
if isinstance(value, str):
108112
value = (value,)
113+
elif isinstance(value, Iterable):
114+
value = tuple(
115+
str(v) if not isinstance(v, str) else v for v in value
116+
)
117+
else:
118+
value = (str(value),)
109119
return value
110120

111121
def keys(self, carrier):

instrumentation/opentelemetry-instrumentation-celery/tests/test_getter.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,40 @@ def test_get_iter(self):
3636
getter = CeleryGetter()
3737
mock_obj.test = ["val"]
3838
val = getter.get(mock_obj, "test")
39-
self.assertEqual(val, ["val"])
39+
self.assertEqual(val, ("val",))
40+
41+
def test_get_int(self):
42+
"""Non-string scalar values should be coerced to strings.
43+
44+
Celery's Context stores some attributes as ints (e.g. priority).
45+
The TextMapPropagator contract requires string values; passing
46+
an int to re.split() in TraceState.from_header() causes a
47+
TypeError.
48+
"""
49+
mock_obj = mock.Mock()
50+
getter = CeleryGetter()
51+
mock_obj.test = 42
52+
val = getter.get(mock_obj, "test")
53+
self.assertEqual(val, ("42",))
54+
55+
def test_get_iter_with_non_string_elements(self):
56+
"""Iterable values containing non-strings should be coerced.
57+
58+
Celery's timelimit attribute is a tuple of ints, e.g. (300, 60).
59+
"""
60+
mock_obj = mock.Mock()
61+
getter = CeleryGetter()
62+
mock_obj.test = (300, 60)
63+
val = getter.get(mock_obj, "test")
64+
self.assertEqual(val, ("300", "60"))
65+
66+
def test_get_iter_with_mixed_types(self):
67+
"""Iterables with a mix of strings and non-strings."""
68+
mock_obj = mock.Mock()
69+
getter = CeleryGetter()
70+
mock_obj.test = ["val", 123]
71+
val = getter.get(mock_obj, "test")
72+
self.assertEqual(val, ("val", "123"))
4073

4174
def test_keys(self):
4275
getter = CeleryGetter()

0 commit comments

Comments
 (0)