Describe your environment
- OS: Linux (Ubuntu 24.04.4 LTS)
- Python version: 3.14.0
- Package versions:
opentelemetry-api 1.41.1
opentelemetry-sdk 1.41.1
opentelemetry-instrumentation-psycopg 0.62b1
opentelemetry-instrumentation-dbapi 0.62b1
psycopg 3.3.3
psycopg-binary 3.3.3
psycopg-pool 3.3.0
What happened?
psycopg 3.3+ on Python 3.14 accepts a PEP 750 t-string (t"...", a string.templatelib.Template) directly in cursor.execute / await cursor.execute. The driver renders the Template safely server-side.
opentelemetry-instrumentation-dbapi (and therefore opentelemetry-instrumentation-psycopg, which delegates to it) has no path for Template objects. Three call sites in CursorTracer assume args[0] is a string-like object, producing two distinct symptoms depending on configuration.
Symptom 1 — query crashes when enable_commenter=True
With PsycopgInstrumentor().instrument(enable_commenter=True), the instrumented execute path goes through CursorTracer._update_args_with_added_sql_comment, which mutates args[0] to a plain string before forwarding to the real execute:
# opentelemetry/instrumentation/dbapi/__init__.py:673
def _update_args_with_added_sql_comment(self, args, cursor) -> tuple:
try:
args_list = list(args)
self._capture_mysql_version(cursor)
commenter_data = self._get_commenter_data()
# Convert sql statement to string, handling psycopg2.sql.Composable object
if hasattr(args_list[0], "as_string"):
args_list[0] = args_list[0].as_string(cursor.connection)
args_list[0] = str(args_list[0]) # <-- destroys Template here
statement = _add_sql_comment(args_list[0], **commenter_data)
args_list[0] = statement
...
For a Template, hasattr(template, "as_string") is False, so the code falls through to str(template). That returns the object's repr, e.g.:
Template(strings=('\n SELECT ... WHERE ', '\n ORDER BY ...\n '), interpolations=(Interpolation(...),))
This repr-string is then passed to psycopg's real execute, which sends it verbatim to PostgreSQL, producing:
psycopg.errors.SyntaxError: syntax error at or near "Template"
LINE 1: Template(strings=('\n SELECT\n concat(\n ...
^
Symptom 2 — noisy warnings and degraded spans with enable_commenter=False (default)
With the default PsycopgInstrumentor().instrument(), the commenter path is skipped, so the query reaches psycopg as a Template and executes correctly. However:
-
CursorTracer._populate_span calls span.set_attribute(DB_STATEMENT, statement) where statement is the Template:
# opentelemetry/instrumentation/dbapi/__init__.py:701
statement = self.get_statement(cursor, args)
...
span.set_attribute(DB_STATEMENT, statement)
get_statement (line 723) returns args[0] unchanged for non-bytes values, so the Template is passed straight to set_attribute. OTel attribute validation rejects non-primitives and emits one warning per execute call:
WARNING ... Invalid type Template for attribute 'db.statement' value.
Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types
-
get_operation_name (line 715) checks isinstance(args[0], str):
def get_operation_name(self, cursor, args):
if args and isinstance(args[0], str):
return self._leading_comment_remover.sub("", args[0]).split()[0]
return ""
Template is not a str, so this returns "". The caller falls back to the database (or instrumentation) name, so spans for t-string queries are named e.g. mydb instead of SELECT.
Net effect of Symptom 2: spans are created and the request succeeds, but db.statement is missing, span names are uninformative, and logs are spammed with warnings — one per t-string execute.
Steps to Reproduce
Both reproducers require a reachable PostgreSQL.
Symptom 1 reproducer (crash)
# repro_crash.py — Python 3.14
import asyncio
import os
from opentelemetry import trace
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
from opentelemetry.sdk.trace import TracerProvider
import psycopg
async def main() -> None:
trace.set_tracer_provider(TracerProvider())
PsycopgInstrumentor().instrument(enable_commenter=True)
dsn = os.environ["DATABASE_URL"] # e.g. postgres://user:pass@localhost/db
async with await psycopg.AsyncConnection.connect(dsn) as conn:
async with conn.cursor() as cur:
value = 1
query = t"SELECT {value} AS x"
await cur.execute(query)
print(await cur.fetchone())
asyncio.run(main())
Symptom 2 reproducer (warning + degraded span)
Identical to above but with the default instrumentation:
PsycopgInstrumentor().instrument() # enable_commenter not set
The query succeeds and prints (1,), but stderr shows:
Invalid type Template for attribute 'db.statement' value.
Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types
and the resulting span carries no db.statement attribute and uses the database name as its operation name instead of SELECT.
Expected Result
The instrumented call should behave identically to the uninstrumented one regardless of enable_commenter:
- Symptom 1: the t-string is passed to psycopg unchanged and renders to a parameterized query server-side. Optionally, the commenter rewrites the rendered SQL to append the comment without losing Template semantics.
- Symptom 2: spans for Template queries should carry a string
db.statement (e.g. parameterized SQL with placeholders) and a meaningful operation name (SELECT, INSERT, …).
Actual Result
-
Symptom 1: psycopg.errors.SyntaxError: syntax error at or near "Template" because _update_args_with_added_sql_comment calls str() on the Template, replacing args[0] with the Template's repr. The repr is then sent to PostgreSQL as raw SQL.
Full traceback (from a real application):
File ".../app/admin/router.py", line 302, in usage_data
await cursor.execute(query)
File ".../opentelemetry/instrumentation/psycopg/__init__.py", line 415, in execute
return await _cursor_tracer.traced_execution_async(
self, super().execute, *args, **kwargs
)
File ".../opentelemetry/instrumentation/dbapi/__init__.py", line 808, in traced_execution_async
return await query_method(*args, **kwargs)
File ".../psycopg/cursor_async.py", line 117, in execute
raise ex.with_traceback(None)
psycopg.errors.SyntaxError: syntax error at or near "Template"
LINE 1: Template(strings=('\n SELECT\n concat(\n ...
^
-
Symptom 2: query succeeds, but each call emits
WARNING opentelemetry.attributes ... Invalid type Template for attribute 'db.statement' value.
Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types
and the resulting span lacks db.statement and has a generic name.
Additional context
Disclaimer — AI-assisted report. This bug report (analysis, code excerpts, reproducers, and suggested fixes) was drafted in full by an AI coding agent based on a real failure observed in a downstream application. A human has reviewed it before filing but did not write the prose. Code references and line numbers were taken from the installed package versions listed below and may need re-checking against main.
PEP 750 string.templatelib.Template was added in Python 3.14. psycopg 3.3 accepts Templates in execute. The dbapi instrumentor currently special-cases psycopg2.sql.Composable (via the as_string attribute) but has no path for Templates.
A complete fix needs Template-aware handling at three call sites in CursorTracer:
-
_update_args_with_added_sql_comment (Symptom 1) — must not stringify the Template via str(). Two options:
- Skip commenter rewriting for Templates and leave
args[0] untouched.
- Render the Template via psycopg's own helpers (e.g.
ClientCursor.mogrify-style), append the comment, and pass the rendered SQL back as a plain string. This loses the prepared-statement benefit but matches commenter behavior for non-Template queries.
-
get_statement (Symptom 2, attribute) — render the Template to a string suitable for the db.statement attribute. The conventional shape is parameterized SQL with placeholders (values redacted). For example, for t"SELECT {x} FROM t WHERE id = {y}" return "SELECT ? FROM t WHERE id = ?" (or %s to match psycopg's native style).
-
get_operation_name (Symptom 2, span name) — extract the operation token from template.strings[0] instead of failing the isinstance(args[0], str) check.
Because rendering a Template safely requires the database driver, the cleanest place for fixes 1 and 2 is in opentelemetry-instrumentation-psycopg (driver-specific). Fix 3 (get_operation_name) and a guard in _update_args_with_added_sql_comment to skip-rather-than-crash on Templates can live in generic opentelemetry-instrumentation-dbapi.
Would you like to implement a fix?
None
Tip
React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.
Describe your environment
opentelemetry-api1.41.1opentelemetry-sdk1.41.1opentelemetry-instrumentation-psycopg0.62b1opentelemetry-instrumentation-dbapi0.62b1psycopg3.3.3psycopg-binary3.3.3psycopg-pool3.3.0What happened?
psycopg3.3+ on Python 3.14 accepts a PEP 750 t-string (t"...", astring.templatelib.Template) directly incursor.execute/await cursor.execute. The driver renders the Template safely server-side.opentelemetry-instrumentation-dbapi(and thereforeopentelemetry-instrumentation-psycopg, which delegates to it) has no path forTemplateobjects. Three call sites inCursorTracerassumeargs[0]is a string-like object, producing two distinct symptoms depending on configuration.Symptom 1 — query crashes when
enable_commenter=TrueWith
PsycopgInstrumentor().instrument(enable_commenter=True), the instrumented execute path goes throughCursorTracer._update_args_with_added_sql_comment, which mutatesargs[0]to a plain string before forwarding to the realexecute:For a
Template,hasattr(template, "as_string")isFalse, so the code falls through tostr(template). That returns the object's repr, e.g.:This repr-string is then passed to psycopg's real
execute, which sends it verbatim to PostgreSQL, producing:Symptom 2 — noisy warnings and degraded spans with
enable_commenter=False(default)With the default
PsycopgInstrumentor().instrument(), the commenter path is skipped, so the query reaches psycopg as aTemplateand executes correctly. However:CursorTracer._populate_spancallsspan.set_attribute(DB_STATEMENT, statement)wherestatementis theTemplate:get_statement(line 723) returnsargs[0]unchanged for non-bytesvalues, so the Template is passed straight toset_attribute. OTel attribute validation rejects non-primitives and emits one warning per execute call:get_operation_name(line 715) checksisinstance(args[0], str):Templateis not astr, so this returns"". The caller falls back to the database (or instrumentation) name, so spans for t-string queries are named e.g.mydbinstead ofSELECT.Net effect of Symptom 2: spans are created and the request succeeds, but
db.statementis missing, span names are uninformative, and logs are spammed with warnings — one per t-string execute.Steps to Reproduce
Both reproducers require a reachable PostgreSQL.
Symptom 1 reproducer (crash)
Symptom 2 reproducer (warning + degraded span)
Identical to above but with the default instrumentation:
The query succeeds and prints
(1,), but stderr shows:and the resulting span carries no
db.statementattribute and uses the database name as its operation name instead ofSELECT.Expected Result
The instrumented call should behave identically to the uninstrumented one regardless of
enable_commenter:db.statement(e.g. parameterized SQL with placeholders) and a meaningful operation name (SELECT,INSERT, …).Actual Result
Symptom 1:
psycopg.errors.SyntaxError: syntax error at or near "Template"because_update_args_with_added_sql_commentcallsstr()on the Template, replacingargs[0]with the Template's repr. The repr is then sent to PostgreSQL as raw SQL.Full traceback (from a real application):
Symptom 2: query succeeds, but each call emits
and the resulting span lacks
db.statementand has a generic name.Additional context
Disclaimer — AI-assisted report. This bug report (analysis, code excerpts, reproducers, and suggested fixes) was drafted in full by an AI coding agent based on a real failure observed in a downstream application. A human has reviewed it before filing but did not write the prose. Code references and line numbers were taken from the installed package versions listed below and may need re-checking against
main.PEP 750
string.templatelib.Templatewas added in Python 3.14. psycopg 3.3 accepts Templates inexecute. The dbapi instrumentor currently special-casespsycopg2.sql.Composable(via theas_stringattribute) but has no path for Templates.A complete fix needs Template-aware handling at three call sites in
CursorTracer:_update_args_with_added_sql_comment(Symptom 1) — must not stringify the Template viastr(). Two options:args[0]untouched.ClientCursor.mogrify-style), append the comment, and pass the rendered SQL back as a plain string. This loses the prepared-statement benefit but matches commenter behavior for non-Template queries.get_statement(Symptom 2, attribute) — render the Template to a string suitable for thedb.statementattribute. The conventional shape is parameterized SQL with placeholders (values redacted). For example, fort"SELECT {x} FROM t WHERE id = {y}"return"SELECT ? FROM t WHERE id = ?"(or%sto match psycopg's native style).get_operation_name(Symptom 2, span name) — extract the operation token fromtemplate.strings[0]instead of failing theisinstance(args[0], str)check.Because rendering a Template safely requires the database driver, the cleanest place for fixes 1 and 2 is in
opentelemetry-instrumentation-psycopg(driver-specific). Fix 3 (get_operation_name) and a guard in_update_args_with_added_sql_commentto skip-rather-than-crash on Templates can live in genericopentelemetry-instrumentation-dbapi.Would you like to implement a fix?
None
Tip
React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding
+1orme too, to help us triage it. Learn more here.