Skip to content

Add sqlcommenter feature for traceparent SQL comments#2

Open
Diggsey wants to merge 4 commits into
db-tracing-spans-transactionsfrom
db-tracing-spans-sqlcommenter
Open

Add sqlcommenter feature for traceparent SQL comments#2
Diggsey wants to merge 4 commits into
db-tracing-spans-transactionsfrom
db-tracing-spans-sqlcommenter

Conversation

@Diggsey
Copy link
Copy Markdown

@Diggsey Diggsey commented May 21, 2026

Opt-in feature that appends /*traceparent='00-{trace}-{span}-{flags}'*/ to outgoing SQL on PostgreSQL and MySQL so server-side observability tools (Cloud SQL Insights, AlloyDB Insights, pg_stat_statements consumers preserving comments) can link query stats back to upstream OTel traces.

sqlx_core::sqlcommenter::comment_for_span reads the OTel trace context by briefly entering the QueryLogger's span and consulting opentelemetry::Context::current() — that resolves to the span's OTel context as long as the user has tracing-opentelemetry's Layer installed. Without a layer or a valid trace context, returns None and the query goes out unmodified.

maybe_append_comment returns Cow::Borrowed on the no-context / feature-off path, so the hot path allocates nothing. The MySQL executor's try_stream! body needs to capture by move, so it goes a step further: builds an Option<String> outside the generator and falls back to logger.sql().as_str() inside, keeping the no-comment fast path allocation-free.

Cost when the feature kicks in: the comment is part of the SQL string used as the prepared-statement cache key, so each call with a fresh trace id re-prepares the statement. That's the price of correlating prepared queries with traces; it's why the feature is opt-in.

SQLite is intentionally skipped — there's no managed observability consumer that would parse the comments.

Optional dep: opentelemetry = "0.27" (trace feature only), gated behind the sqlcommenter feature on sqlx-core and passed through from sqlx-postgres, sqlx-mysql, and the top-level sqlx crate.

Does your PR solve an issue?

Delete this text and add "fixes #(issue number)".

Do not just list issue numbers here as they will not be automatically closed on merging this pull request unless prefixed with "fixes" or "closes".

Is this a breaking change?

Delete this text and answer yes/no and explain.

If yes, this pull request will need to wait for the next major release (0.{x + 1}.0)

Behavior changes can be breaking if significant enough.
Consider Hyrum's Law:

With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.

Opt-in feature that appends `/*traceparent='00-{trace}-{span}-{flags}'*/`
to outgoing SQL on PostgreSQL and MySQL so server-side observability
tools (Cloud SQL Insights, AlloyDB Insights, pg_stat_statements
consumers preserving comments) can link query stats back to upstream
OTel traces.

`sqlx_core::sqlcommenter::comment_for_span` reads the OTel trace
context by briefly entering the `QueryLogger`'s span and consulting
`opentelemetry::Context::current()` — that resolves to the span's OTel
context as long as the user has `tracing-opentelemetry`'s `Layer`
installed. Without a layer or a valid trace context, returns `None`
and the query goes out unmodified.

`maybe_append_comment` returns `Cow::Borrowed` on the no-context /
feature-off path, so the hot path allocates nothing. The MySQL
executor's `try_stream!` body needs to capture by move, so it goes a
step further: builds an `Option<String>` outside the generator and
falls back to `logger.sql().as_str()` inside, keeping the no-comment
fast path allocation-free.

Cost when the feature kicks in: the comment is part of the SQL string
used as the prepared-statement cache key, so each call with a fresh
trace id re-prepares the statement. That's the price of correlating
prepared queries with traces; it's why the feature is opt-in.

SQLite is intentionally skipped — there's no managed observability
consumer that would parse the comments.

Optional dep: `opentelemetry = "0.27"` (trace feature only), gated
behind the `sqlcommenter` feature on `sqlx-core` and passed through
from `sqlx-postgres`, `sqlx-mysql`, and the top-level `sqlx` crate.
@Diggsey Diggsey force-pushed the db-tracing-spans-sqlcommenter branch from 0f03bd1 to b2d011c Compare May 21, 2026 17:33
Diggsey added 3 commits May 26, 2026 16:29
…ontext::current

The previous implementation entered the QueryLogger span and read
`opentelemetry::Context::current()`, on the assumption that
tracing-opentelemetry's Layer syncs the OTel current context with the
entered tracing span. It doesn't — `Layer::on_enter` only updates
timing data; nothing ever calls `Context::attach()`. So `current()`
returned the default (empty) context for every query, the carrier
stayed empty, `comment_for_span` returned `None`, and no comment was
ever appended. The downstream `persistent` override in the executors
therefore never triggered, leaving every query going out as a cached
named prepared statement with no traceparent — exactly the "no trace
correlation in Cloud SQL Insights" symptom.

Switch to `tracing_opentelemetry::OpenTelemetrySpanExt::context()`,
which downcasts the subscriber to find the OTel layer's `WithContext`
and builds an `opentelemetry::Context` from the `OtelData` extension
stashed on the span at `on_new_span` time. That's the documented way
to extract the OTel context from a tracing span, and it works whether
or not the span is currently entered. If no OTel layer is installed,
`context()` falls through to `Context::default()` and the existing
empty-carrier check still produces `None`, so the no-context fast path
is unchanged.

Pulls in tracing-opentelemetry as a feature-gated optional dep (no
extra cost for users not opting into sqlcommenter).
Forcing `persistent = false` made `prepare()` send `Parse(UNNAMED)`,
but `resolve_statement_metadata` and `apply_patches` may issue
`raw_sql(...).fetch_all` (a simple Query) to resolve custom types,
which destroys the unnamed statement before Bind runs and fails with
"unnamed prepared statement does not exist".

Split the old `persistent` flag in `get_or_prepare` into separate
`named` and `cache` axes. The sqlcommenter path now uses a named
statement (immune to simple-query side trips) without caching, and
`Close::Statement`s it after Execute so Postgres doesn't accumulate
one-shot statements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant