Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix psycopg2 instrument_connection AttributeError #3043

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
69d1ea3
Fix psycopg2 instrument_connection
tammy-baylis-swi Nov 23, 2024
c6181ef
Changelog
tammy-baylis-swi Nov 23, 2024
72d41e5
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 4, 2024
ea8d73d
Fix unit test
tammy-baylis-swi Dec 5, 2024
a0f74ca
Rm psycopg2 wrapt dep
tammy-baylis-swi Dec 5, 2024
a0c9940
Fix docs and comments
tammy-baylis-swi Dec 5, 2024
b591b0a
Add and update tests
tammy-baylis-swi Dec 5, 2024
59fa855
Rm enable_commenter fixes for a different pr
tammy-baylis-swi Dec 5, 2024
dd7e3ea
Revert doc reqs
tammy-baylis-swi Dec 5, 2024
0b3f273
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 5, 2024
40f077b
Update readme
tammy-baylis-swi Dec 6, 2024
5a099d5
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 9, 2024
a7f969d
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 11, 2024
0f7baec
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 19, 2024
a2fa981
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Dec 20, 2024
e8d47bf
Rm instrument_connection is-instrumented check; use dict to store cnx
tammy-baylis-swi Dec 21, 2024
8a316a4
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Jan 2, 2025
b3b3888
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Jan 7, 2025
238746e
Merge branch 'main' into fix-psycopg2-instrument-connection
tammy-baylis-swi Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2816](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2816))
- `opentelemetry-instrumentation-sqlalchemy`: Fix a remaining memory leak in EngineTracer
([#3053](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3053))
- `opentelemetry-instrumentation-psycopg2`: fix AttributeError at `instrument_connection`
([#3043](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3043))

### Breaking changes

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
OpenTelemetry Psycopg Instrumentation
OpenTelemetry Psycopg2 Instrumentation
=====================================

tammy-baylis-swi marked this conversation as resolved.
Show resolved Hide resolved
|pypi|
Expand All @@ -16,6 +16,6 @@ Installation

References
----------
* `OpenTelemetry Psycopg Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/psycopg2/psycopg2.html>`_
* `OpenTelemetry Psycopg2 Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/psycopg2/psycopg2.html>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
# limitations under the License.

"""
The integration with PostgreSQL supports the `Psycopg`_ library, it can be enabled by
The integration with PostgreSQL supports the `Psycopg2`_ library, it can be enabled by
using ``Psycopg2Instrumentor``.

.. _Psycopg: http://initd.org/psycopg/
.. _Psycopg2: https://www.psycopg.org/docs/

SQLCOMMENTER
*****************************************
Expand Down Expand Up @@ -111,13 +111,13 @@
)
from psycopg2.sql import Composed # pylint: disable=no-name-in-module

from opentelemetry import trace as trace_api
from opentelemetry.instrumentation import dbapi
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.psycopg2.package import _instruments
from opentelemetry.instrumentation.psycopg2.version import __version__

_logger = logging.getLogger(__name__)
_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory"


class Psycopg2Instrumentor(BaseInstrumentor):
Expand All @@ -130,12 +130,14 @@ class Psycopg2Instrumentor(BaseInstrumentor):

_DATABASE_SYSTEM = "postgresql"

_otel_cursor_factory = None

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
"""Integrate with PostgreSQL Psycopg library.
Psycopg: http://initd.org/psycopg/
"""Integrate with PostgreSQL Psycopg2 library.
Psycopg2: https://www.psycopg.org/docs/
"""
tracer_provider = kwargs.get("tracer_provider")
enable_sqlcommenter = kwargs.get("enable_commenter", False)
Expand All @@ -158,32 +160,35 @@ def _uninstrument(self, **kwargs):
dbapi.unwrap_connect(psycopg2, "connect")

# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
@staticmethod
def instrument_connection(connection, tracer_provider=None):
if not hasattr(connection, "_is_instrumented_by_opentelemetry"):
tammy-baylis-swi marked this conversation as resolved.
Show resolved Hide resolved
connection._is_instrumented_by_opentelemetry = False

if not connection._is_instrumented_by_opentelemetry:
setattr(
connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory
)
connection.cursor_factory = _new_cursor_factory(
tracer_provider=tracer_provider
)
connection._is_instrumented_by_opentelemetry = True
else:
def instrument_connection(
self,
connection,
tracer_provider: typing.Optional[trace_api.TracerProvider] = None,
):
if self._is_instrumented_by_opentelemetry:
# _instrument (via BaseInstrumentor) or instrument_connection (this)
# was already called
_logger.warning(
"Attempting to instrument Psycopg connection while already instrumented"
"Attempting to instrument Psycopg2 connection while already instrumented"
)
self._is_instrumented_by_opentelemetry = True
return connection

# Save cursor_factory at instrumentor level because
# psycopg2 connection type does not allow arbitrary attrs
self._otel_cursor_factory = connection.cursor_factory
tammy-baylis-swi marked this conversation as resolved.
Show resolved Hide resolved
connection.cursor_factory = _new_cursor_factory(
base_factory=connection.cursor_factory,
tracer_provider=tracer_provider,
)
self._is_instrumented_by_opentelemetry = True

return connection

# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
@staticmethod
def uninstrument_connection(connection):
connection.cursor_factory = getattr(
connection, _OTEL_CURSOR_FACTORY_KEY, None
)

def uninstrument_connection(self, connection):
self._is_instrumented_by_opentelemetry = False
connection.cursor_factory = self._otel_cursor_factory
return connection


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
from unittest import mock

import psycopg2
import pytest

import opentelemetry.instrumentation.psycopg2
from opentelemetry import trace
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
from opentelemetry.instrumentation.psycopg2 import (
Psycopg2Instrumentor,
)
from opentelemetry.sdk import resources
from opentelemetry.test.test_base import TestBase

Expand Down Expand Up @@ -66,6 +69,10 @@ def get_dsn_parameters(self): # pylint: disable=no-self-use


class TestPostgresqlIntegration(TestBase):
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self.caplog = caplog # pylint: disable=attribute-defined-outside-init

def setUp(self):
super().setUp()
self.cursor_mock = mock.patch(
Expand Down Expand Up @@ -190,7 +197,13 @@ def test_instrument_connection(self):
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 0)

cnx = Psycopg2Instrumentor().instrument_connection(cnx)
instrumentor = Psycopg2Instrumentor()
original_cursor_factory = cnx.cursor_factory
cnx = instrumentor.instrument_connection(cnx)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)
self.assertEqual(
instrumentor._otel_cursor_factory, original_cursor_factory
)
cursor = cnx.cursor()
cursor.execute(query)

Expand All @@ -207,8 +220,52 @@ def test_instrument_connection_with_instrument(self):
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 0)

Psycopg2Instrumentor().instrument()
instrumentor = Psycopg2Instrumentor()
instrumentor.instrument()
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)

cnx = psycopg2.connect(database="test")
cnx = Psycopg2Instrumentor().instrument_connection(cnx)
self.assertEqual(
self.caplog.records[0].getMessage(),
"Attempting to instrument Psycopg2 connection while already instrumented",
)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)
# Will not set cursor_factory if already instrumented
self.assertEqual(instrumentor._otel_cursor_factory, None)
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)

def test_instrument_connection_with_instrument_connection(self):
cnx = psycopg2.connect(database="test")
tammy-baylis-swi marked this conversation as resolved.
Show resolved Hide resolved
query = "SELECT * FROM test"
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 0)

cnx = psycopg2.connect(database="test")
original_cursor_factory = cnx.cursor_factory
instrumentor = Psycopg2Instrumentor()
cnx = instrumentor.instrument_connection(cnx)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)
self.assertEqual(
instrumentor._otel_cursor_factory, original_cursor_factory
)

cnx = Psycopg2Instrumentor().instrument_connection(cnx)
self.assertEqual(
self.caplog.records[0].getMessage(),
"Attempting to instrument Psycopg2 connection while already instrumented",
)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)
self.assertEqual(
instrumentor._otel_cursor_factory, original_cursor_factory
)
cursor = cnx.cursor()
cursor.execute(query)

Expand All @@ -217,39 +274,50 @@ def test_instrument_connection_with_instrument(self):

# pylint: disable=unused-argument
def test_uninstrument_connection_with_instrument(self):
Psycopg2Instrumentor().instrument()
instrumentor = Psycopg2Instrumentor()
instrumentor.instrument()
cnx = psycopg2.connect(database="test")
query = "SELECT * FROM test"
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)

cnx = Psycopg2Instrumentor().uninstrument_connection(cnx)
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertFalse(instrumentor._is_instrumented_by_opentelemetry)

# pylint: disable=unused-argument
def test_uninstrument_connection_with_instrument_connection(self):
cnx = psycopg2.connect(database="test")
Psycopg2Instrumentor().instrument_connection(cnx)
original_cursor_factory = cnx.cursor_factory
instrumentor = Psycopg2Instrumentor()
instrumentor.instrument_connection(cnx)
query = "SELECT * FROM test"
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertTrue(instrumentor._is_instrumented_by_opentelemetry)
self.assertEqual(
instrumentor._otel_cursor_factory, original_cursor_factory
)

cnx = Psycopg2Instrumentor().uninstrument_connection(cnx)
cursor = cnx.cursor()
cursor.execute(query)

spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertFalse(instrumentor._is_instrumented_by_opentelemetry)
self.assertEqual(instrumentor._otel_cursor_factory, None)

@mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect")
def test_sqlcommenter_enabled(self, event_mocked):
Expand Down
Loading