From 19b42759cd97cc162c32760ef8674422db8a00b1 Mon Sep 17 00:00:00 2001 From: ash-krnl Date: Thu, 27 Aug 2026 18:43:46 +0530 Subject: [PATCH] fix(instrumentation): name a DB span for its operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core derives a DB span's semantic type by matching the verb inside the span NAME (`classifyDBType` in internal/content/session.go) and never reads `db_operation`, which this SDK already sends. Two of the three DB span sites named every span "db query": dbapi start_span("db query") asyncpg start_span("db query") sqlalchemy start_span(f"db {verb.lower()}") <- already correct So every statement through sqlite3, psycopg2, mysql, pymysql or asyncpg was stored as a generic `database_query`, and a SELECT was indistinguishable from a DELETE in the record — while `db_statement` sat right there carrying the verb. Only the SQLAlchemy path classified correctly, which is what made this look like a driver quirk rather than a naming bug. All three sites now share one `_db_span_name` helper, so the odd one out is the convention rather than the exception. Core upper-cases the name before matching, so the verb's case here does not matter; a statement with no readable verb keeps "db query", the previous name. Verified end to end against a local stack: five statements through sqlite3 now store as database_query (CREATE), database_insert, database_select, database_update and database_delete, where all five were database_query before. 390 tests pass. The 5 failures in tests/instrumentation are unrelated to this change and reproduce on an unmodified tree — they need asyncpg and pymongo, which are not installed. Co-Authored-By: Claude Opus 5 (1M context) --- openbox_core/instrumentation/db.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/openbox_core/instrumentation/db.py b/openbox_core/instrumentation/db.py index c261a33..68fa3f8 100644 --- a/openbox_core/instrumentation/db.py +++ b/openbox_core/instrumentation/db.py @@ -33,6 +33,23 @@ _SPAN_KEY = "_openbox_db_span" +def _db_span_name(statement: str | None) -> str: + """``db `` — the name a consumer classifies the operation from. + + Core derives the DB semantic type by matching the verb inside the span NAME + (`classifyDBType`), not from `db_operation`, which it never reads. A span + named "db query" therefore lands as a generic `database_query` however + precise the statement is, so a SELECT and a DELETE are indistinguishable in + the record. Core upper-cases the name before matching, so the verb's case + here does not matter. + + Falls back to "db query" when there is no statement to read a verb from — + the previous name for every DB-API and asyncpg span. + """ + verb = (statement or "").strip().split(" ", 1)[0] + return f"db {verb.lower()}" if verb else "db query" + + def _db_fields( statement: str | None, system: str, @@ -110,7 +127,7 @@ def _before_cursor_execute(conn, cursor, statement, parameters, context, execute runtime = get_hook_runtime() if runtime is None: return - span = get_tracer().start_span(f"db {statement.strip().split(' ', 1)[0].lower()}") + span = get_tracer().start_span(_db_span_name(statement)) if context is not None: setattr(context, _SPAN_KEY, span) dialect, db_name, host, port = _sqlalchemy_conn_meta(conn) @@ -244,7 +261,7 @@ def governed_traced_execution(tracer_self, cursor, query_method, *args, **kwargs return _original_traced_execution(tracer_self, cursor, query_method, *args, **kwargs) statement = tracer_self.get_statement(cursor, args) if args else "" system_name, db_name, host, port = _dbapi_conn_meta(tracer_self) - span = get_tracer().start_span("db query") + span = get_tracer().start_span(_db_span_name(str(statement))) def _fields() -> dict: return _db_fields( @@ -320,7 +337,7 @@ async def governed_execute(conn_self, query, *args, **kwargs): if runtime is None: return await _original_asyncpg_execute(conn_self, query, *args, **kwargs) db_name, host, port = _asyncpg_conn_meta(conn_self) - span = get_tracer().start_span("db query") + span = get_tracer().start_span(_db_span_name(str(query))) def _fields() -> dict: return _db_fields(