Skip to content

Commit b8a8020

Browse files
antoine-dexrmx
andauthored
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>
1 parent 36aa71b commit b8a8020

File tree

3 files changed

+44
-4
lines changed

3 files changed

+44
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
104104
([#3922](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3922))
105105
- `opentelemetry-instrumentation-urllib3`: fix multiple arguments error
106106
([#4144](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4144))
107+
- `opentelemetry-instrumentation-psycopg`: Fix instrument of typed psycopg sql
108+
([#4078](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4171))
107109
- `opentelemetry-instrumentation-aiohttp-server`: fix HTTP error inconsistencies
108110
([#4175](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4175))
109111

@@ -112,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
112114
- `opentelemetry-instrumentation-logging`: Inject span context attributes into logging LogRecord only if configured to do so
113115
([#4112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4112))
114116
- `opentelemetry-instrumentation-django`: Drop support for Django < 2.0
115-
([#3848](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
117+
([#4083](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
116118

117119
## Version 1.39.0/0.60b0 (2025-12-03)
118120

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
from typing import Any, Callable, Collection, TypeVar
148148

149149
import psycopg # pylint: disable=import-self
150-
from psycopg.sql import Composed # pylint: disable=no-name-in-module
150+
from psycopg.sql import Composable # pylint: disable=no-name-in-module
151151

152152
from opentelemetry.instrumentation import dbapi
153153
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -338,7 +338,7 @@ def get_operation_name(self, cursor: CursorT, args: list[Any]) -> str:
338338
return ""
339339

340340
statement = args[0]
341-
if isinstance(statement, Composed):
341+
if isinstance(statement, Composable):
342342
statement = statement.as_string(cursor)
343343

344344
# `statement` can be empty string. See #2643
@@ -353,7 +353,7 @@ def get_statement(self, cursor: CursorT, args: list[Any]) -> str:
353353
return ""
354354

355355
statement = args[0]
356-
if isinstance(statement, Composed):
356+
if isinstance(statement, Composable):
357357
statement = statement.as_string(cursor)
358358
return statement
359359

instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from unittest import IsolatedAsyncioTestCase, mock
1818

1919
import psycopg
20+
from psycopg.sql import SQL, Composed
2021

2122
import opentelemetry.instrumentation.psycopg
2223
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
@@ -34,6 +35,8 @@ class MockCursor:
3435
callproc = mock.MagicMock(spec=types.MethodType)
3536
callproc.__name__ = "callproc"
3637

38+
connection = None
39+
3740
rowcount = "SomeRowCount"
3841

3942
def __init__(self, *args, **kwargs):
@@ -348,6 +351,41 @@ def test_instrument_connection(self):
348351
spans_list = self.memory_exporter.get_finished_spans()
349352
self.assertEqual(len(spans_list), 1)
350353

354+
def test_instrument_connection_typed_sql_query(self):
355+
cnx = psycopg.connect(database="test")
356+
query = SQL("SELECT * FROM test")
357+
358+
cnx = PsycopgInstrumentor().instrument_connection(cnx)
359+
360+
self.assertTrue(issubclass(cnx.cursor_factory, MockCursor))
361+
362+
cursor = cnx.cursor()
363+
cursor.execute(query)
364+
365+
spans_list = self.memory_exporter.get_finished_spans()
366+
self.assertEqual(len(spans_list), 1)
367+
self.assertEqual(spans_list[0].name, "SELECT")
368+
self.assertEqual(
369+
spans_list[0].attributes["db.statement"], "SELECT * FROM test"
370+
)
371+
372+
def test_instrument_connection_composed_query(self):
373+
cnx = psycopg.connect(database="test")
374+
query: Composed = SQL("SELECT * FROM test").format()
375+
376+
cnx = PsycopgInstrumentor().instrument_connection(cnx)
377+
self.assertTrue(issubclass(cnx.cursor_factory, MockCursor))
378+
379+
cursor = cnx.cursor()
380+
cursor.execute(query)
381+
382+
spans_list = self.memory_exporter.get_finished_spans()
383+
self.assertEqual(len(spans_list), 1)
384+
self.assertEqual(spans_list[0].name, "SELECT")
385+
self.assertEqual(
386+
spans_list[0].attributes["db.statement"], "SELECT * FROM test"
387+
)
388+
351389
# pylint: disable=unused-argument
352390
def test_instrument_connection_with_instrument(self):
353391
cnx = psycopg.connect(database="test")

0 commit comments

Comments
 (0)