sqlx_otel/database.rs
1/// Per-backend contract providing the database system name, connect-attribute extraction,
2/// and `rows_affected` projection.
3///
4/// Implemented by this crate for [`sqlx::Sqlite`], [`sqlx::Postgres`], and [`sqlx::MySql`]
5/// behind their respective feature flags. The trait exists so the generic wrapper types
6/// (`Pool`, `PoolConnection`, `Transaction`) can resolve connection attributes once at
7/// pool construction and project `rows_affected` from the per-backend `QueryResult` types.
8///
9/// **The trait is sealed.** It is an internal generic-dispatch contract, not an extension
10/// point. Additional backends would require both an upstream `sqlx::Database` impl and a release
11/// of this crate.
12///
13/// [`sqlx::Sqlite`]: https://docs.rs/sqlx/latest/sqlx/struct.Sqlite.html
14/// [`sqlx::Postgres`]: https://docs.rs/sqlx/latest/sqlx/struct.Postgres.html
15/// [`sqlx::MySql`]: https://docs.rs/sqlx/latest/sqlx/struct.MySql.html
16pub trait Database: sqlx::Database + sealed::Sealed {
17 /// The OpenTelemetry `db.system.name` value for this backend (e.g. `"postgresql"`,
18 /// `"sqlite"`, `"mysql"`).
19 const SYSTEM: &'static str;
20
21 /// Default `network.protocol.name` for this backend. `Some("postgresql")` /
22 /// `Some("mysql")` for the network-protocol backends; `None` for embedded backends
23 /// that do not speak a wire protocol (e.g. `SQLite`). Used by `PoolBuilder::from` to
24 /// pre-populate the attribute on every span and per-operation metric; override via
25 /// [`PoolBuilder::with_network_protocol_name`](crate::PoolBuilder::with_network_protocol_name).
26 const DEFAULT_NETWORK_PROTOCOL_NAME: Option<&'static str>;
27
28 /// Extract host, port, and database namespace from the backend's connect options.
29 ///
30 /// Returns `(host, port, namespace)` where any component may be `None` if the backend
31 /// does not support it (e.g. Sqlite has no host or port).
32 fn connection_attributes(
33 pool: &sqlx::Pool<Self>,
34 ) -> (Option<String>, Option<u16>, Option<String>);
35
36 /// Extract the number of rows affected from a `QueryResult`.
37 ///
38 /// Each `SQLx` backend defines its own `QueryResult` type with an inherent
39 /// `rows_affected()` method. This trait method provides a uniform interface for the
40 /// instrumentation layer.
41 fn rows_affected(result: &<Self as sqlx::Database>::QueryResult) -> u64;
42}
43
44/// Sealing module for [`Database`]. The supertrait bound on `Database: sealed::Sealed`
45/// prevents downstream impls because only this crate can implement [`Sealed`](self::sealed::Sealed)
46/// for the backend types.
47mod sealed {
48 /// Marker trait that prevents external impls of [`Database`](super::Database).
49 pub trait Sealed {}
50
51 #[cfg(feature = "sqlite")]
52 impl Sealed for sqlx::Sqlite {}
53
54 #[cfg(feature = "postgres")]
55 impl Sealed for sqlx::Postgres {}
56
57 #[cfg(feature = "mysql")]
58 impl Sealed for sqlx::MySql {}
59}
60
61/// Extract `(host, port, namespace)` from a network-style backend's connect options by
62/// rendering them to a URL and parsing the components.
63///
64/// Used by the Postgres and `MySQL` impls of [`Database::connection_attributes`] – both
65/// share the same URL-based extraction logic. `SQLite` supplies a filename instead and
66/// does not need this helper.
67#[cfg(any(feature = "postgres", feature = "mysql"))]
68fn url_based_connection_attributes<O: sqlx::ConnectOptions>(
69 options: &O,
70) -> (Option<String>, Option<u16>, Option<String>) {
71 let url = options.to_url_lossy();
72 let host = url.host_str().map(String::from);
73 let port = url.port();
74 let namespace = url
75 .path_segments()
76 .and_then(|mut segments| segments.next().map(String::from));
77 (host, port, namespace)
78}
79
80#[cfg(feature = "sqlite")]
81#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
82impl Database for sqlx::Sqlite {
83 const SYSTEM: &'static str = "sqlite";
84 const DEFAULT_NETWORK_PROTOCOL_NAME: Option<&'static str> = None;
85
86 fn connection_attributes(
87 pool: &sqlx::Pool<Self>,
88 ) -> (Option<String>, Option<u16>, Option<String>) {
89 let namespace = pool
90 .connect_options()
91 .get_filename()
92 .to_str()
93 .map(String::from);
94 (None, None, namespace)
95 }
96
97 fn rows_affected(result: &sqlx::sqlite::SqliteQueryResult) -> u64 {
98 result.rows_affected()
99 }
100}
101
102#[cfg(feature = "postgres")]
103#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
104impl Database for sqlx::Postgres {
105 const SYSTEM: &'static str = "postgresql";
106 const DEFAULT_NETWORK_PROTOCOL_NAME: Option<&'static str> = Some("postgresql");
107
108 fn connection_attributes(
109 pool: &sqlx::Pool<Self>,
110 ) -> (Option<String>, Option<u16>, Option<String>) {
111 url_based_connection_attributes(pool.connect_options().as_ref())
112 }
113
114 fn rows_affected(result: &sqlx::postgres::PgQueryResult) -> u64 {
115 result.rows_affected()
116 }
117}
118
119#[cfg(feature = "mysql")]
120#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
121impl Database for sqlx::MySql {
122 const SYSTEM: &'static str = "mysql";
123 const DEFAULT_NETWORK_PROTOCOL_NAME: Option<&'static str> = Some("mysql");
124
125 fn connection_attributes(
126 pool: &sqlx::Pool<Self>,
127 ) -> (Option<String>, Option<u16>, Option<String>) {
128 url_based_connection_attributes(pool.connect_options().as_ref())
129 }
130
131 fn rows_affected(result: &sqlx::mysql::MySqlQueryResult) -> u64 {
132 result.rows_affected()
133 }
134}