fix instrument of typed psycopg sql (#4171)

* fix instrument of typed psycopg sql

The instrumentation is not working when using [typed `SQL`](https://www.psycopg.org/psycopg3/docs/api/sql.html) from psycopg (only when using the `Composed` type, that is returned when the query is formated).

```python
from psycopg.sql import SQL
query = SQL("SELECT * FROM test")
```

This fixes it by checking the `Composable` base class instead of the more restricted `Composed`.

* add changelog

* fix tests for python 3.9

using an identifier requires a real connection, I just replaced it since we only want to test with a composed.

---------

Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
This commit is contained in:
Antoine D
2026-02-10 17:34:24 +01:00
committed by GitHub
parent 36aa71b21c
commit b8a80209d7
3 changed files with 44 additions and 4 deletions

View File

@@ -104,6 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3922](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3922))
- `opentelemetry-instrumentation-urllib3`: fix multiple arguments error
([#4144](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4144))
- `opentelemetry-instrumentation-psycopg`: Fix instrument of typed psycopg sql
([#4078](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4171))
- `opentelemetry-instrumentation-aiohttp-server`: fix HTTP error inconsistencies
([#4175](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4175))
@@ -112,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-logging`: Inject span context attributes into logging LogRecord only if configured to do so
([#4112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4112))
- `opentelemetry-instrumentation-django`: Drop support for Django < 2.0
([#3848](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
([#4083](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
## Version 1.39.0/0.60b0 (2025-12-03)

View File

@@ -147,7 +147,7 @@ import logging
from typing import Any, Callable, Collection, TypeVar
import psycopg # pylint: disable=import-self
from psycopg.sql import Composed # pylint: disable=no-name-in-module
from psycopg.sql import Composable # pylint: disable=no-name-in-module
from opentelemetry.instrumentation import dbapi
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -338,7 +338,7 @@ class CursorTracer(dbapi.CursorTracer):
return ""
statement = args[0]
if isinstance(statement, Composed):
if isinstance(statement, Composable):
statement = statement.as_string(cursor)
# `statement` can be empty string. See #2643
@@ -353,7 +353,7 @@ class CursorTracer(dbapi.CursorTracer):
return ""
statement = args[0]
if isinstance(statement, Composed):
if isinstance(statement, Composable):
statement = statement.as_string(cursor)
return statement

View File

@@ -17,6 +17,7 @@ import types
from unittest import IsolatedAsyncioTestCase, mock
import psycopg
from psycopg.sql import SQL, Composed
import opentelemetry.instrumentation.psycopg
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
@@ -34,6 +35,8 @@ class MockCursor:
callproc = mock.MagicMock(spec=types.MethodType)
callproc.__name__ = "callproc"
connection = None
rowcount = "SomeRowCount"
def __init__(self, *args, **kwargs):
@@ -348,6 +351,41 @@ class TestPostgresqlIntegration(PostgresqlIntegrationTestMixin, TestBase):
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
def test_instrument_connection_typed_sql_query(self):
cnx = psycopg.connect(database="test")
query = SQL("SELECT * FROM test")
cnx = PsycopgInstrumentor().instrument_connection(cnx)
self.assertTrue(issubclass(cnx.cursor_factory, MockCursor))
cursor = cnx.cursor()
cursor.execute(query)
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertEqual(spans_list[0].name, "SELECT")
self.assertEqual(
spans_list[0].attributes["db.statement"], "SELECT * FROM test"
)
def test_instrument_connection_composed_query(self):
cnx = psycopg.connect(database="test")
query: Composed = SQL("SELECT * FROM test").format()
cnx = PsycopgInstrumentor().instrument_connection(cnx)
self.assertTrue(issubclass(cnx.cursor_factory, MockCursor))
cursor = cnx.cursor()
cursor.execute(query)
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
self.assertEqual(spans_list[0].name, "SELECT")
self.assertEqual(
spans_list[0].attributes["db.statement"], "SELECT * FROM test"
)
# pylint: disable=unused-argument
def test_instrument_connection_with_instrument(self):
cnx = psycopg.connect(database="test")