Skip to main content

sqlx_otel/
lib.rs

1//! Lightweight [`SQLx`](https://docs.rs/sqlx) wrapper that emits OpenTelemetry-native spans
2//! and metrics following the [database client semantic conventions][semconv].
3//!
4//! `sqlx-otel` talks to the [`opentelemetry`] API directly – there is no `tracing` bridge
5//! indirection. The wrapper is **zero-cost when no `TracerProvider` or `MeterProvider` is
6//! installed** because the global `opentelemetry` API resolves to no-op instruments in that
7//! configuration; the only overhead is one `Arc` clone per acquired connection or transaction.
8//!
9//! [semconv]: https://opentelemetry.io/docs/specs/semconv/database/
10//!
11//! # Quick start
12//!
13//! Wrap an existing `sqlx::Pool` with [`PoolBuilder`] and use the result anywhere a `sqlx::Pool`
14//! is accepted – every operation through the wrapper is instrumented.
15//!
16//! ```no_run
17//! # #[cfg(feature = "sqlite")]
18//! # async fn _doc() -> Result<(), sqlx::Error> {
19//! use sqlx_otel::PoolBuilder;
20//!
21//! // Wrap an existing sqlx pool. Connection-level attributes (host, port, db namespace) are
22//! // auto-extracted from the underlying connect options.
23//! let raw = sqlx::SqlitePool::connect(":memory:").await?;
24//! let pool = PoolBuilder::from(raw).build();
25//!
26//! // Use it exactly like a sqlx pool – `&pool` is an `sqlx::Executor`.
27//! let row: (i64,) = sqlx::query_as("SELECT 1").fetch_one(&pool).await?;
28//! assert_eq!(row.0, 1);
29//!
30//! // Transactions work via `&mut tx` (note: not `&mut *tx`; the wrapper does not deref).
31//! let mut tx = pool.begin().await?;
32//! sqlx::query("CREATE TABLE users (name TEXT)")
33//!     .execute(&mut tx)
34//!     .await?;
35//! tx.commit().await?;
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! Every operation through the pool emits a [`SpanKind::Client`][SpanKind] span, records the
41//! operation duration, and tracks pool-level metrics. No code changes are required beyond the
42//! one-line wrap.
43//!
44//! [SpanKind]: https://docs.rs/opentelemetry/latest/opentelemetry/trace/enum.SpanKind.html
45//!
46//! # Setting up an OpenTelemetry SDK
47//!
48//! `sqlx-otel` produces telemetry but **does not install a provider** – that is the application's
49//! responsibility. Pair this crate with the [`opentelemetry_sdk`] (or any other compliant SDK) and
50//! set up exporters for your traces and metrics. Until a provider is installed via
51//! [`opentelemetry::global::set_tracer_provider`] / [`set_meter_provider`][m], the instrumentation
52//! is a no-op.
53//!
54//! [`opentelemetry_sdk`]: https://docs.rs/opentelemetry_sdk
55//! [m]: https://docs.rs/opentelemetry/latest/opentelemetry/global/fn.set_meter_provider.html
56//!
57//! # What gets emitted
58//!
59//! ## Spans
60//!
61//! Every [`sqlx::Executor`] method (`execute`, `fetch`, `fetch_all`, `fetch_one`, `fetch_optional`,
62//! `fetch_many`, `execute_many`, `prepare`, `prepare_with`, `describe`) produces a
63//! `SpanKind::Client` span. The span name follows the [semantic-convention hierarchy][naming]:
64//! `db.query.summary` (when set) → `"{operation} {collection}"` → `"{operation}"` → `"{db.system.name}"`.
65//!
66//! [naming]: https://opentelemetry.io/docs/specs/semconv/database/database-spans/#name
67//!
68//! | Attribute                   | Source                                                   | Condition                   |
69//! |-----------------------------|----------------------------------------------------------|-----------------------------|
70//! | `db.system.name`            | Backend (`"postgresql"`, `"sqlite"`, `"mysql"`)          | Always                      |
71//! | `db.namespace`              | Database name extracted from connect options             | When available              |
72//! | `server.address`            | Hostname extracted from connect options                  | When available              |
73//! | `server.port`               | Port extracted from connect options                      | When available              |
74//! | `network.peer.address`      | Resolved IP address                                      | When set via builder        |
75//! | `network.peer.port`         | Resolved port                                            | When set via builder        |
76//! | `db.query.text`             | The SQL query string with inter-token whitespace collapsed | Unless [`QueryTextMode::Off`] |
77//! | `db.operation.name`         | Database operation (e.g. `SELECT`)                       | When [annotated]            |
78//! | `db.collection.name`        | Target table or collection                               | When [annotated]            |
79//! | `db.query.summary`          | Low-cardinality query summary                            | When [annotated]            |
80//! | `db.stored_procedure.name`  | Stored procedure name                                    | When [annotated]            |
81//! | `db.response.returned_rows` | Row count                                                | On `fetch*` methods         |
82//! | `db.response.affected_rows` | Rows affected (`QueryResult::rows_affected()`)           | On `execute` ([note](#a-note-on-dbresponseaffected_rows)) |
83//! | `db.response.status_code`   | SQLSTATE / driver error code                             | On database errors          |
84//! | `error.type`                | Error variant name                                       | On any error                |
85//!
86//! [annotated]: #per-query-annotations
87//!
88//! On error, the span status is set to `Error` and an `exception` event is added carrying
89//! `exception.type` and `exception.message`.
90//!
91//! ## Operation metrics
92//!
93//! | Instrument                         | Type      | Unit | Description                           |
94//! |------------------------------------|-----------|------|---------------------------------------|
95//! | `db.client.operation.duration`     | Histogram | `s`  | Duration of each database operation   |
96//! | `db.client.response.returned_rows` | Histogram |      | Number of rows returned per operation |
97//!
98//! These carry the connection-level attributes (`db.system.name`, `db.namespace`, `server.address`,
99//! `server.port`).
100//!
101//! ## Connection-pool metrics
102//!
103//! | Instrument                              | Type            | Unit | Description                                          |
104//! |-----------------------------------------|-----------------|------|------------------------------------------------------|
105//! | `db.client.connection.wait_time`        | Histogram       | `s`  | Time spent waiting for a connection in `acquire()`   |
106//! | `db.client.connection.use_time`         | Histogram       | `s`  | Time a connection was held before being returned     |
107//! | `db.client.connection.timeouts`         | Counter         |      | Number of acquire attempts that timed out            |
108//! | `db.client.connection.pending_requests` | `UpDownCounter` |      | Number of callers currently waiting in `acquire()`   |
109//! | `db.client.connection.count`            | Gauge           |      | Current connections by state (`idle` / `used`)       |
110//! | `db.client.connection.max`              | Gauge           |      | Maximum number of connections allowed                |
111//! | `db.client.connection.idle.max`         | Gauge           |      | Maximum idle connections (equals `max` in `SQLx`)    |
112//! | `db.client.connection.idle.min`         | Gauge           |      | Configured minimum connections                       |
113//!
114//! The first four are recorded inline on every `acquire()` and connection drop – no sampling
115//! gaps. `connection.count` is polled by a background task and requires both
116//! [`PoolBuilder::with_pool_name`] and a runtime feature (`runtime-tokio` or
117//! `runtime-async-std`); without either, the gauge is silent. The remaining three are static
118//! gauges recorded once at [`PoolBuilder::build`].
119//!
120//! ## A note on `db.response.affected_rows`
121//!
122//! `db.response.affected_rows` is **not part of the OpenTelemetry semantic conventions** at
123//! the time of writing; it is a custom attribute we find useful. It carries the
124//! database-confirmed count from `QueryResult::rows_affected()` on every `execute()` call,
125//! using the same connection-level attributes as the standard `db.response.returned_rows`.
126//! It is not recorded for `execute_many`, which is [considered deprecated upstream][exec-many].
127//!
128//! [exec-many]: https://github.com/launchbadge/sqlx/issues/3108
129//!
130//! # Per-query annotations
131//!
132//! `sqlx-otel` does **not** parse SQL. The four per-query semantic-convention attributes –
133//! `db.operation.name`, `db.collection.name`, `db.query.summary`, and
134//! `db.stored_procedure.name` – are the caller's responsibility, supplied through the
135//! annotation API. There are two equivalent surfaces depending on whether you prefer the
136//! annotation to live next to the executor or next to the query:
137//!
138//! **Executor-side** ([`Pool::with_annotations`], [`PoolConnection::with_annotations`],
139//! [`Transaction::with_annotations`]) returns a borrowed wrapper that is itself an
140//! `sqlx::Executor`. Use it when the same query text is reused with one set of annotations,
141//! or when you want to annotate `prepare` / `describe` flows.
142//!
143//! ```no_run
144//! # #[cfg(feature = "sqlite")]
145//! # async fn _doc() -> Result<(), sqlx::Error> {
146//! # use sqlx_otel::PoolBuilder;
147//! use sqlx::Executor as _; // brings `fetch_all` / `execute` into scope.
148//! use sqlx_otel::QueryAnnotations;
149//! # let pool = PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
150//!
151//! pool.with_annotations(
152//!     QueryAnnotations::new()
153//!         .operation("SELECT")
154//!         .collection("users"),
155//! )
156//! .fetch_all("SELECT * FROM users")
157//! .await?;
158//!
159//! // Shorthand for the common two-attribute case.
160//! pool.with_operation("INSERT", "orders")
161//!     .execute("INSERT INTO orders (id) VALUES (1)")
162//!     .await?;
163//! # Ok(())
164//! # }
165//! ```
166//!
167//! **Query-side** ([`QueryAnnotateExt`]) attaches the annotation directly to the
168//! `sqlx::query` / `sqlx::query_as` / `sqlx::query_scalar` builder. Use it when annotations
169//! belong with the query text – the typical case. Works with `Query::map` /
170//! `Query::try_map` and with the compile-time validated macro forms.
171//!
172//! ```no_run
173//! # #[cfg(feature = "sqlite")]
174//! # async fn _doc() -> Result<(), sqlx::Error> {
175//! # use sqlx_otel::PoolBuilder;
176//! use sqlx_otel::QueryAnnotateExt;
177//! # let pool = PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
178//!
179//! sqlx::query("INSERT INTO orders (user_id) VALUES (?)")
180//!     .bind(7_i64)
181//!     .with_operation("INSERT", "orders")
182//!     .execute(&pool)
183//!     .await?;
184//! # Ok(())
185//! # }
186//! ```
187//!
188//! See [`QueryAnnotations`] for the full set of fields and [`QueryAnnotateExt`] for the
189//! query-side surface (including the three valid annotation positions on `Query::map`
190//! chains).
191//!
192//! # Error handling
193//!
194//! All wrapper methods return `sqlx::Error` unchanged – there is no error wrapping or
195//! retranslation. When an error surfaces, the active span's status is set to `Error`, an
196//! `exception` event is added with `exception.type` and `exception.message`, and –
197//! whenever the error is a [`sqlx::Error::Database`] with a SQLSTATE/driver code – the
198//! `db.response.status_code` attribute is recorded. The instrumentation is otherwise
199//! transparent: the caller sees exactly the same `Result` it would see without the wrapper.
200//!
201//! # Feature flags
202//!
203//! `sqlx-otel` ships with **no default features** – pick at least one backend.
204//!
205//! | Feature             | Effect                                                              |
206//! |---------------------|---------------------------------------------------------------------|
207//! | `sqlite`            | Enable the `sqlx::Sqlite` backend.                                  |
208//! | `postgres`          | Enable the `sqlx::Postgres` backend.                                |
209//! | `mysql`             | Enable the `sqlx::MySql` backend.                                   |
210//! | `runtime-tokio`     | Spawn the `db.client.connection.count` polling task with `tokio`.   |
211//! | `runtime-async-std` | Same as above, but with `async-std`. Ignored if `tokio` is enabled. |
212//!
213//! ```toml
214//! [dependencies]
215//! sqlx-otel = { version = "0.1", features = ["postgres", "runtime-tokio"] }
216//! ```
217//!
218//! All operation- and pool-level metrics other than `db.client.connection.count` work
219//! without a runtime feature.
220//!
221//! # MSRV
222//!
223//! Minimum supported Rust version: **1.85.0**.
224
225#![cfg_attr(docsrs, feature(doc_cfg))]
226
227#[macro_use]
228mod annotations;
229pub(crate) mod attributes;
230mod compact;
231mod connection;
232mod database;
233mod executor;
234mod metrics;
235mod obfuscate;
236mod pool;
237mod pool_metrics;
238mod query_ext;
239mod runtime;
240mod transaction;
241
242pub use annotations::{Annotated, AnnotatedMut, QueryAnnotations};
243pub use attributes::QueryTextMode;
244pub use connection::PoolConnection;
245pub use database::Database;
246pub use pool::{Pool, PoolBuilder};
247pub use query_ext::{AnnotatedQuery, QueryAnnotateExt};
248pub use transaction::Transaction;