Repository navigation
feat(database): make the statement span the only span a statement gets #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@maple-dev/effect-orm": minor | ||
| --- | ||
|
|
||
| A statement's client span is now the only span it gets. It carries `db.query.text` (the statement as sent), and on failure `db.response.status_code` (the SQLSTATE) beside `error.type`, so the SQL driver's own span, which repeated the query text under the database name, runs untraced. Pass `driverSpans: true` to `fromSqlClient` to keep it as a child for what only the driver records, such as `server.address` or connection waits. `observe` runs inside the statement span, so attributes it annotates land there. Plain SQL (`Db.sql`) spans are named after the statement's leading operation and first table (`UPDATE items`) instead of the database system, with `db.operation.name` and `db.collection.name` to match. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,9 @@ | |
| // database semantic conventions describe: | ||
| // https://opentelemetry.io/docs/specs/semconv/database/database-spans/ | ||
| // | ||
| // Effect's own span still runs inside it with the query text, but it cannot | ||
| // carry these: it is named after the database, not the operation, and only a | ||
| // compiled query knows its table. | ||
| // It is the only span a statement gets: it carries the query text, so the SQL | ||
| // driver's own span (named after the database, not the operation, and blind to | ||
| // the table) runs untraced unless `driverSpans` asks for it. | ||
| import { Context, Effect } from "effect" | ||
| import type { CompiledQuery } from "../ch/compile" | ||
| import type { Dialect } from "../ch/dialect" | ||
|
|
@@ -40,6 +40,22 @@ | |
| delete: "DELETE", | ||
| } | ||
|
|
||
| // Plain SQL has no compiled operation or table, so they are read off its text, | ||
| // as the conventions allow: the first operation and the first collection named. | ||
| const LEADING_WORD = /^\s*(\w+)/ | ||
| const FIRST_TABLE = /(?:from|into|update|join|table)\s+\W?([\w.]+)/i | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning
F2 · Warning · correctness
🤖 Prompt to fix with an AI agent |
||
| /** String literals, with `''` and backslash escapes, so text inside them is not read as a table. */ | ||
| const STRING_LITERAL = /'(?:[^'\\]|\\.|'')*'/g | ||
|
|
||
| /** The operation and table a statement names: compiled, or read off plain SQL. */ | ||
| const describeStatement = (statement: TracedStatement): { operation?: string; collection?: string } => { | ||
| if (statement.kind !== undefined) return { operation: OPERATIONS[statement.kind], collection: statement.collection } | ||
| return { | ||
| operation: LEADING_WORD.exec(statement.sql)?.[1]?.toUpperCase(), | ||
| collection: FIRST_TABLE.exec(statement.sql.replace(STRING_LITERAL, "''"))?.[1], | ||
|
Check warning on line 55 in src/database/tracing.ts
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Runs `effect`, which sends `statement`, in its client span. The span name is | ||
| * the summary when there is one, then `{operation} {table}`, then the system. | ||
|
|
@@ -54,19 +70,24 @@ | |
| Effect.withFiber((fiber) => { | ||
| const configured = fiber.getRef(QuerySummary) | ||
| const system = SYSTEM_NAMES[dialect.name] ?? dialect.name | ||
| const operation = statement.kind === undefined ? undefined : OPERATIONS[statement.kind] | ||
| const { operation, collection } = describeStatement(statement) | ||
| const summary = | ||
| configured ?? (operation === undefined ? undefined : statement.collection === undefined ? operation : `${operation} ${statement.collection}`) | ||
| const attributes: Record<string, unknown> = { "db.system.name": system } | ||
| configured ?? (operation === undefined ? undefined : collection === undefined ? operation : `${operation} ${collection}`) | ||
| const attributes: Record<string, unknown> = { "db.system.name": system, "db.query.text": statement.sql } | ||
| if (operation !== undefined) attributes["db.operation.name"] = operation | ||
| if (statement.collection !== undefined) attributes["db.collection.name"] = statement.collection | ||
| if (collection !== undefined) attributes["db.collection.name"] = collection | ||
| if (summary !== undefined) attributes["db.query.summary"] = summary | ||
| return effect.pipe( | ||
| Effect.tap((result) => | ||
| rowsOf === undefined ? Effect.void : Effect.annotateCurrentSpan("db.response.returned_rows", rowsOf(result)), | ||
| ), | ||
| // `error.type` is the failure's class: a DatabaseError's reason, else its tag. | ||
| // `db.response.status_code` is the SQLSTATE, when the server sent one. | ||
| Effect.tapError((error) => { | ||
| const sqlState = | ||
| typeof error === "object" && error !== null && "sqlState" in error && typeof error.sqlState === "string" | ||
| ? error.sqlState | ||
| : undefined | ||
| const reason = | ||
| typeof error === "object" && error !== null | ||
| ? "reason" in error && typeof error.reason === "string" | ||
|
|
@@ -75,7 +96,10 @@ | |
| ? error._tag | ||
| : undefined | ||
| : undefined | ||
| return reason === undefined ? Effect.void : Effect.annotateCurrentSpan("error.type", reason) | ||
| const failure: Record<string, unknown> = {} | ||
| if (reason !== undefined) failure["error.type"] = reason | ||
| if (sqlState !== undefined) failure["db.response.status_code"] = sqlState | ||
| return Effect.annotateCurrentSpan(failure) | ||
| }), | ||
| Effect.withSpan(summary ?? system, { kind: "client", attributes }), | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning
Statement spans no longer name the peer now the driver span is off
F1 · Warning · observability · MAP-01
driver()switches the tracer off, so the span that carried the peer is gone:src/clickhouse-http/client.ts:279putsdb.namespace,server.address,server.portandpeer.serviceon that driver span only. Statement spans keepdb.system.namealone, so the service-map edge promised indocs/running-queries.md:110disappears for everyDbcaller.🤖 Prompt to fix with an AI agent