Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Instrumentation breaks psycopg2 when registering types.
**Describe your environment** 
PostgreSQL supports JSON and JSONB types of DB columns. psycopg2 supports registering custom typecasters for these types. Most of the registration functions accept a connection or a cursor instance. The library crashes if anything else is passed in place of a connection or a cursor. 

Most higher level frameworks and ORMs support these features out of the box meaning they crash as well. For example, django projects using psycopg2 driver will crash. 

**Steps to reproduce**

Running the following program
```python
import psycopg2.extras
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor

trace.set_tracer_provider(TracerProvider())
Psycopg2Instrumentor().instrument()

cnx = psycopg2.connect(host='localhost', port='5432', dbname='postgres', password='1234', user='postgres')
psycopg2.extras.register_default_jsonb(conn_or_curs=cnx, loads=lambda x: x)
```

results in the following exception:

```
❯ python main.py
Traceback (most recent call last):
  File "main.py", line 10, in <module>
    psycopg2.extras.register_default_jsonb(conn_or_curs=cnx, loads=lambda x: x)
  File "/Users/olone/playground/otel-psycopg2/venv/lib/python3.7/site-packages/psycopg2/_json.py", line 156, in register_default_jsonb
    loads=loads, oid=JSONB_OID, array_oid=JSONBARRAY_OID, name='jsonb')
  File "/Users/olone/playground/otel-psycopg2/venv/lib/python3.7/site-packages/psycopg2/_json.py", line 125, in register_json
    register_type(JSON, not globally and conn_or_curs or None)
TypeError: argument 2 must be a connection, cursor or None
```

**What is the expected behavior?**
For the instrumentation to not crash psycopg2 ever.

**What is the actual behavior?**
It crashes when psycopg2 enforces connection or cursor types.

**Additional context**
This is happening because instrumentation wraps connection factories and returns wrapt.ObjectProxy instances that wrap connection or cursor objects. These objects fail the type checks psycopg2 runs internally (probably in C code).

Here: https://github.com/open-telemetry/opentelemetry-python/blob/6019a91980ec84bbf969b0d82d44483c93f3ea4c/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py#L287-L304
And here: https://github.com/open-telemetry/opentelemetry-python/blob/6019a91980ec84bbf969b0d82d44483c93f3ea4c/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py#L361-L389

The solution might be to change how DBAPI instrumentation works and patch actual connection and cursor classes instead of wrap with object proxies. 


Discussion:
On a related note, we might want to reevaluate if we should be wrapping objects with different types or patching classes in place as a lot more libraries may rely on such runtime type checking. In general, instrumentation should not change the behavior of instrumented libraries in any way. I think swapping out types/implementations probably counts as changing behavior as `isinstance/issubclass` checks will fail if we do that. 
Yes, using `JSON`/`JSONB` data types raises `Invalid type Composed for attribute value. Expected one of ['bool', 'str', 'int', 'float'] or a sequence of those types`, even with the build in `extras.JSON` in psycopg2.
Unless I've misunderstood, is it possible we did not fully port the ddtrace reference?

For example:

https://github.com/open-telemetry/opentelemetry-python-contrib/blob/5aee3ce32e418e1ce2ae46e303d1ca0630f3f836/reference/ddtrace/contrib/psycopg/patch.py#L127-L130

https://github.com/open-telemetry/opentelemetry-python-contrib/blob/5aee3ce32e418e1ce2ae46e303d1ca0630f3f836/reference/ddtrace/contrib/psycopg/patch.py#L140-L143

https://github.com/open-telemetry/opentelemetry-python-contrib/blob/5aee3ce32e418e1ce2ae46e303d1ca0630f3f836/reference/ddtrace/contrib/psycopg/patch.py#L162-L165

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py
--- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py
@@ -4,8 +4,8 @@
 from aiopg.utils import _ContextManager, _PoolAcquireContextManager
 
 from opentelemetry.instrumentation.dbapi import (
+    CursorTracer,
     DatabaseApiIntegration,
-    TracedCursor,
 )
 from opentelemetry.trace import SpanKind
 from opentelemetry.trace.status import Status, StatusCode
@@ -94,25 +94,29 @@ async def _acquire(self):
     return TracedPoolProxy(pool, *args, **kwargs)
 
 
-class AsyncTracedCursor(TracedCursor):
+class AsyncCursorTracer(CursorTracer):
     async def traced_execution(
         self,
+        cursor,
         query_method: typing.Callable[..., typing.Any],
         *args: typing.Tuple[typing.Any, typing.Any],
         **kwargs: typing.Dict[typing.Any, typing.Any]
     ):
         name = ""
-        if len(args) > 0 and args[0]:
-            name = args[0]
-        elif self._db_api_integration.database:
-            name = self._db_api_integration.database
-        else:
-            name = self._db_api_integration.name
+        if args:
+            name = self.get_operation_name(cursor, args)
+
+        if not name:
+            name = (
+                self._db_api_integration.database
+                if self._db_api_integration.database
+                else self._db_api_integration.name
+            )
 
         with self._db_api_integration.get_tracer().start_as_current_span(
             name, kind=SpanKind.CLIENT
         ) as span:
-            self._populate_span(span, *args)
+            self._populate_span(span, cursor, *args)
             try:
                 result = await query_method(*args, **kwargs)
                 return result
@@ -123,10 +127,10 @@ async def traced_execution(
 
 
 def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs):
-    _traced_cursor = AsyncTracedCursor(db_api_integration)
+    _traced_cursor = AsyncCursorTracer(db_api_integration)
 
     # pylint: disable=abstract-method
-    class AsyncTracedCursorProxy(AsyncProxyObject):
+    class AsyncCursorTracerProxy(AsyncProxyObject):
 
         # pylint: disable=unused-argument
         def __init__(self, cursor, *args, **kwargs):
@@ -134,20 +138,20 @@ def __init__(self, cursor, *args, **kwargs):
 
         async def execute(self, *args, **kwargs):
             result = await _traced_cursor.traced_execution(
-                self.__wrapped__.execute, *args, **kwargs
+                self, self.__wrapped__.execute, *args, **kwargs
             )
             return result
 
         async def executemany(self, *args, **kwargs):
             result = await _traced_cursor.traced_execution(
-                self.__wrapped__.executemany, *args, **kwargs
+                self, self.__wrapped__.executemany, *args, **kwargs
             )
             return result
 
         async def callproc(self, *args, **kwargs):
             result = await _traced_cursor.traced_execution(
-                self.__wrapped__.callproc, *args, **kwargs
+                self, self.__wrapped__.callproc, *args, **kwargs
             )
             return result
 
-    return AsyncTracedCursorProxy(cursor, *args, **kwargs)
+    return AsyncCursorTracerProxy(cursor, *args, **kwargs)
diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py
@@ -60,6 +60,7 @@ def trace_integration(
     connection_attributes: typing.Dict = None,
     tracer_provider: typing.Optional[TracerProvider] = None,
     capture_parameters: bool = False,
+    db_api_integration_factory=None,
 ):
     """Integrate with DB API library.
     https://www.python.org/dev/peps/pep-0249/
@@ -86,6 +87,7 @@ def trace_integration(
         version=__version__,
         tracer_provider=tracer_provider,
         capture_parameters=capture_parameters,
+        db_api_integration_factory=db_api_integration_factory,
     )
 
 
@@ -99,6 +101,7 @@ def wrap_connect(
     version: str = "",
     tracer_provider: typing.Optional[TracerProvider] = None,
     capture_parameters: bool = False,
+    db_api_integration_factory=None,
 ):
     """Integrate with DB API library.
     https://www.python.org/dev/peps/pep-0249/
@@ -115,6 +118,9 @@ def wrap_connect(
         capture_parameters: Configure if db.statement.parameters should be captured.
 
     """
+    db_api_integration_factory = (
+        db_api_integration_factory or DatabaseApiIntegration
+    )
 
     # pylint: disable=unused-argument
     def wrap_connect_(
@@ -123,7 +129,7 @@ def wrap_connect_(
         args: typing.Tuple[typing.Any, typing.Any],
         kwargs: typing.Dict[typing.Any, typing.Any],
     ):
-        db_integration = DatabaseApiIntegration(
+        db_integration = db_api_integration_factory(
             name,
             database_component,
             database_type=database_type,
@@ -314,16 +320,19 @@ def __exit__(self, *args, **kwargs):
     return TracedConnectionProxy(connection, *args, **kwargs)
 
 
-class TracedCursor:
+class CursorTracer:
     def __init__(self, db_api_integration: DatabaseApiIntegration):
         self._db_api_integration = db_api_integration
 
     def _populate_span(
-        self, span: trace_api.Span, *args: typing.Tuple[typing.Any, typing.Any]
+        self,
+        span: trace_api.Span,
+        cursor,
+        *args: typing.Tuple[typing.Any, typing.Any]
     ):
         if not span.is_recording():
             return
-        statement = args[0] if args else ""
+        statement = self.get_statement(cursor, args)
         span.set_attribute(
             "component", self._db_api_integration.database_component
         )
@@ -342,24 +351,38 @@ def _populate_span(
         if self._db_api_integration.capture_parameters and len(args) > 1:
             span.set_attribute("db.statement.parameters", str(args[1]))
 
+    def get_operation_name(self, cursor, args):  # pylint: disable=no-self-use
+        if args and isinstance(args[0], str):
+            return args[0].split()[0]
+        return ""
+
+    def get_statement(self, cursor, args):  # pylint: disable=no-self-use
+        if not args:
+            return ""
+        statement = args[0]
+        if isinstance(statement, bytes):
+            return statement.decode("utf8", "replace")
+        return statement
+
     def traced_execution(
         self,
+        cursor,
         query_method: typing.Callable[..., typing.Any],
         *args: typing.Tuple[typing.Any, typing.Any],
         **kwargs: typing.Dict[typing.Any, typing.Any]
     ):
-        name = ""
-        if args:
-            name = args[0]
-        elif self._db_api_integration.database:
-            name = self._db_api_integration.database
-        else:
-            name = self._db_api_integration.name
+        name = self.get_operation_name(cursor, args)
+        if not name:
+            name = (
+                self._db_api_integration.database
+                if self._db_api_integration.database
+                else self._db_api_integration.name
+            )
 
         with self._db_api_integration.get_tracer().start_as_current_span(
             name, kind=SpanKind.CLIENT
         ) as span:
-            self._populate_span(span, *args)
+            self._populate_span(span, cursor, *args)
             try:
                 result = query_method(*args, **kwargs)
                 return result
@@ -370,7 +393,7 @@ def traced_execution(
 
 
 def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs):
-    _traced_cursor = TracedCursor(db_api_integration)
+    _cursor_tracer = CursorTracer(db_api_integration)
 
     # pylint: disable=abstract-method
     class TracedCursorProxy(wrapt.ObjectProxy):
@@ -380,18 +403,18 @@ def __init__(self, cursor, *args, **kwargs):
             wrapt.ObjectProxy.__init__(self, cursor)
 
         def execute(self, *args, **kwargs):
-            return _traced_cursor.traced_execution(
-                self.__wrapped__.execute, *args, **kwargs
+            return _cursor_tracer.traced_execution(
+                self.__wrapped__, self.__wrapped__.execute, *args, **kwargs
             )
 
         def executemany(self, *args, **kwargs):
-            return _traced_cursor.traced_execution(
-                self.__wrapped__.executemany, *args, **kwargs
+            return _cursor_tracer.traced_execution(
+                self.__wrapped__, self.__wrapped__.executemany, *args, **kwargs
             )
 
         def callproc(self, *args, **kwargs):
-            return _traced_cursor.traced_execution(
-                self.__wrapped__.callproc, *args, **kwargs
+            return _cursor_tracer.traced_execution(
+                self.__wrapped__, self.__wrapped__.callproc, *args, **kwargs
             )
 
         def __enter__(self):
diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py
@@ -39,12 +39,19 @@
 ---
 """
 
+import typing
+
 import psycopg2
+from psycopg2.extensions import (
+    cursor as pg_cursor,  # pylint: disable=no-name-in-module
+)
+from psycopg2.sql import Composed  # pylint: disable=no-name-in-module
 
 from opentelemetry.instrumentation import dbapi
 from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
 from opentelemetry.instrumentation.psycopg2.version import __version__
-from opentelemetry.trace import get_tracer
+
+_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory"
 
 
 class Psycopg2Instrumentor(BaseInstrumentor):
@@ -62,7 +69,6 @@ def _instrument(self, **kwargs):
         """Integrate with PostgreSQL Psycopg library.
            Psycopg: http://initd.org/psycopg/
         """
-
         tracer_provider = kwargs.get("tracer_provider")
 
         dbapi.wrap_connect(
@@ -74,39 +80,101 @@ def _instrument(self, **kwargs):
             self._CONNECTION_ATTRIBUTES,
             version=__version__,
             tracer_provider=tracer_provider,
+            db_api_integration_factory=DatabaseApiIntegration,
         )
 
     def _uninstrument(self, **kwargs):
         """"Disable Psycopg2 instrumentation"""
         dbapi.unwrap_connect(psycopg2, "connect")
 
-    # pylint:disable=no-self-use
-    def instrument_connection(self, connection):
-        """Enable instrumentation in a Psycopg2 connection.
-
-        Args:
-            connection: The connection to instrument.
+    # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+    def instrument_connection(self, connection):  # pylint: disable=no-self-use
+        setattr(
+            connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory
+        )
+        connection.cursor_factory = _new_cursor_factory()
+        return connection
+
+    # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+    def uninstrument_connection(
+        self, connection
+    ):  # pylint: disable=no-self-use
+        connection.cursor_factory = getattr(
+            connection, _OTEL_CURSOR_FACTORY_KEY, None
+        )
+        return connection
+
+
+# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+class DatabaseApiIntegration(dbapi.DatabaseApiIntegration):
+    def wrapped_connection(
+        self,
+        connect_method: typing.Callable[..., typing.Any],
+        args: typing.Tuple[typing.Any, typing.Any],
+        kwargs: typing.Dict[typing.Any, typing.Any],
+    ):
+        """Add object proxy to connection object."""
+        base_cursor_factory = kwargs.pop("cursor_factory", None)
+        new_factory_kwargs = {"db_api": self}
+        if base_cursor_factory:
+            new_factory_kwargs["base_factory"] = base_cursor_factory
+        kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs)
+        connection = connect_method(*args, **kwargs)
+        self.get_connection_attributes(connection)
+        return connection
+
+
+class CursorTracer(dbapi.CursorTracer):
+    def get_operation_name(self, cursor, args):
+        if not args:
+            return ""
+
+        statement = args[0]
+        if isinstance(statement, Composed):
+            statement = statement.as_string(cursor)
+
+        if isinstance(statement, str):
+            return statement.split()[0]
+
+        return ""
+
+    def get_statement(self, cursor, args):
+        if not args:
+            return ""
+
+        statement = args[0]
+        if isinstance(statement, Composed):
+            statement = statement.as_string(cursor)
+        return statement
+
+
+def _new_cursor_factory(db_api=None, base_factory=None):
+    if not db_api:
+        db_api = DatabaseApiIntegration(
+            __name__,
+            Psycopg2Instrumentor._DATABASE_COMPONENT,
+            database_type=Psycopg2Instrumentor._DATABASE_TYPE,
+            connection_attributes=Psycopg2Instrumentor._CONNECTION_ATTRIBUTES,
+            version=__version__,
+        )
 
-        Returns:
-            An instrumented connection.
-        """
-        tracer = get_tracer(__name__, __version__)
+    base_factory = base_factory or pg_cursor
+    _cursor_tracer = CursorTracer(db_api)
 
-        return dbapi.instrument_connection(
-            tracer,
-            connection,
-            self._DATABASE_COMPONENT,
-            self._DATABASE_TYPE,
-            self._CONNECTION_ATTRIBUTES,
-        )
+    class TracedCursorFactory(base_factory):
+        def execute(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().execute, *args, **kwargs
+            )
 
-    def uninstrument_connection(self, connection):
-        """Disable instrumentation in a Psycopg2 connection.
+        def executemany(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().executemany, *args, **kwargs
+            )
 
-        Args:
-            connection: The connection to uninstrument.
+        def callproc(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().callproc, *args, **kwargs
+            )
 
-        Returns:
-            An uninstrumented connection.
-        """
-        return dbapi.uninstrument_connection(connection)
+    return TracedCursorFactory


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py
--- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py
@@ -256,7 +256,7 @@ def test_span_succeeded(self):
         spans_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans_list), 1)
         span = spans_list[0]
-        self.assertEqual(span.name, "Test query")
+        self.assertEqual(span.name, "Test")
         self.assertIs(span.kind, trace_api.SpanKind.CLIENT)
 
         self.assertEqual(span.attributes["component"], "testcomponent")
diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py
--- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py
@@ -50,7 +50,7 @@ def test_span_succeeded(self):
         spans_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans_list), 1)
         span = spans_list[0]
-        self.assertEqual(span.name, "Test query")
+        self.assertEqual(span.name, "Test")
         self.assertIs(span.kind, trace_api.SpanKind.CLIENT)
 
         self.assertEqual(span.attributes["component"], "testcomponent")
@@ -65,6 +65,27 @@ def test_span_succeeded(self):
             span.status.status_code, trace_api.status.StatusCode.UNSET
         )
 
+    def test_span_name(self):
+        db_integration = dbapi.DatabaseApiIntegration(
+            self.tracer, "testcomponent", "testtype", {}
+        )
+        mock_connection = db_integration.wrapped_connection(
+            mock_connect, {}, {}
+        )
+        cursor = mock_connection.cursor()
+        cursor.execute("Test query", ("param1Value", False))
+        cursor.execute(
+            """multi
+        line
+        query"""
+        )
+        cursor.execute("tab\tseparated query")
+        spans_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans_list), 3)
+        self.assertEqual(spans_list[0].name, "Test")
+        self.assertEqual(spans_list[1].name, "multi")
+        self.assertEqual(spans_list[2].name, "tab")
+
     def test_span_succeeded_with_capture_of_statement_parameters(self):
         connection_props = {
             "database": "testdatabase",
@@ -93,7 +114,7 @@ def test_span_succeeded_with_capture_of_statement_parameters(self):
         spans_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans_list), 1)
         span = spans_list[0]
-        self.assertEqual(span.name, "Test query")
+        self.assertEqual(span.name, "Test")
         self.assertIs(span.kind, trace_api.SpanKind.CLIENT)
 
         self.assertEqual(span.attributes["component"], "testcomponent")
diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py
--- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import types
 from unittest import mock
 
 import psycopg2
@@ -22,15 +23,69 @@
 from opentelemetry.test.test_base import TestBase
 
 
+class MockCursor:
+
+    execute = mock.MagicMock(spec=types.MethodType)
+    execute.__name__ = "execute"
+
+    executemany = mock.MagicMock(spec=types.MethodType)
+    executemany.__name__ = "executemany"
+
+    callproc = mock.MagicMock(spec=types.MethodType)
+    callproc.__name__ = "callproc"
+
+    rowcount = "SomeRowCount"
+
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        return self
+
+
+class MockConnection:
+
+    commit = mock.MagicMock(spec=types.MethodType)
+    commit.__name__ = "commit"
+
+    rollback = mock.MagicMock(spec=types.MethodType)
+    rollback.__name__ = "rollback"
+
+    def __init__(self, *args, **kwargs):
+        self.cursor_factory = kwargs.pop("cursor_factory", None)
+
+    def cursor(self):
+        if self.cursor_factory:
+            return self.cursor_factory(self)
+        return MockCursor()
+
+    def get_dsn_parameters(self):  # pylint: disable=no-self-use
+        return dict(dbname="test")
+
+
 class TestPostgresqlIntegration(TestBase):
+    def setUp(self):
+        self.cursor_mock = mock.patch(
+            "opentelemetry.instrumentation.psycopg2.pg_cursor", MockCursor
+        )
+        self.connection_mock = mock.patch("psycopg2.connect", MockConnection)
+
+        self.cursor_mock.start()
+        self.connection_mock.start()
+
     def tearDown(self):
         super().tearDown()
+        self.memory_exporter.clear()
+        self.cursor_mock.stop()
+        self.connection_mock.stop()
         with self.disable_logging():
             Psycopg2Instrumentor().uninstrument()
 
-    @mock.patch("psycopg2.connect")
     # pylint: disable=unused-argument
-    def test_instrumentor(self, mock_connect):
+    def test_instrumentor(self):
         Psycopg2Instrumentor().instrument()
 
         cnx = psycopg2.connect(database="test")
@@ -60,9 +115,8 @@ def test_instrumentor(self, mock_connect):
         spans_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans_list), 1)
 
-    @mock.patch("psycopg2.connect")
     # pylint: disable=unused-argument
-    def test_not_recording(self, mock_connect):
+    def test_not_recording(self):
         mock_tracer = mock.Mock()
         mock_span = mock.Mock()
         mock_span.is_recording.return_value = False
@@ -83,9 +137,8 @@ def test_not_recording(self, mock_connect):
 
         Psycopg2Instrumentor().uninstrument()
 
-    @mock.patch("psycopg2.connect")
     # pylint: disable=unused-argument
-    def test_custom_tracer_provider(self, mock_connect):
+    def test_custom_tracer_provider(self):
         resource = resources.Resource.create({})
         result = self.create_tracer_provider(resource=resource)
         tracer_provider, exporter = result
@@ -103,9 +156,8 @@ def test_custom_tracer_provider(self, mock_connect):
 
         self.assertIs(span.resource, resource)
 
-    @mock.patch("psycopg2.connect")
     # pylint: disable=unused-argument
-    def test_instrument_connection(self, mock_connect):
+    def test_instrument_connection(self):
         cnx = psycopg2.connect(database="test")
         query = "SELECT * FROM test"
         cursor = cnx.cursor()
@@ -121,9 +173,8 @@ def test_instrument_connection(self, mock_connect):
         spans_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans_list), 1)
 
-    @mock.patch("psycopg2.connect")
     # pylint: disable=unused-argument
-    def test_uninstrument_connection(self, mock_connect):
+    def test_uninstrument_connection(self):
         Psycopg2Instrumentor().instrument()
         cnx = psycopg2.connect(database="test")
         query = "SELECT * FROM test"
diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py
--- a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py
@@ -60,7 +60,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id integer)"
         with self._tracer.start_as_current_span("rootSpan"):
             self._cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_executemany(self):
         """Should create a child span for executemany"""
@@ -68,7 +68,7 @@ def test_executemany(self):
         with self._tracer.start_as_current_span("rootSpan"):
             data = [("1",), ("2",), ("3",)]
             self._cursor.executemany(stmt, data)
-        self.validate_spans(stmt)
+        self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""
diff --git a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py
--- a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py
@@ -81,7 +81,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
         with self._tracer.start_as_current_span("rootSpan"):
             self._cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_execute_with_connection_context_manager(self):
         """Should create a child span for execute with connection context"""
@@ -90,7 +90,7 @@ def test_execute_with_connection_context_manager(self):
             with self._connection as conn:
                 cursor = conn.cursor()
                 cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_execute_with_cursor_context_manager(self):
         """Should create a child span for execute with cursor context"""
@@ -98,7 +98,7 @@ def test_execute_with_cursor_context_manager(self):
         with self._tracer.start_as_current_span("rootSpan"):
             with self._connection.cursor() as cursor:
                 cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_executemany(self):
         """Should create a child span for executemany"""
@@ -106,7 +106,7 @@ def test_executemany(self):
         with self._tracer.start_as_current_span("rootSpan"):
             data = (("1",), ("2",), ("3",))
             self._cursor.executemany(stmt, data)
-        self.validate_spans(stmt)
+        self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""
diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py
--- a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py
@@ -89,7 +89,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id integer)"
         with self._tracer.start_as_current_span("rootSpan"):
             async_call(self._cursor.execute(stmt))
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_executemany(self):
         """Should create a child span for executemany"""
@@ -98,7 +98,7 @@ def test_executemany(self):
             with self._tracer.start_as_current_span("rootSpan"):
                 data = (("1",), ("2",), ("3",))
                 async_call(self._cursor.executemany(stmt, data))
-            self.validate_spans(stmt)
+            self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""
@@ -167,7 +167,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id integer)"
         with self._tracer.start_as_current_span("rootSpan"):
             async_call(self._cursor.execute(stmt))
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_executemany(self):
         """Should create a child span for executemany"""
@@ -176,7 +176,7 @@ def test_executemany(self):
             with self._tracer.start_as_current_span("rootSpan"):
                 data = (("1",), ("2",), ("3",))
                 async_call(self._cursor.executemany(stmt, data))
-            self.validate_spans(stmt)
+            self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""
diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py
--- a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py
@@ -15,6 +15,7 @@
 import os
 
 import psycopg2
+from psycopg2 import sql
 
 from opentelemetry import trace as trace_api
 from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
@@ -81,7 +82,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id integer)"
         with self._tracer.start_as_current_span("rootSpan"):
             self._cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_execute_with_connection_context_manager(self):
         """Should create a child span for execute with connection context"""
@@ -90,7 +91,7 @@ def test_execute_with_connection_context_manager(self):
             with self._connection as conn:
                 cursor = conn.cursor()
                 cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_execute_with_cursor_context_manager(self):
         """Should create a child span for execute with cursor context"""
@@ -98,7 +99,7 @@ def test_execute_with_cursor_context_manager(self):
         with self._tracer.start_as_current_span("rootSpan"):
             with self._connection.cursor() as cursor:
                 cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
         self.assertTrue(cursor.closed)
 
     def test_executemany(self):
@@ -107,7 +108,7 @@ def test_executemany(self):
         with self._tracer.start_as_current_span("rootSpan"):
             data = (("1",), ("2",), ("3",))
             self._cursor.executemany(stmt, data)
-        self.validate_spans(stmt)
+        self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""
@@ -116,3 +117,30 @@ def test_callproc(self):
         ):
             self._cursor.callproc("test", ())
             self.validate_spans("test")
+
+    def test_register_types(self):
+        psycopg2.extras.register_default_jsonb(
+            conn_or_curs=self._cursor, loads=lambda x: x
+        )
+
+    def test_composed_queries(self):
+        stmt = "CREATE TABLE IF NOT EXISTS users (id integer, name varchar)"
+        with self._tracer.start_as_current_span("rootSpan"):
+            self._cursor.execute(stmt)
+        self.validate_spans("CREATE")
+
+        self._cursor.execute(
+            sql.SQL("SELECT FROM {table} where {field}='{value}'").format(
+                table=sql.Identifier("users"),
+                field=sql.Identifier("name"),
+                value=sql.Identifier("abc"),
+            )
+        )
+
+        spans = self.memory_exporter.get_finished_spans()
+        span = spans[2]
+        self.assertEqual(span.name, "SELECT")
+        self.assertEqual(
+            span.attributes["db.statement"],
+            'SELECT FROM "users" where "name"=\'"abc"\'',
+        )
diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py
--- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py
@@ -78,7 +78,7 @@ def test_execute(self):
         stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
         with self._tracer.start_as_current_span("rootSpan"):
             self._cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_execute_with_cursor_context_manager(self):
         """Should create a child span for execute with cursor context"""
@@ -86,7 +86,7 @@ def test_execute_with_cursor_context_manager(self):
         with self._tracer.start_as_current_span("rootSpan"):
             with self._connection.cursor() as cursor:
                 cursor.execute(stmt)
-        self.validate_spans(stmt)
+        self.validate_spans("CREATE")
 
     def test_executemany(self):
         """Should create a child span for executemany"""
@@ -94,7 +94,7 @@ def test_executemany(self):
         with self._tracer.start_as_current_span("rootSpan"):
             data = (("1",), ("2",), ("3",))
             self._cursor.executemany(stmt, data)
-        self.validate_spans(stmt)
+        self.validate_spans("INSERT")
 
     def test_callproc(self):
         """Should create a child span for callproc"""