Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
26 changes: 24 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -34,7 +37,8 @@ pub struct PoolBuilder<DB: sqlx::Database> {
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<sqlx::Pool<sqlx::Postgres>> for PoolBuilder<sqlx::Postgres> {
/// Create a new builder from an existing SQLx pool.
Expand All @@ -54,7 +58,6 @@ impl From<sqlx::Pool<sqlx::Postgres>> for PoolBuilder<sqlx::Postgres> {
}
}

// this is required because `pool.connect_options().to_url_lossy()` panics with sqlite
#[cfg(feature = "sqlite")]
impl From<sqlx::Pool<sqlx::Sqlite>> for PoolBuilder<sqlx::Sqlite> {
/// Create a new builder from an existing SQLx pool.
Expand All @@ -73,6 +76,25 @@ impl From<sqlx::Pool<sqlx::Sqlite>> for PoolBuilder<sqlx::Sqlite> {
}
}

#[cfg(feature = "mysql")]
impl From<sqlx::Pool<sqlx::MySql>> for PoolBuilder<sqlx::MySql> {
/// Create a new builder from an existing SQLx pool.
fn from(pool: sqlx::Pool<sqlx::MySql>) -> 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<DB: sqlx::Database> PoolBuilder<DB> {
/// Set a custom name for the pool (for peer.service attribute).
pub fn with_name(mut self, name: impl Into<String>) -> Self {
Expand Down
3 changes: 3 additions & 0 deletions src/mysql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
impl crate::prelude::Database for sqlx::MySql {
const SYSTEM: &'static str = "mysql";
}
79 changes: 79 additions & 0 deletions tests/mysql.rs
Original file line number Diff line number Diff line change
@@ -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<testcontainers::GenericImage>,
}

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<MySql> {
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<T: Clone>() {}
assert_clone::<Pool<MySql>>();
}