diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7097976f..28f32f3a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `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 + ([#4360](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4360)) - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes` ([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339)) - `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 diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 3d6691f382..ca61e9b455 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -104,8 +104,18 @@ def get(self, carrier, key): value = getattr(carrier, key, None) if value is None: return None - if isinstance(value, str) or not isinstance(value, Iterable): + # Celery's Context copies all message properties as instance + # attributes, including non-string values like timelimit (tuple + # of ints). The TextMapPropagator contract requires string + # values, so coerce anything that isn't already a string. + if isinstance(value, str): value = (value,) + elif isinstance(value, Iterable): + value = tuple( + str(v) if not isinstance(v, str) else v for v in value + ) + else: + value = (str(value),) return value def keys(self, carrier): diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_getter.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_getter.py index 5943055f00..eb3d632f6e 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_getter.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_getter.py @@ -36,7 +36,40 @@ def test_get_iter(self): getter = CeleryGetter() mock_obj.test = ["val"] val = getter.get(mock_obj, "test") - self.assertEqual(val, ["val"]) + self.assertEqual(val, ("val",)) + + def test_get_int(self): + """Non-string scalar values should be coerced to strings. + + Celery's Context stores some attributes as ints (e.g. priority). + The TextMapPropagator contract requires string values; passing + an int to re.split() in TraceState.from_header() causes a + TypeError. + """ + mock_obj = mock.Mock() + getter = CeleryGetter() + mock_obj.test = 42 + val = getter.get(mock_obj, "test") + self.assertEqual(val, ("42",)) + + def test_get_iter_with_non_string_elements(self): + """Iterable values containing non-strings should be coerced. + + Celery's timelimit attribute is a tuple of ints, e.g. (300, 60). + """ + mock_obj = mock.Mock() + getter = CeleryGetter() + mock_obj.test = (300, 60) + val = getter.get(mock_obj, "test") + self.assertEqual(val, ("300", "60")) + + def test_get_iter_with_mixed_types(self): + """Iterables with a mix of strings and non-strings.""" + mock_obj = mock.Mock() + getter = CeleryGetter() + mock_obj.test = ["val", 123] + val = getter.get(mock_obj, "test") + self.assertEqual(val, ("val", "123")) def test_keys(self): getter = CeleryGetter()