Skip to content

dbapi / PsycopgInstrumentor does not handle PEP 750 t-string queries #4690

Description

@tlinhart

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:

  1. 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
    
  2. 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:

  1. _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.
  2. 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).

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions