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 /// Extract host, port, and database namespace from the backend's connect options.
22 ///
23 /// Returns `(host, port, namespace)` where any component may be `None` if the backend
24 /// does not support it (e.g. Sqlite has no host or port).
25 fn connection_attributes(
26 pool: &sqlx::Pool<Self>,
27 ) -> (Option<String>, Option<u16>, Option<String>);
28
29 /// Extract the number of rows affected from a `QueryResult`.
30 ///
31 /// Each `SQLx` backend defines its own `QueryResult` type with an inherent
32 /// `rows_affected()` method. This trait method provides a uniform interface for the
33 /// instrumentation layer.
34 fn rows_affected(result: &<Self as sqlx::Database>::QueryResult) -> u64;
35}
36
37/// Sealing module for [`Database`]. The supertrait bound on `Database: sealed::Sealed`
38/// prevents downstream impls because only this crate can implement [`Sealed`](self::sealed::Sealed)
39/// for the backend types.
40mod sealed {
41 /// Marker trait that prevents external impls of [`Database`](super::Database).
42 pub trait Sealed {}
43
44 #[cfg(feature = "sqlite")]
45 impl Sealed for sqlx::Sqlite {}
46
47 #[cfg(feature = "postgres")]
48 impl Sealed for sqlx::Postgres {}
49
50 #[cfg(feature = "mysql")]
51 impl Sealed for sqlx::MySql {}
52}
53
54/// Extract `(host, port, namespace)` from a network-style backend's connect options by
55/// rendering them to a URL and parsing the components.
56///
57/// Used by the Postgres and `MySQL` impls of [`Database::connection_attributes`] – both
58/// share the same URL-based extraction logic. `SQLite` supplies a filename instead and
59/// does not need this helper.
60#[cfg(any(feature = "postgres", feature = "mysql"))]
61fn url_based_connection_attributes<O: sqlx::ConnectOptions>(
62 options: &O,
63) -> (Option<String>, Option<u16>, Option<String>) {
64 let url = options.to_url_lossy();
65 let host = url.host_str().map(String::from);
66 let port = url.port();
67 let namespace = url
68 .path_segments()
69 .and_then(|mut segments| segments.next().map(String::from));
70 (host, port, namespace)
71}
72
73#[cfg(feature = "sqlite")]
74#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
75impl Database for sqlx::Sqlite {
76 const SYSTEM: &'static str = "sqlite";
77
78 fn connection_attributes(
79 pool: &sqlx::Pool<Self>,
80 ) -> (Option<String>, Option<u16>, Option<String>) {
81 let namespace = pool
82 .connect_options()
83 .get_filename()
84 .to_str()
85 .map(String::from);
86 (None, None, namespace)
87 }
88
89 fn rows_affected(result: &sqlx::sqlite::SqliteQueryResult) -> u64 {
90 result.rows_affected()
91 }
92}
93
94#[cfg(feature = "postgres")]
95#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
96impl Database for sqlx::Postgres {
97 const SYSTEM: &'static str = "postgresql";
98
99 fn connection_attributes(
100 pool: &sqlx::Pool<Self>,
101 ) -> (Option<String>, Option<u16>, Option<String>) {
102 url_based_connection_attributes(pool.connect_options().as_ref())
103 }
104
105 fn rows_affected(result: &sqlx::postgres::PgQueryResult) -> u64 {
106 result.rows_affected()
107 }
108}
109
110#[cfg(feature = "mysql")]
111#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
112impl Database for sqlx::MySql {
113 const SYSTEM: &'static str = "mysql";
114
115 fn connection_attributes(
116 pool: &sqlx::Pool<Self>,
117 ) -> (Option<String>, Option<u16>, Option<String>) {
118 url_based_connection_attributes(pool.connect_options().as_ref())
119 }
120
121 fn rows_affected(result: &sqlx::mysql::MySqlQueryResult) -> u64 {
122 result.rows_affected()
123 }
124}