Skip to main content

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}