diff --git a/Cargo.toml b/Cargo.toml index 1b14b85..293e2e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ categories = ["database", "development-tools::debugging", "development-tools::pr [features] postgres = ["sqlx/postgres"] sqlite = ["sqlx/sqlite"] +mysql = ["sqlx/mysql"] [dependencies] futures = { version = "0.3" } diff --git a/README.md b/README.md index 309bbdc..00f74b4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **OpenTelemetry Integration**: Traces are compatible with OpenTelemetry, making it easy to export to collectors and observability platforms. - **Error Recording**: Errors are automatically annotated with kind, message, and stacktrace in the tracing span. - **Returned Rows**: The number of rows returned by queries is recorded for observability. -- **Database Agnostic**: Supports both PostgreSQL and SQLite via feature flags. +- **Database Agnostic**: Supports PostgreSQL, MySQL, and SQLite via feature flags. - **Macros**: Includes a macro for consistent span creation around queries. ## Usage @@ -25,6 +25,7 @@ tracing = "0.1" Enable the desired database feature: - For PostgreSQL: `features = ["postgres"]` +- For MySQL: `features = ["mysql"]` - For SQLite: `features = ["sqlite"]` Wrap your SQLx pool: @@ -74,7 +75,7 @@ To export traces, set up an OpenTelemetry collector and configure the tracing su ## Testing -Integration tests are provided for both PostgreSQL and SQLite, using [testcontainers](https://docs.rs/testcontainers) and a local OpenTelemetry collector. +Integration tests are provided for PostgreSQL, MySQL, and SQLite, using [testcontainers](https://docs.rs/testcontainers) and a local OpenTelemetry collector. ## License diff --git a/src/lib.rs b/src/lib.rs index db86288..f59931c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ pub mod postgres; #[cfg(feature = "sqlite")] pub mod sqlite; +#[cfg(feature = "mysql")] +pub mod mysql; + /// Attributes describing the database connection and context. /// Used for span enrichment and attribute propagation. #[derive(Debug, Default)] @@ -34,7 +37,8 @@ pub struct PoolBuilder { attributes: Attributes, } -// this is required because `pool.connect_options().to_url_lossy()` panics with sqlite +// URL-based attribute extraction — works for TCP-backed drivers (postgres, mysql). +// Sqlite has its own impl below because `to_url_lossy()` panics on sqlite options. #[cfg(feature = "postgres")] impl From> for PoolBuilder { /// Create a new builder from an existing SQLx pool. @@ -54,7 +58,6 @@ impl From> for PoolBuilder { } } -// this is required because `pool.connect_options().to_url_lossy()` panics with sqlite #[cfg(feature = "sqlite")] impl From> for PoolBuilder { /// Create a new builder from an existing SQLx pool. @@ -73,6 +76,25 @@ impl From> for PoolBuilder { } } +#[cfg(feature = "mysql")] +impl From> for PoolBuilder { + /// Create a new builder from an existing SQLx pool. + fn from(pool: sqlx::Pool) -> Self { + use sqlx::ConnectOptions; + + let url = pool.connect_options().to_url_lossy(); + let attributes = Attributes { + name: None, + host: url.host_str().map(String::from), + port: url.port(), + database: url + .path_segments() + .and_then(|mut segments| segments.next().map(String::from)), + }; + Self { pool, attributes } + } +} + impl PoolBuilder { /// Set a custom name for the pool (for peer.service attribute). pub fn with_name(mut self, name: impl Into) -> Self { diff --git a/src/mysql.rs b/src/mysql.rs new file mode 100644 index 0000000..924a0d8 --- /dev/null +++ b/src/mysql.rs @@ -0,0 +1,3 @@ +impl crate::prelude::Database for sqlx::MySql { + const SYSTEM: &'static str = "mysql"; +} diff --git a/tests/mysql.rs b/tests/mysql.rs new file mode 100644 index 0000000..5598ef6 --- /dev/null +++ b/tests/mysql.rs @@ -0,0 +1,79 @@ +#![cfg(feature = "mysql")] + +use std::time::Duration; + +use sqlx::MySql; +use sqlx_tracing::Pool; +use testcontainers::{ + GenericImage, ImageExt, + core::{ContainerPort, WaitFor}, + runners::AsyncRunner, +}; + +mod common; + +#[derive(Debug)] +struct MySqlContainer { + container: testcontainers::ContainerAsync, +} + +impl MySqlContainer { + async fn create() -> Self { + let container = GenericImage::new("mysql", "8") + .with_wait_for(WaitFor::message_on_stderr( + "ready for connections. Bind-address: '::' port: 3306", + )) + .with_exposed_port(ContainerPort::Tcp(3306)) + .with_env_var("MYSQL_ALLOW_EMPTY_PASSWORD", "yes") + .with_env_var("MYSQL_DATABASE", "test") + .with_startup_timeout(Duration::from_secs(120)) + .start() + .await + .expect("starting a mysql database"); + + Self { container } + } + + async fn client(&self) -> sqlx_tracing::Pool { + let port = self.container.get_host_port_ipv4(3306).await.unwrap(); + let url = format!("mysql://root@localhost:{port}/test"); + sqlx::MySqlPool::connect(&url) + .await + .map(sqlx_tracing::Pool::from) + .unwrap() + } +} + +#[tokio::test] +async fn execute() { + let observability = opentelemetry_testing::ObservabilityContainer::create().await; + let provider = observability.install().await; + + let container = MySqlContainer::create().await; + let pool = container.client().await; + + common::should_trace("trace_pool", "mysql", &observability, &provider, &pool).await; + + { + let mut conn = pool.acquire().await.unwrap(); + common::should_trace("trace_conn", "mysql", &observability, &provider, &mut conn).await; + } + + { + let mut tx: sqlx_tracing::Transaction<'_, MySql> = pool.begin().await.unwrap(); + common::should_trace( + "trace_tx", + "mysql", + &observability, + &provider, + &mut tx.executor(), + ) + .await; + } +} + +#[test] +fn pool_mysql_is_clone() { + fn assert_clone() {} + assert_clone::>(); +}