Skip to content

Commit 5857012

Browse files
committed
Merge branch 'main' into TID-rule
2 parents 252db1a + 51b5753 commit 5857012

File tree

14 files changed

+284
-12
lines changed

14 files changed

+284
-12
lines changed

CHANGELOG.md

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

1616
- Bump `pylint` to `4.0.5`
1717
([#4244](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4244))
18+
- `opentelemetry-instrumentation-sqlite3`: Add uninstrument, error status, suppress, and no-op tests
19+
([#4335](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4335))
20+
21+
### Fixed
22+
23+
- `opentelemetry-instrumentation-pika` Use `ObjectProxy` instead of `BaseObjectProxy` for `ReadyMessagesDequeProxy` to restore iterability with wrapt 2.x
24+
([#4461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4461))
25+
- `opentelemetry-instrumentation-dbapi` Use `ObjectProxy` instead of `BaseObjectProxy` for `TracedCursorProxy` to restore iterability with wrapt 2.x
26+
([#4427](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4427))
27+
- `opentelemetry-instrumentation-flask`: Clean up environ keys in `_teardown_request` to prevent duplicate execution
28+
([#4341](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4341))
1829

1930
### Breaking changes
2031

@@ -42,6 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4253

4354
### Fixed
4455

56+
- `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
57+
([#4360](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4360))
4558
- `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes`
4659
([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339))
4760
- `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

docs-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ sqlalchemy>=1.0
4949
starlette~=0.50
5050
tornado>=5.1.1
5151
tortoise-orm>=0.17.0
52+
wrapt~=2.1
5253

5354
# required by opamp
5455
uuid_utils

docs/nitpick-exceptions.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ py-class=
4444
psycopg.Connection
4545
psycopg.AsyncConnection
4646
ObjectProxy
47+
wrapt.proxies.ObjectProxy
4748
fastapi.applications.FastAPI
4849
starlette.applications.Starlette
4950
_contextvars.Token

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()

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,12 @@
177177

178178
try:
179179
# wrapt 2.0.0+
180-
from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module
180+
from wrapt import ( # pylint: disable=no-name-in-module
181+
BaseObjectProxy,
182+
ObjectProxy,
183+
)
181184
except ImportError:
185+
from wrapt import ObjectProxy
182186
from wrapt import ObjectProxy as BaseObjectProxy
183187

184188
from opentelemetry import trace as trace_api
@@ -805,14 +809,14 @@ async def traced_execution_async(
805809

806810

807811
# pylint: disable=abstract-method,no-member
808-
class TracedCursorProxy(BaseObjectProxy, Generic[CursorT]):
812+
class TracedCursorProxy(ObjectProxy, Generic[CursorT]):
809813
# pylint: disable=unused-argument
810814
def __init__(
811815
self,
812816
cursor: CursorT,
813817
db_api_integration: DatabaseApiIntegration,
814818
):
815-
BaseObjectProxy.__init__(self, cursor)
819+
ObjectProxy.__init__(self, cursor)
816820
self._self_cursor_tracer = CursorTracer[CursorT](db_api_integration)
817821

818822
def execute(self, *args: Any, **kwargs: Any):

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,20 @@ def test_executemany(self):
306306
span = spans_list[0]
307307
self.assertEqual(span.attributes[DB_STATEMENT], "Test query")
308308

309+
# pylint: disable=no-self-use
310+
def test_executemany_iterable_cursor(self):
311+
db_integration = dbapi.DatabaseApiIntegration(
312+
"instrumenting_module_test_name", "testcomponent"
313+
)
314+
mock_connection = db_integration.wrapped_connection(
315+
mock_connect, {}, {}
316+
)
317+
cursor = mock_connection.cursor()
318+
cursor.executemany("Test query")
319+
320+
for _row in cursor:
321+
pass
322+
309323
def test_executemany_comment(self):
310324
connect_module = mock.MagicMock()
311325
connect_module.__name__ = "test"
@@ -1296,13 +1310,17 @@ def __init__(self) -> None:
12961310
self._cnx._cmysql.get_client_info = mock.MagicMock(
12971311
return_value="1.2.3"
12981312
)
1313+
self._items = []
12991314

13001315
# pylint: disable=unused-argument, no-self-use
13011316
def execute(self, query, params=None, throw_exception=False):
13021317
if throw_exception:
13031318
# pylint: disable=broad-exception-raised
13041319
raise Exception("Test Exception")
13051320

1321+
def __iter__(self):
1322+
yield from self._items
1323+
13061324
# pylint: disable=unused-argument, no-self-use
13071325
def executemany(self, query, params=None, throw_exception=False):
13081326
if throw_exception:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,8 @@ def _teardown_request(exc):
636636

637637
if token:
638638
context.detach(token)
639+
flask.request.environ.pop(_ENVIRON_ACTIVATION_KEY, None)
640+
flask.request.environ.pop(_ENVIRON_TOKEN, None)
639641

640642
except (RuntimeError, AttributeError, ValueError) as teardown_exc:
641643
# Log the error but don't raise it to avoid breaking the request handling

instrumentation/opentelemetry-instrumentation-flask/tests/test_flask_compatibility.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
from opentelemetry import trace
2828
from opentelemetry.instrumentation.flask import (
29+
_ENVIRON_ACTIVATION_KEY,
30+
_ENVIRON_TOKEN,
2931
FlaskInstrumentor,
3032
_request_ctx_ref,
3133
)
@@ -354,3 +356,59 @@ def generate():
354356
# This ensures OpenTelemetry contexts are properly cleaned up for streaming responses
355357
# following Logfire's recommendations (see open-telemetry/opentelemetry-python#2606)
356358
# If we reach this point, the Flask 3.1+ streaming context cleanup is working
359+
360+
def test_duplicate_teardown_request_does_not_cause_errors(self):
361+
"""Test that _teardown_request can be called multiple times without errors.
362+
363+
Flask may call teardown_request multiple times in some scenarios.
364+
After the first call, _ENVIRON_ACTIVATION_KEY and _ENVIRON_TOKEN
365+
should be cleaned up so subsequent calls are no-ops.
366+
"""
367+
app = flask.Flask(__name__)
368+
369+
# Register the check handler BEFORE instrumenting so it runs AFTER
370+
# the instrumentation's teardown (Flask uses LIFO order).
371+
cleaned_up = {}
372+
call_count = {"teardown_calls": 0}
373+
374+
@app.teardown_request
375+
def check_cleanup(exc):
376+
cleaned_up["activation_present"] = (
377+
_ENVIRON_ACTIVATION_KEY in flask.request.environ
378+
)
379+
cleaned_up["token_present"] = (
380+
_ENVIRON_TOKEN in flask.request.environ
381+
)
382+
383+
FlaskInstrumentor().instrument_app(app)
384+
385+
# Wrap the instrumentation's teardown to count calls and invoke it
386+
# a second time to simulate duplicate teardown.
387+
instrumentation_teardown = app.teardown_request_funcs[None][-1]
388+
389+
def counting_teardown(exc):
390+
call_count["teardown_calls"] += 1
391+
instrumentation_teardown(exc)
392+
# Call it again to simulate duplicate teardown - should not raise
393+
instrumentation_teardown(exc)
394+
395+
app.teardown_request_funcs[None][-1] = counting_teardown
396+
397+
@app.route("/test")
398+
def test_endpoint():
399+
return "OK"
400+
401+
client = app.test_client()
402+
response = client.get("/test")
403+
self.assertEqual(response.status_code, 200)
404+
# Verify the teardown was actually called
405+
self.assertGreater(call_count["teardown_calls"], 0)
406+
# Verify env keys are cleaned up after teardown
407+
self.assertFalse(
408+
cleaned_up.get("activation_present", True),
409+
"_ENVIRON_ACTIVATION_KEY should be cleaned up after teardown",
410+
)
411+
self.assertFalse(
412+
cleaned_up.get("token_present", True),
413+
"_ENVIRON_TOKEN should be cleaned up after teardown",
414+
)

instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
)
88
from pika.channel import Channel
99
from pika.spec import Basic, BasicProperties
10-
11-
try:
12-
# wrapt 2.0.0+
13-
from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module
14-
except ImportError:
15-
from wrapt import ObjectProxy as BaseObjectProxy
10+
from wrapt import ObjectProxy
1611

1712
from opentelemetry import context, propagate, trace
1813
from opentelemetry.instrumentation.utils import is_instrumentation_enabled
@@ -201,7 +196,7 @@ def _enrich_span(
201196

202197

203198
# pylint:disable=abstract-method
204-
class ReadyMessagesDequeProxy(BaseObjectProxy):
199+
class ReadyMessagesDequeProxy(ObjectProxy):
205200
def __init__(
206201
self,
207202
wrapped,

0 commit comments

Comments
 (0)