Skip to content

Commit ce28b31

Browse files
authored
Merge branch 'main' into aiohttp-server-instrumentation
2 parents 653d923 + dadcd01 commit ce28b31

File tree

13 files changed

+225
-36
lines changed

13 files changed

+225
-36
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
([#1870](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1870))
1616
- Update falcon instrumentation to follow semantic conventions
1717
([#1824](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1824))
18+
- Fix sqlalchemy instrumentation wrap methods to accept sqlcommenter options([#1873](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1873))
1819

1920
### Added
2021

@@ -34,12 +35,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3435
([#1833](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1833))
3536
- Fix elasticsearch `Transport.perform_request` instrument wrap for elasticsearch >= 8
3637
([#1810](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1810))
38+
- `opentelemetry-instrumentation-urllib3` Add support for urllib3 version 2
39+
([#1879](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1879))
3740

3841
## Version 1.18.0/0.39b0 (2023-05-10)
3942

4043
- `opentelemetry-instrumentation-system-metrics` Add `process.` prefix to `runtime.memory`, `runtime.cpu.time`, and `runtime.gc_count`. Change `runtime.memory` from count to UpDownCounter. ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735))
4144
- Add request and response hooks for GRPC instrumentation (client only)
4245
([#1706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1706))
46+
- Fix memory leak in SQLAlchemy instrumentation where disposed `Engine` does not get garbage collected
47+
([#1771](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1771)
4348
- `opentelemetry-instrumentation-pymemcache` Update instrumentation to support pymemcache >4
4449
([#1764](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1764))
4550
- `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions of confluent_kafka

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ depend on `opentelemetry-sdk` or another package that implements the API.
4949
**Please note** that these libraries are currently in _beta_, and shouldn't
5050
generally be used in production environments.
5151

52+
Unless explicitly stated otherwise, any instrumentation here for a particular library is not developed or maintained by the authors of such library.
53+
5254
The
5355
[`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation)
5456
directory includes OpenTelemetry instrumentation packages, which can be installed

instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@
4141
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes
4242
| [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No
4343
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes
44-
| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 2.0.0 | Yes
44+
| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes
4545
| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ def _instrument(self, **kwargs):
134134
``engine``: a SQLAlchemy engine instance
135135
``engines``: a list of SQLAlchemy engine instances
136136
``tracer_provider``: a TracerProvider, defaults to global
137+
``meter_provider``: a MeterProvider, defaults to global
138+
``enable_commenter``: bool to enable sqlcommenter, defaults to False
139+
``commenter_options``: dict of sqlcommenter config, defaults to None
137140
138141
Returns:
139142
An instrumented engine if passed in as an argument or list of instrumented engines, None otherwise.
@@ -151,16 +154,21 @@ def _instrument(self, **kwargs):
151154
)
152155

153156
enable_commenter = kwargs.get("enable_commenter", False)
157+
commenter_options = kwargs.get("commenter_options", {})
154158

155159
_w(
156160
"sqlalchemy",
157161
"create_engine",
158-
_wrap_create_engine(tracer, connections_usage, enable_commenter),
162+
_wrap_create_engine(
163+
tracer, connections_usage, enable_commenter, commenter_options
164+
),
159165
)
160166
_w(
161167
"sqlalchemy.engine",
162168
"create_engine",
163-
_wrap_create_engine(tracer, connections_usage, enable_commenter),
169+
_wrap_create_engine(
170+
tracer, connections_usage, enable_commenter, commenter_options
171+
),
164172
)
165173
_w(
166174
"sqlalchemy.engine.base",
@@ -172,7 +180,10 @@ def _instrument(self, **kwargs):
172180
"sqlalchemy.ext.asyncio",
173181
"create_async_engine",
174182
_wrap_create_async_engine(
175-
tracer, connections_usage, enable_commenter
183+
tracer,
184+
connections_usage,
185+
enable_commenter,
186+
commenter_options,
176187
),
177188
)
178189
if kwargs.get("engine") is not None:

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

+51-22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
import os
1515
import re
16+
import weakref
1617

1718
from sqlalchemy.event import ( # pylint: disable=no-name-in-module
1819
listen,
@@ -42,7 +43,7 @@ def _normalize_vendor(vendor):
4243

4344

4445
def _wrap_create_async_engine(
45-
tracer, connections_usage, enable_commenter=False
46+
tracer, connections_usage, enable_commenter=False, commenter_options=None
4647
):
4748
# pylint: disable=unused-argument
4849
def _wrap_create_async_engine_internal(func, module, args, kwargs):
@@ -51,20 +52,32 @@ def _wrap_create_async_engine_internal(func, module, args, kwargs):
5152
"""
5253
engine = func(*args, **kwargs)
5354
EngineTracer(
54-
tracer, engine.sync_engine, connections_usage, enable_commenter
55+
tracer,
56+
engine.sync_engine,
57+
connections_usage,
58+
enable_commenter,
59+
commenter_options,
5560
)
5661
return engine
5762

5863
return _wrap_create_async_engine_internal
5964

6065

61-
def _wrap_create_engine(tracer, connections_usage, enable_commenter=False):
66+
def _wrap_create_engine(
67+
tracer, connections_usage, enable_commenter=False, commenter_options=None
68+
):
6269
def _wrap_create_engine_internal(func, _module, args, kwargs):
6370
"""Trace the SQLAlchemy engine, creating an `EngineTracer`
6471
object that will listen to SQLAlchemy events.
6572
"""
6673
engine = func(*args, **kwargs)
67-
EngineTracer(tracer, engine, connections_usage, enable_commenter)
74+
EngineTracer(
75+
tracer,
76+
engine,
77+
connections_usage,
78+
enable_commenter,
79+
commenter_options,
80+
)
6881
return engine
6982

7083
return _wrap_create_engine_internal
@@ -99,11 +112,11 @@ def __init__(
99112
commenter_options=None,
100113
):
101114
self.tracer = tracer
102-
self.engine = engine
103115
self.connections_usage = connections_usage
104116
self.vendor = _normalize_vendor(engine.name)
105117
self.enable_commenter = enable_commenter
106118
self.commenter_options = commenter_options if commenter_options else {}
119+
self._engine_attrs = _get_attributes_from_engine(engine)
107120
self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
108121

109122
self._register_event_listener(
@@ -118,23 +131,11 @@ def __init__(
118131
self._register_event_listener(engine, "checkin", self._pool_checkin)
119132
self._register_event_listener(engine, "checkout", self._pool_checkout)
120133

121-
def _get_connection_string(self):
122-
drivername = self.engine.url.drivername or ""
123-
host = self.engine.url.host or ""
124-
port = self.engine.url.port or ""
125-
database = self.engine.url.database or ""
126-
return f"{drivername}://{host}:{port}/{database}"
127-
128-
def _get_pool_name(self):
129-
if self.engine.pool.logging_name is not None:
130-
return self.engine.pool.logging_name
131-
return self._get_connection_string()
132-
133134
def _add_idle_to_connection_usage(self, value):
134135
self.connections_usage.add(
135136
value,
136137
attributes={
137-
"pool.name": self._get_pool_name(),
138+
**self._engine_attrs,
138139
"state": "idle",
139140
},
140141
)
@@ -143,7 +144,7 @@ def _add_used_to_connection_usage(self, value):
143144
self.connections_usage.add(
144145
value,
145146
attributes={
146-
"pool.name": self._get_pool_name(),
147+
**self._engine_attrs,
147148
"state": "used",
148149
},
149150
)
@@ -169,12 +170,21 @@ def _pool_checkout(
169170
@classmethod
170171
def _register_event_listener(cls, target, identifier, func, *args, **kw):
171172
listen(target, identifier, func, *args, **kw)
172-
cls._remove_event_listener_params.append((target, identifier, func))
173+
cls._remove_event_listener_params.append(
174+
(weakref.ref(target), identifier, func)
175+
)
173176

174177
@classmethod
175178
def remove_all_event_listeners(cls):
176-
for remove_params in cls._remove_event_listener_params:
177-
remove(*remove_params)
179+
for (
180+
weak_ref_target,
181+
identifier,
182+
func,
183+
) in cls._remove_event_listener_params:
184+
# Remove an event listener only if saved weak reference points to an object
185+
# which has not been garbage collected
186+
if weak_ref_target() is not None:
187+
remove(weak_ref_target(), identifier, func)
178188
cls._remove_event_listener_params.clear()
179189

180190
def _operation_name(self, db_name, statement):
@@ -300,3 +310,22 @@ def _get_attributes_from_cursor(vendor, cursor, attrs):
300310
if info.port:
301311
attrs[SpanAttributes.NET_PEER_PORT] = int(info.port)
302312
return attrs
313+
314+
315+
def _get_connection_string(engine):
316+
drivername = engine.url.drivername or ""
317+
host = engine.url.host or ""
318+
port = engine.url.port or ""
319+
database = engine.url.database or ""
320+
return f"{drivername}://{host}:{port}/{database}"
321+
322+
323+
def _get_attributes_from_engine(engine):
324+
"""Set metadata attributes of the database engine"""
325+
attrs = {}
326+
327+
attrs["pool.name"] = getattr(
328+
getattr(engine, "pool", None), "logging_name", None
329+
) or _get_connection_string(engine)
330+
331+
return attrs

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

+120
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import asyncio
15+
import logging
1516
from unittest import mock
1617

1718
import pytest
@@ -176,6 +177,43 @@ def test_create_engine_wrapper(self):
176177
"opentelemetry.instrumentation.sqlalchemy",
177178
)
178179

180+
def test_create_engine_wrapper_enable_commenter(self):
181+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
182+
SQLAlchemyInstrumentor().instrument(
183+
enable_commenter=True,
184+
commenter_options={"db_framework": False},
185+
)
186+
from sqlalchemy import create_engine # pylint: disable-all
187+
188+
engine = create_engine("sqlite:///:memory:")
189+
cnx = engine.connect()
190+
cnx.execute("SELECT 1;").fetchall()
191+
# sqlcommenter
192+
self.assertRegex(
193+
self.caplog.records[-2].getMessage(),
194+
r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
195+
)
196+
197+
def test_create_engine_wrapper_enable_commenter_otel_values_false(self):
198+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
199+
SQLAlchemyInstrumentor().instrument(
200+
enable_commenter=True,
201+
commenter_options={
202+
"db_framework": False,
203+
"opentelemetry_values": False,
204+
},
205+
)
206+
from sqlalchemy import create_engine # pylint: disable-all
207+
208+
engine = create_engine("sqlite:///:memory:")
209+
cnx = engine.connect()
210+
cnx.execute("SELECT 1;").fetchall()
211+
# sqlcommenter
212+
self.assertRegex(
213+
self.caplog.records[-2].getMessage(),
214+
r"SELECT 1 /\*db_driver='(.*)'\*/;",
215+
)
216+
179217
def test_custom_tracer_provider(self):
180218
provider = TracerProvider(
181219
resource=Resource.create(
@@ -242,6 +280,65 @@ async def run():
242280

243281
asyncio.get_event_loop().run_until_complete(run())
244282

283+
@pytest.mark.skipif(
284+
not sqlalchemy.__version__.startswith("1.4"),
285+
reason="only run async tests for 1.4",
286+
)
287+
def test_create_async_engine_wrapper_enable_commenter(self):
288+
async def run():
289+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
290+
SQLAlchemyInstrumentor().instrument(
291+
enable_commenter=True,
292+
commenter_options={
293+
"db_framework": False,
294+
},
295+
)
296+
from sqlalchemy.ext.asyncio import ( # pylint: disable-all
297+
create_async_engine,
298+
)
299+
300+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
301+
async with engine.connect() as cnx:
302+
await cnx.execute(sqlalchemy.text("SELECT 1;"))
303+
# sqlcommenter
304+
self.assertRegex(
305+
self.caplog.records[1].getMessage(),
306+
r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
307+
)
308+
309+
asyncio.get_event_loop().run_until_complete(run())
310+
311+
@pytest.mark.skipif(
312+
not sqlalchemy.__version__.startswith("1.4"),
313+
reason="only run async tests for 1.4",
314+
)
315+
def test_create_async_engine_wrapper_enable_commenter_otel_values_false(
316+
self,
317+
):
318+
async def run():
319+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
320+
SQLAlchemyInstrumentor().instrument(
321+
enable_commenter=True,
322+
commenter_options={
323+
"db_framework": False,
324+
"opentelemetry_values": False,
325+
},
326+
)
327+
from sqlalchemy.ext.asyncio import ( # pylint: disable-all
328+
create_async_engine,
329+
)
330+
331+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
332+
async with engine.connect() as cnx:
333+
await cnx.execute(sqlalchemy.text("SELECT 1;"))
334+
# sqlcommenter
335+
self.assertRegex(
336+
self.caplog.records[1].getMessage(),
337+
r"SELECT 1 /\*db_driver='(.*)'\*/;",
338+
)
339+
340+
asyncio.get_event_loop().run_until_complete(run())
341+
245342
def test_uninstrument(self):
246343
engine = create_engine("sqlite:///:memory:")
247344
SQLAlchemyInstrumentor().instrument(
@@ -307,3 +404,26 @@ def test_no_op_tracer_provider(self):
307404
cnx.execute("SELECT 1 + 1;").fetchall()
308405
spans = self.memory_exporter.get_finished_spans()
309406
self.assertEqual(len(spans), 0)
407+
408+
def test_no_memory_leakage_if_engine_diposed(self):
409+
SQLAlchemyInstrumentor().instrument()
410+
import gc
411+
import weakref
412+
413+
from sqlalchemy import create_engine
414+
415+
callback = mock.Mock()
416+
417+
def make_shortlived_engine():
418+
engine = create_engine("sqlite:///:memory:")
419+
# Callback will be called if engine is deallocated during garbage
420+
# collection
421+
weakref.finalize(engine, callback)
422+
with engine.connect() as conn:
423+
conn.execute("SELECT 1 + 1;").fetchall()
424+
425+
for _ in range(0, 5):
426+
make_shortlived_engine()
427+
428+
gc.collect()
429+
assert callback.call_count == 5

0 commit comments

Comments
 (0)